add stub MainActivity

This commit is contained in:
Daniel Gultsch 2023-02-19 15:40:56 +01:00
parent c105c3420e
commit 87e33a779f
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
29 changed files with 540 additions and 30 deletions

View file

@ -106,6 +106,10 @@ dependencies {
// XMPP Address library // XMPP Address library
implementation 'org.jxmpp:jxmpp-jid:1.0.3' implementation 'org.jxmpp:jxmpp-jid:1.0.3'
// Consistent Color Generation
implementation 'org.hsluv:hsluv:0.2'
// DNS library (XMPP needs to resolve SRV records) // DNS library (XMPP needs to resolve SRV records)
implementation 'de.measite.minidns:minidns-hla:0.2.4' implementation 'de.measite.minidns:minidns-hla:0.2.4'

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "070e419bfe6857a47cda745017f04a57", "identityHash": "6186e2691813f4fbd804b90fd770e18b",
"entities": [ "entities": [
{ {
"tableName": "account", "tableName": "account",
@ -2290,7 +2290,7 @@
}, },
{ {
"tableName": "roster_group", "tableName": "roster_group",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rosterItemId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`rosterItemId`, `groupId`), FOREIGN KEY(`rosterItemId`) REFERENCES `roster`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`groupId`) REFERENCES `group`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rosterItemId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`rosterItemId`, `groupId`), FOREIGN KEY(`rosterItemId`) REFERENCES `roster`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`groupId`) REFERENCES `group`(`id`) ON UPDATE NO ACTION ON DELETE RESTRICT )",
"fields": [ "fields": [
{ {
"fieldPath": "rosterItemId", "fieldPath": "rosterItemId",
@ -2337,7 +2337,7 @@
}, },
{ {
"table": "group", "table": "group",
"onDelete": "CASCADE", "onDelete": "RESTRICT",
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"columns": [ "columns": [
"groupId" "groupId"
@ -2352,7 +2352,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '070e419bfe6857a47cda745017f04a57')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6186e2691813f4fbd804b90fd770e18b')"
] ]
} }
} }

View file

@ -2,7 +2,6 @@ package im.conversations.android;
import android.app.Application; import android.app.Application;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import com.google.android.material.color.DynamicColors;
import im.conversations.android.dns.Resolver; import im.conversations.android.dns.Resolver;
import im.conversations.android.notification.Channels; import im.conversations.android.notification.Channels;
import im.conversations.android.xmpp.ConnectionPool; import im.conversations.android.xmpp.ConnectionPool;
@ -32,6 +31,6 @@ public class Conversations extends Application {
ConnectionPool.getInstance(this).reconfigure(); ConnectionPool.getInstance(this).reconfigure();
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); // For night mode theme AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); // For night mode theme
DynamicColors.applyToActivitiesIfAvailable(this); // DynamicColors.applyToActivitiesIfAvailable(this);
} }
} }

View file

@ -1,11 +1,13 @@
package im.conversations.android.database.dao; package im.conversations.android.database.dao;
import androidx.lifecycle.LiveData;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Insert; import androidx.room.Insert;
import androidx.room.Query; import androidx.room.Query;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import im.conversations.android.database.entity.AccountEntity; import im.conversations.android.database.entity.AccountEntity;
import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Account;
import im.conversations.android.database.model.AccountIdentifier;
import im.conversations.android.database.model.Connection; import im.conversations.android.database.model.Connection;
import java.util.List; import java.util.List;
import org.jxmpp.jid.BareJid; import org.jxmpp.jid.BareJid;
@ -28,6 +30,9 @@ public interface AccountDao {
@Query("SELECT id,address,randomSeed FROM account WHERE id=:id AND enabled=1") @Query("SELECT id,address,randomSeed FROM account WHERE id=:id AND enabled=1")
ListenableFuture<Account> getEnabledAccount(long id); ListenableFuture<Account> getEnabledAccount(long id);
@Query("SELECT id,address FROM account")
LiveData<List<AccountIdentifier>> getAccounts();
@Query("SELECT hostname,port,directTls FROM account WHERE id=:id AND hostname != null") @Query("SELECT hostname,port,directTls FROM account WHERE id=:id AND hostname != null")
Connection getConnectionSettings(long id); Connection getConnectionSettings(long id);

View file

@ -1,5 +1,6 @@
package im.conversations.android.database.dao; package im.conversations.android.database.dao;
import androidx.lifecycle.LiveData;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Insert; import androidx.room.Insert;
import androidx.room.Query; import androidx.room.Query;
@ -10,6 +11,7 @@ import im.conversations.android.database.model.ChatIdentifier;
import im.conversations.android.database.model.ChatType; import im.conversations.android.database.model.ChatType;
import im.conversations.android.xmpp.model.stanza.Message; import im.conversations.android.xmpp.model.stanza.Message;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@Dao @Dao
@ -52,4 +54,7 @@ public abstract class ChatDao {
@Insert @Insert
protected abstract long insert(ChatEntity chatEntity); protected abstract long insert(ChatEntity chatEntity);
@Query("SELECT name FROM `group` ORDER BY name")
public abstract LiveData<List<String>> getGroups();
} }

View file

@ -13,10 +13,14 @@ import im.conversations.android.database.model.Account;
import im.conversations.android.xmpp.model.roster.Item; import im.conversations.android.xmpp.model.roster.Item;
import java.util.Collection; import java.util.Collection;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Dao @Dao
public abstract class RosterDao extends GroupDao { public abstract class RosterDao extends GroupDao {
private static final Logger LOGGER = LoggerFactory.getLogger(RosterDao.class);
@Insert(onConflict = REPLACE) @Insert(onConflict = REPLACE)
protected abstract long insert(RosterItemEntity rosterItem); protected abstract long insert(RosterItemEntity rosterItem);
@ -32,6 +36,7 @@ public abstract class RosterDao extends GroupDao {
@Transaction @Transaction
public void set( public void set(
final Account account, final String version, final Collection<Item> rosterItems) { final Account account, final String version, final Collection<Item> rosterItems) {
LOGGER.info("items: " + rosterItems);
clear(account.id); clear(account.id);
for (final Item item : rosterItems) { for (final Item item : rosterItems) {
final long id = insert(RosterItemEntity.of(account.id, item)); final long id = insert(RosterItemEntity.of(account.id, item));

View file

@ -12,30 +12,14 @@ import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
import org.jxmpp.jid.BareJid; import org.jxmpp.jid.BareJid;
public class Account { public class Account extends AccountIdentifier {
public final long id;
@NonNull public final BareJid address;
@NonNull public final byte[] randomSeed; @NonNull public final byte[] randomSeed;
@Override
public String toString() {
return "Account{"
+ "id="
+ id
+ ", address="
+ address
+ ", randomSeed="
+ Arrays.toString(randomSeed)
+ '}';
}
public Account(final long id, @NonNull final BareJid address, @NonNull byte[] randomSeed) { public Account(final long id, @NonNull final BareJid address, @NonNull byte[] randomSeed) {
Preconditions.checkNotNull(address, "Account can not be instantiated without an address"); super(id, address);
Preconditions.checkArgument( Preconditions.checkArgument(
randomSeed.length == 32, "RandomSeed must have exactly 32 bytes"); randomSeed.length == 32, "RandomSeed must have exactly 32 bytes");
this.id = id;
this.address = address;
this.randomSeed = randomSeed; this.randomSeed = randomSeed;
} }
@ -43,15 +27,14 @@ public class Account {
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Account account = (Account) o; Account account = (Account) o;
return id == account.id return Arrays.equals(randomSeed, account.randomSeed);
&& Objects.equal(address, account.address)
&& Arrays.equals(randomSeed, account.randomSeed);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode(id, address, randomSeed); return Objects.hashCode(super.hashCode(), randomSeed);
} }
public boolean isOnion() { public boolean isOnion() {

View file

@ -0,0 +1,31 @@
package im.conversations.android.database.model;
import androidx.annotation.NonNull;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import org.jxmpp.jid.BareJid;
public class AccountIdentifier {
public final long id;
@NonNull public final BareJid address;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AccountIdentifier that = (AccountIdentifier) o;
return id == that.id && Objects.equal(address, that.address);
}
@Override
public int hashCode() {
return Objects.hashCode(id, address);
}
public AccountIdentifier(long id, @NonNull BareJid address) {
Preconditions.checkNotNull(address, "Account can not be instantiated without an address");
this.id = id;
this.address = address;
}
}

View file

@ -2,6 +2,7 @@ package im.conversations.android.repository;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -9,11 +10,13 @@ import im.conversations.android.IDs;
import im.conversations.android.database.CredentialStore; import im.conversations.android.database.CredentialStore;
import im.conversations.android.database.entity.AccountEntity; import im.conversations.android.database.entity.AccountEntity;
import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Account;
import im.conversations.android.database.model.AccountIdentifier;
import im.conversations.android.xmpp.ConnectionPool; import im.conversations.android.xmpp.ConnectionPool;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.manager.RegistrationManager; import im.conversations.android.xmpp.manager.RegistrationManager;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.List;
import org.jxmpp.jid.BareJid; import org.jxmpp.jid.BareJid;
public class AccountRepository extends AbstractRepository { public class AccountRepository extends AbstractRepository {
@ -107,6 +110,10 @@ public class AccountRepository extends AbstractRepository {
ConnectionPool.getInstance(context).reconnect(account); ConnectionPool.getInstance(context).reconnect(account);
} }
public LiveData<List<AccountIdentifier>> getAccounts() {
return database.accountDao().getAccounts();
}
public static class AccountAlreadyExistsException extends IllegalStateException { public static class AccountAlreadyExistsException extends IllegalStateException {
public AccountAlreadyExistsException(BareJid address) { public AccountAlreadyExistsException(BareJid address) {
super(String.format("The account %s has already been setup", address)); super(String.format("The account %s has already been setup", address));

View file

@ -0,0 +1,16 @@
package im.conversations.android.repository;
import android.content.Context;
import androidx.lifecycle.LiveData;
import java.util.List;
public class ChatRepository extends AbstractRepository {
public ChatRepository(Context context) {
super(context);
}
public LiveData<List<String>> getGroups() {
return this.database.chatDao().getGroups();
}
}

View file

@ -2,7 +2,11 @@ package im.conversations.android.ui.activity;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import im.conversations.android.R;
import im.conversations.android.databinding.ActivityMainBinding;
import im.conversations.android.service.ForegroundService; import im.conversations.android.service.ForegroundService;
import im.conversations.android.ui.Activities;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
@ -10,5 +14,8 @@ public class MainActivity extends AppCompatActivity {
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ForegroundService.start(this); ForegroundService.start(this);
final ActivityMainBinding binding =
DataBindingUtil.setContentView(this, R.layout.activity_main);
Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
} }
} }

View file

@ -0,0 +1,102 @@
package im.conversations.android.ui.fragment.main;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.elevation.SurfaceColors;
import com.google.android.material.search.SearchView;
import im.conversations.android.R;
import im.conversations.android.database.model.AccountIdentifier;
import im.conversations.android.databinding.FragmentOverviewBinding;
import im.conversations.android.ui.activity.SetupActivity;
import im.conversations.android.ui.model.OverviewViewModel;
import java.util.List;
public class OverviewFragment extends Fragment {
private FragmentOverviewBinding binding;
private OverviewViewModel overviewViewModel;
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
this.binding =
DataBindingUtil.inflate(inflater, R.layout.fragment_overview, container, false);
final ViewModelProvider viewModelProvider =
new ViewModelProvider(this, getDefaultViewModelProviderFactory());
this.overviewViewModel = viewModelProvider.get(OverviewViewModel.class);
binding.setLifecycleOwner(getViewLifecycleOwner());
binding.searchBar.setNavigationOnClickListener(
view -> {
binding.drawerLayout.open();
});
binding.searchView.addTransitionListener(
(searchView, previousState, newState) -> {
final var activity = requireActivity();
final var window = activity.getWindow();
if (newState == SearchView.TransitionState.SHOWN) {
window.setStatusBarColor(SurfaceColors.SURFACE_4.getColor(activity));
} else if (newState == SearchView.TransitionState.SHOWING
|| newState == SearchView.TransitionState.HIDING) {
window.setStatusBarColor(SurfaceColors.SURFACE_1.getColor(activity));
} else {
window.setStatusBarColor(SurfaceColors.SURFACE_0.getColor(activity));
}
});
binding.navigationView.setNavigationItemSelectedListener(
item -> {
if (item.getItemId() == R.id.add_account) {
startActivity(new Intent(requireContext(), SetupActivity.class));
}
return true;
});
binding.navigationView.setCheckedItem(R.id.chats);
this.overviewViewModel
.getAccounts()
.observe(getViewLifecycleOwner(), this::onAccountsUpdated);
this.overviewViewModel.getGroups().observe(getViewLifecycleOwner(), this::onGroupsUpdated);
return binding.getRoot();
}
private void onGroupsUpdated(final List<String> groups) {
final var menu = this.binding.navigationView.getMenu();
final var menuItemSpaces = menu.findItem(R.id.spaces);
if (groups.isEmpty()) {
menuItemSpaces.setVisible(false);
return;
}
menuItemSpaces.setVisible(true);
final var subMenu = menuItemSpaces.getSubMenu();
subMenu.clear();
for (final String group : groups) {
final var menuItemSpace = subMenu.add(group);
menuItemSpace.setCheckable(true);
menuItemSpace.setIcon(R.drawable.ic_workspaces_24dp);
}
}
private void onAccountsUpdated(List<AccountIdentifier> accounts) {
final var menu = this.binding.navigationView.getMenu();
final var menuItemAccounts = menu.findItem(R.id.accounts);
if (accounts.size() <= 1) {
menuItemAccounts.setVisible(false);
return;
}
menuItemAccounts.setVisible(true);
final var subMenu = menuItemAccounts.getSubMenu();
subMenu.clear();
for (final AccountIdentifier account : accounts) {
final var menuItemAccount = subMenu.add(account.address);
menuItemAccount.setCheckable(true);
menuItemAccount.setIcon(R.drawable.ic_person_24dp);
}
}
}

View file

@ -0,0 +1,31 @@
package im.conversations.android.ui.model;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Transformations;
import im.conversations.android.database.model.AccountIdentifier;
import im.conversations.android.repository.AccountRepository;
import im.conversations.android.repository.ChatRepository;
import java.util.List;
public class OverviewViewModel extends AndroidViewModel {
private final AccountRepository accountRepository;
private final ChatRepository chatRepository;
public OverviewViewModel(@NonNull Application application) {
super(application);
this.accountRepository = new AccountRepository(application);
this.chatRepository = new ChatRepository(application);
}
public LiveData<List<AccountIdentifier>> getAccounts() {
return Transformations.distinctUntilChanged(this.accountRepository.getAccounts());
}
public LiveData<List<String>> getGroups() {
return Transformations.distinctUntilChanged(this.chatRepository.getGroups());
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2019-2021 Daniel Gultsch
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.conversations.android.util;
import android.content.Context;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import com.google.android.material.color.MaterialColors;
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import org.hsluv.HUSLColorConverter;
import org.jxmpp.jid.BareJid;
public final class ConsistentColorGeneration {
private ConsistentColorGeneration() {
throw new IllegalStateException("This is a Utility class");
}
@SuppressWarnings("deprecation")
private static double angle(final String input) {
final byte[] digest = Hashing.sha1().hashString(input, Charsets.UTF_8).asBytes();
final int angle = ((int) (digest[0]) & 0xff) + ((int) (digest[1]) & 0xff) * 256;
return angle / 65536.0;
}
@ColorInt
public static int rgb(final String input) {
final double[] rgb =
HUSLColorConverter.hsluvToRgb(new double[] {angle(input) * 360, 100, 50});
return rgb(
(int) Math.round(rgb[0] * 255),
(int) Math.round(rgb[1] * 255),
(int) Math.round(rgb[2] * 255));
}
@ColorInt
public static int harmonized(final Context context, final String input) {
return MaterialColors.harmonizeWithPrimary(context, rgb(input));
}
public static int harmonized(final Context context, final BareJid jid) {
return harmonized(context, jid.toString());
}
@ColorInt
private static int rgb(
@IntRange(from = 0, to = 255) int red,
@IntRange(from = 0, to = 255) int green,
@IntRange(from = 0, to = 255) int blue) {
return 0xff000000 | (red << 16) | (green << 8) | blue;
}
}

View file

@ -2014,8 +2014,7 @@ public class XmppConnection implements Runnable {
public boolean fromAccount(final Stanza stanza) { public boolean fromAccount(final Stanza stanza) {
final Jid from = stanza.getFrom(); final Jid from = stanza.getFrom();
// TODO null is valid too?! return from == null || from.asBareJid().equals(connectionAddress.asBareJid());
return from != null && from.asBareJid().equals(connectionAddress.asBareJid());
} }
public boolean toAccount(final Stanza stanza) { public boolean toAccount(final Stanza stanza) {

View file

@ -1,7 +1,9 @@
package im.conversations.android.xmpp.model.roster; package im.conversations.android.xmpp.model.roster;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class Group extends Extension { public class Group extends Extension {
public Group() { public Group() {

View file

@ -69,6 +69,7 @@ public class IqProcessor extends XmppConnection.Delegate implements Consumer<Iq>
} }
final var extensionIds = packet.getExtensionIds(); final var extensionIds = packet.getExtensionIds();
LOGGER.info("Received from {} type {}", packet.getFrom(), type);
LOGGER.info("Could not handle {}. Sending feature-not-implemented", extensionIds); LOGGER.info("Could not handle {}. Sending feature-not-implemented", extensionIds);
connection.sendErrorFor(packet, new Condition.FeatureNotImplemented()); connection.sendErrorFor(packet, new Condition.FeatureNotImplemented());
} }

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z" />
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" />
<path
android:fillColor="@android:color/white"
android:pathData="M10.67,13.02C10.45,13.01 10.23,13 10,13c-2.42,0 -4.68,0.67 -6.61,1.82C2.51,15.34 2,16.32 2,17.35V20h9.26C10.47,18.87 10,17.49 10,16C10,14.93 10.25,13.93 10.67,13.02z" />
<path
android:fillColor="@android:color/white"
android:pathData="M20.75,16c0,-0.22 -0.03,-0.42 -0.06,-0.63l1.14,-1.01l-1,-1.73l-1.45,0.49c-0.32,-0.27 -0.68,-0.48 -1.08,-0.63L18,11h-2l-0.3,1.49c-0.4,0.15 -0.76,0.36 -1.08,0.63l-1.45,-0.49l-1,1.73l1.14,1.01c-0.03,0.21 -0.06,0.41 -0.06,0.63s0.03,0.42 0.06,0.63l-1.14,1.01l1,1.73l1.45,-0.49c0.32,0.27 0.68,0.48 1.08,0.63L16,21h2l0.3,-1.49c0.4,-0.15 0.76,-0.36 1.08,-0.63l1.45,0.49l1,-1.73l-1.14,-1.01C20.72,16.42 20.75,16.22 20.75,16zM17,18c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S18.1,18 17,18z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M13,8c0,-2.21 -1.79,-4 -4,-4S5,5.79 5,8s1.79,4 4,4S13,10.21 13,8zM15,10v2h3v3h2v-3h3v-2h-3V7h-2v3H15zM1,18v2h16v-2c0,-2.66 -5.33,-4 -8,-4S1,15.34 1,18z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,13c-2.2,0 -4,1.8 -4,4s1.8,4 4,4s4,-1.8 4,-4S8.2,13 6,13zM12,3C9.8,3 8,4.8 8,7s1.8,4 4,4s4,-1.8 4,-4S14.2,3 12,3zM18,13c-2.2,0 -4,1.8 -4,4s1.8,4 4,4s4,-1.8 4,-4S20.2,13 18,13z" />
</vector>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2019 Daniel Gultsch
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/main_navigation" />
</FrameLayout>
</layout>

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:openDrawer="start">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="0dp"
app:liftOnScroll="false">
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search_in_chats"
app:navigationIcon="@drawable/ic_menu_24dp"/>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.search.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/search_in_chats"
app:layout_anchor="@id/search_bar"
app:useDrawerArrowDrawable="true">
<!-- Search suggestions/results go here (ScrollView, RecyclerView, etc.). -->
</com.google.android.material.search.SearchView>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/extended_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_gravity="bottom|end"
android:text="@string/start_chat"
app:icon="@drawable/ic_chat_24dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<!-- Screen content -->
<!-- Use app:layout_behavior="@string/appbar_scrolling_view_behavior" to fit below top app bar -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/fragment_overview"
app:headerLayout="@layout/item_navigation_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
</layout>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="24dp"
android:layout_marginHorizontal="24dp"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:textColor="?attr/colorOnSurface"
android:text="@string/app_name" />
</LinearLayout>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/chats"
android:checkable="true"
android:icon="@drawable/ic_chat_24dp"
android:title="@string/all_chats" />
<item
android:id="@+id/accounts"
android:title="@string/accounts">
<menu />
</item>
<item
android:id="@+id/spaces"
android:title="@string/spaces">
<menu/>
</item>
<group android:id="@+id/navigation">
<item
android:id="@+id/add_account"
android:checkable="true"
android:icon="@drawable/ic_person_add_24dp"
android:title="@string/action_add_account" />
<item
android:id="@+id/manage_accounts"
android:checkable="true"
android:icon="@drawable/ic_manage_accounts_24dp"
android:title="@string/title_activity_manage_accounts" />
<item
android:id="@+id/settings"
android:checkable="true"
android:icon="@drawable/ic_settings_24dp"
android:title="@string/action_settings" />
</group>
</menu>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2019 Daniel Gultsch
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_navigation"
app:startDestination="@+id/overview">
<fragment
android:id="@+id/overview"
android:name="im.conversations.android.ui.fragment.main.OverviewFragment"
tools:layout="@layout/fragment_overview" />
</navigation>

View file

@ -1011,5 +1011,10 @@
<string name="welcome">Welcome</string> <string name="welcome">Welcome</string>
<string name="no_account_register">No account? Register</string> <string name="no_account_register">No account? Register</string>
<string name="account_settings">Account settings</string> <string name="account_settings">Account settings</string>
<string name="search_in_chats">Search in chats</string>
<string name="start_chat">Start chat</string>
<string name="all_chats">All chats</string>
<string name="accounts">Accounts</string>
<string name="spaces">Spaces</string>
</resources> </resources>