diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index 9257a2d26..3971df6e8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -28,6 +28,7 @@ import org.briarproject.bramble.plugin.tor.AndroidTorPluginFactory; import org.briarproject.bramble.util.AndroidUtils; import org.briarproject.bramble.util.StringUtils; import org.briarproject.briar.android.account.LockManagerImpl; +import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.keyagreement.ContactExchangeModule; import org.briarproject.briar.android.login.LoginModule; import org.briarproject.briar.android.navdrawer.NavDrawerModule; @@ -68,6 +69,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; ViewModelModule.class, DevReportModule.class, // below need to be within same scope as ViewModelProvider.Factory + ForumModule.BindsModule.class, GroupListModule.class, }) public class AppModule { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListAdapter.java index 77cebffac..c97b491ae 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListAdapter.java @@ -1,134 +1,47 @@ package org.briarproject.briar.android.forum; -import android.content.Context; -import android.content.Intent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; -import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; -import org.briarproject.briar.android.util.BriarAdapter; -import org.briarproject.briar.android.util.UiUtils; -import org.briarproject.briar.android.view.TextAvatarView; -import org.briarproject.briar.api.forum.Forum; -import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.DiffUtil.ItemCallback; +import androidx.recyclerview.widget.ListAdapter; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static androidx.recyclerview.widget.SortedList.INVALID_POSITION; -import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; -import static org.briarproject.briar.android.activity.BriarActivity.GROUP_NAME; +@NotNullByDefault +class ForumListAdapter extends ListAdapter { -class ForumListAdapter - extends BriarAdapter { - - ForumListAdapter(Context ctx) { - super(ctx, ForumListItem.class); + ForumListAdapter() { + super(new ForumListCallback()); } @Override public ForumViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater.from(ctx).inflate( + View v = LayoutInflater.from(parent.getContext()).inflate( R.layout.list_item_forum, parent, false); return new ForumViewHolder(v); } @Override - public void onBindViewHolder(ForumViewHolder ui, int position) { - ForumListItem item = getItemAt(position); - if (item == null) return; + public void onBindViewHolder(ForumViewHolder viewHolder, int position) { + viewHolder.bind(getItem(position)); + } - // Avatar - ui.avatar.setText(item.getForum().getName().substring(0, 1)); - ui.avatar.setBackgroundBytes(item.getForum().getId().getBytes()); - ui.avatar.setUnreadCount(item.getUnreadCount()); - - // Forum Name - ui.name.setText(item.getForum().getName()); - - // Post Count - int postCount = item.getPostCount(); - if (postCount > 0) { - ui.postCount.setText(ctx.getResources() - .getQuantityString(R.plurals.posts, postCount, - postCount)); - } else { - ui.postCount.setText(ctx.getString(R.string.no_posts)); + @NotNullByDefault + private static class ForumListCallback extends ItemCallback { + @Override + public boolean areItemsTheSame(ForumListItem a, ForumListItem b) { + return a.equals(b); } - // Date - if (item.isEmpty()) { - ui.date.setVisibility(GONE); - } else { - long timestamp = item.getTimestamp(); - ui.date.setText(UiUtils.formatDate(ctx, timestamp)); - ui.date.setVisibility(VISIBLE); - } - - // Open Forum on Click - ui.layout.setOnClickListener(v -> { - Intent i = new Intent(ctx, ForumActivity.class); - Forum f = item.getForum(); - i.putExtra(GROUP_ID, f.getId().getBytes()); - i.putExtra(GROUP_NAME, f.getName()); - ctx.startActivity(i); - }); - } - - @Override - public int compare(ForumListItem a, ForumListItem b) { - if (a == b) return 0; - // The forum with the newest message comes first - long aTime = a.getTimestamp(), bTime = b.getTimestamp(); - if (aTime > bTime) return -1; - if (aTime < bTime) return 1; - // Break ties by forum name - String aName = a.getForum().getName(); - String bName = b.getForum().getName(); - return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); - } - - @Override - public boolean areContentsTheSame(ForumListItem a, ForumListItem b) { - return a.isEmpty() == b.isEmpty() && - a.getTimestamp() == b.getTimestamp() && - a.getUnreadCount() == b.getUnreadCount(); - } - - @Override - public boolean areItemsTheSame(ForumListItem a, ForumListItem b) { - return a.getForum().equals(b.getForum()); - } - - int findItemPosition(GroupId g) { - int count = getItemCount(); - for (int i = 0; i < count; i++) { - ForumListItem item = getItemAt(i); - if (item != null && item.getForum().getGroup().getId().equals(g)) - return i; - } - return INVALID_POSITION; // Not found - } - - static class ForumViewHolder extends RecyclerView.ViewHolder { - - private final ViewGroup layout; - private final TextAvatarView avatar; - private final TextView name; - private final TextView postCount; - private final TextView date; - - private ForumViewHolder(View v) { - super(v); - - layout = (ViewGroup) v; - avatar = v.findViewById(R.id.avatarView); - name = v.findViewById(R.id.forumNameView); - postCount = v.findViewById(R.id.postCountView); - date = v.findViewById(R.id.dateView); + @Override + public boolean areContentsTheSame(ForumListItem a, ForumListItem b) { + return a.isEmpty() == b.isEmpty() && + a.getTimestamp() == b.getTimestamp() && + a.getUnreadCount() == b.getUnreadCount(); } } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java index 8d7735133..093968a68 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java @@ -12,80 +12,48 @@ import android.view.ViewGroup; import com.google.android.material.snackbar.Snackbar; -import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.NoSuchGroupException; -import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.event.GroupAddedEvent; -import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; -import org.briarproject.briar.android.fragment.BaseEventFragment; +import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.sharing.ForumInvitationActivity; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; -import org.briarproject.briar.api.android.AndroidNotificationManager; -import org.briarproject.briar.api.client.MessageTracker.GroupCount; -import org.briarproject.briar.api.forum.Forum; -import org.briarproject.briar.api.forum.ForumManager; -import org.briarproject.briar.api.forum.ForumPostHeader; -import org.briarproject.briar.api.forum.ForumSharingManager; -import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent; -import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; -import androidx.annotation.UiThread; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE; -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logDuration; -import static org.briarproject.bramble.util.LogUtils.logException; -import static org.briarproject.bramble.util.LogUtils.now; -import static org.briarproject.briar.api.forum.ForumManager.CLIENT_ID; +import static java.util.Objects.requireNonNull; @MethodsNotNullByDefault @ParametersNotNullByDefault -public class ForumListFragment extends BaseEventFragment implements +public class ForumListFragment extends BaseFragment implements OnClickListener { public final static String TAG = ForumListFragment.class.getName(); - private final static Logger LOG = Logger.getLogger(TAG); + private ForumListViewModel viewModel; private BriarRecyclerView list; - private ForumListAdapter adapter; private Snackbar snackbar; + private final ForumListAdapter adapter = new ForumListAdapter(); @Inject - AndroidNotificationManager notificationManager; - - // Fields that are accessed from background threads must be volatile - @Inject - volatile ForumManager forumManager; - @Inject - volatile ForumSharingManager forumSharingManager; + ViewModelProvider.Factory viewModelFactory; public static ForumListFragment newInstance() { - - Bundle args = new Bundle(); - - ForumListFragment fragment = new ForumListFragment(); - fragment.setArguments(args); - return fragment; + return new ForumListFragment(); } @Override public void injectFragment(ActivityComponent component) { component.inject(this); + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(ForumListViewModel.class); } @Nullable @@ -93,24 +61,35 @@ public class ForumListFragment extends BaseEventFragment implements public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - requireActivity().setTitle(R.string.forums_button); - View contentView = - inflater.inflate(R.layout.fragment_forum_list, container, - false); + View v = inflater.inflate(R.layout.fragment_forum_list, container, + false); - adapter = new ForumListAdapter(getActivity()); - - list = contentView.findViewById(R.id.forumList); + list = v.findViewById(R.id.forumList); list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setAdapter(adapter); + viewModel.getForumListItems().observe(getViewLifecycleOwner(), result -> + result.onError(this::handleException).onSuccess(items -> { + adapter.submitList(items); + if (requireNonNull(items).size() == 0) list.showData(); + }) + ); snackbar = new BriarSnackbarBuilder() .setAction(R.string.show, this) .make(list, "", LENGTH_INDEFINITE); + viewModel.getNumInvitations().observe(getViewLifecycleOwner(), num -> { + if (num == 0) { + snackbar.dismiss(); + } else { + snackbar.setText(getResources().getQuantityString( + R.plurals.forums_shared, num, num)); + if (!snackbar.isShownOrQueued()) snackbar.show(); + } + }); - return contentView; + return v; } @Override @@ -121,19 +100,23 @@ public class ForumListFragment extends BaseEventFragment implements @Override public void onStart() { super.onStart(); - // TODO block all forum post notifications as well - notificationManager.clearAllForumPostNotifications(); - loadForums(); - loadAvailableForums(); + viewModel.blockAllForumPostNotifications(); + viewModel.clearAllForumPostNotifications(); + // The attributes and sorting of the forums may have changed while we + // were stopped and we have no way finding out about them, so re-load + // e.g. less unread posts in a forum after viewing it. + viewModel.loadForums(); + // The number of invitations might have changed while we were stopped + // e.g. because of accepting an invitation which does not trigger event + viewModel.loadForumInvitations(); list.startPeriodicUpdate(); } @Override public void onStop() { super.onStop(); - adapter.clear(); - list.showProgressBar(); list.stopPeriodicUpdate(); + viewModel.unblockAllForumPostNotifications(); } @Override @@ -145,123 +128,12 @@ public class ForumListFragment extends BaseEventFragment implements @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle presses on the action bar items - switch (item.getItemId()) { - case R.id.action_create_forum: - Intent intent = - new Intent(getContext(), CreateForumActivity.class); - startActivity(intent); - return true; - default: - return super.onOptionsItemSelected(item); + if (item.getItemId() == R.id.action_create_forum) { + Intent intent = new Intent(getContext(), CreateForumActivity.class); + startActivity(intent); + return true; } - } - - private void loadForums() { - int revision = adapter.getRevision(); - listener.runOnDbThread(() -> { - try { - long start = now(); - Collection forums = new ArrayList<>(); - for (Forum f : forumManager.getForums()) { - try { - GroupCount count = - forumManager.getGroupCount(f.getId()); - forums.add(new ForumListItem(f, count)); - } catch (NoSuchGroupException e) { - // Continue - } - } - logDuration(LOG, "Full load", start); - displayForums(revision, forums); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); - } - - private void displayForums(int revision, Collection forums) { - runOnUiThreadUnlessDestroyed(() -> { - if (revision == adapter.getRevision()) { - adapter.incrementRevision(); - if (forums.isEmpty()) list.showData(); - else adapter.replaceAll(forums); - } else { - LOG.info("Concurrent update, reloading"); - loadForums(); - } - }); - } - - private void loadAvailableForums() { - listener.runOnDbThread(() -> { - try { - long start = now(); - int available = forumSharingManager.getInvitations().size(); - logDuration(LOG, "Loading available", start); - displayAvailableForums(available); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); - } - - private void displayAvailableForums(int availableCount) { - runOnUiThreadUnlessDestroyed(() -> { - if (availableCount == 0) { - snackbar.dismiss(); - } else { - snackbar.setText(getResources().getQuantityString( - R.plurals.forums_shared, availableCount, - availableCount)); - if (!snackbar.isShownOrQueued()) snackbar.show(); - } - }); - } - - @Override - public void eventOccurred(Event e) { - if (e instanceof ContactRemovedEvent) { - LOG.info("Contact removed, reloading available forums"); - loadAvailableForums(); - } else if (e instanceof GroupAddedEvent) { - GroupAddedEvent g = (GroupAddedEvent) e; - if (g.getGroup().getClientId().equals(CLIENT_ID)) { - LOG.info("Forum added, reloading forums"); - loadForums(); - } - } else if (e instanceof GroupRemovedEvent) { - GroupRemovedEvent g = (GroupRemovedEvent) e; - if (g.getGroup().getClientId().equals(CLIENT_ID)) { - LOG.info("Forum removed, removing from list"); - removeForum(g.getGroup().getId()); - } - } else if (e instanceof ForumPostReceivedEvent) { - ForumPostReceivedEvent f = (ForumPostReceivedEvent) e; - LOG.info("Forum post added, updating item"); - updateItem(f.getGroupId(), f.getHeader()); - } else if (e instanceof ForumInvitationRequestReceivedEvent) { - LOG.info("Forum invitation received, reloading available forums"); - loadAvailableForums(); - } - } - - @UiThread - private void updateItem(GroupId g, ForumPostHeader m) { - adapter.incrementRevision(); - int position = adapter.findItemPosition(g); - ForumListItem item = adapter.getItemAt(position); - if (item != null) { - item.addHeader(m); - adapter.updateItemAt(position, item); - } - } - - @UiThread - private void removeForum(GroupId g) { - adapter.incrementRevision(); - int position = adapter.findItemPosition(g); - ForumListItem item = adapter.getItemAt(position); - if (item != null) adapter.remove(item); + return super.onOptionsItemSelected(item); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListItem.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListItem.java index 5341d3f1f..19f16c018 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListItem.java @@ -4,12 +4,16 @@ import org.briarproject.briar.api.client.MessageTracker.GroupCount; import org.briarproject.briar.api.forum.Forum; import org.briarproject.briar.api.forum.ForumPostHeader; -// This class is NOT thread-safe -class ForumListItem { +import javax.annotation.concurrent.Immutable; + +import androidx.annotation.Nullable; + +@Immutable +class ForumListItem implements Comparable { private final Forum forum; - private int postCount, unread; - private long timestamp; + private final int postCount, unread; + private final long timestamp; ForumListItem(Forum forum, GroupCount count) { this.forum = forum; @@ -18,10 +22,11 @@ class ForumListItem { this.timestamp = count.getLatestMsgTime(); } - void addHeader(ForumPostHeader h) { - postCount++; - if (!h.isRead()) unread++; - if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp(); + ForumListItem(ForumListItem item, ForumPostHeader h) { + this.forum = item.forum; + this.postCount = item.postCount + 1; + this.unread = item.unread + (h.isRead() ? 0 : 1); + this.timestamp = Math.max(item.timestamp, h.getTimestamp()); } Forum getForum() { @@ -43,4 +48,29 @@ class ForumListItem { int getUnreadCount() { return unread; } + + @Override + public int hashCode() { + return forum.getId().hashCode(); + } + + @Override + public boolean equals(@Nullable Object o) { + return o instanceof ForumListItem && getForum().equals( + ((ForumListItem) o).getForum()); + } + + @Override + public int compareTo(ForumListItem o) { + if (this == o) return 0; + // The forum with the newest message comes first + long aTime = getTimestamp(), bTime = o.getTimestamp(); + if (aTime > bTime) return -1; + if (aTime < bTime) return 1; + // Break ties by forum name + String aName = getForum().getName(); + String bName = o.getForum().getName(); + return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java new file mode 100644 index 000000000..4f98c823e --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java @@ -0,0 +1,187 @@ +package org.briarproject.briar.android.forum; + +import android.app.Application; + +import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.event.GroupAddedEvent; +import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; +import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.viewmodel.DbViewModel; +import org.briarproject.briar.android.viewmodel.LiveResult; +import org.briarproject.briar.api.android.AndroidNotificationManager; +import org.briarproject.briar.api.client.MessageTracker.GroupCount; +import org.briarproject.briar.api.forum.Forum; +import org.briarproject.briar.api.forum.ForumManager; +import org.briarproject.briar.api.forum.ForumPostHeader; +import org.briarproject.briar.api.forum.ForumSharingManager; +import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent; +import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import androidx.annotation.UiThread; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logDuration; +import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.bramble.util.LogUtils.now; +import static org.briarproject.briar.api.forum.ForumManager.CLIENT_ID; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +class ForumListViewModel extends DbViewModel implements EventListener { + + private static final Logger LOG = + getLogger(ForumListViewModel.class.getName()); + + private final ForumManager forumManager; + private final ForumSharingManager forumSharingManager; + private final AndroidNotificationManager notificationManager; + private final EventBus eventBus; + + private final MutableLiveData>> forumItems = + new MutableLiveData<>(); + private final MutableLiveData numInvitations = + new MutableLiveData<>(); + + @Inject + ForumListViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor, + ForumManager forumManager, + ForumSharingManager forumSharingManager, + AndroidNotificationManager notificationManager, EventBus eventBus) { + super(application, dbExecutor, lifecycleManager, db, androidExecutor); + this.forumManager = forumManager; + this.forumSharingManager = forumSharingManager; + this.notificationManager = notificationManager; + this.eventBus = eventBus; + this.eventBus.addListener(this); + } + + @Override + protected void onCleared() { + super.onCleared(); + eventBus.removeListener(this); + } + + void clearAllForumPostNotifications() { + notificationManager.clearAllForumPostNotifications(); + } + + void blockAllForumPostNotifications() { + // TODO + } + + void unblockAllForumPostNotifications() { + // TODO + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof ContactRemovedEvent) { + LOG.info("Contact removed, reloading available forums"); + loadForumInvitations(); + } else if (e instanceof ForumInvitationRequestReceivedEvent) { + LOG.info("Forum invitation received, reloading available forums"); + loadForumInvitations(); + } else if (e instanceof GroupAddedEvent) { + GroupAddedEvent g = (GroupAddedEvent) e; + if (g.getGroup().getClientId().equals(CLIENT_ID)) { + LOG.info("Forum added, reloading forums"); + loadForums(); + } + } else if (e instanceof GroupRemovedEvent) { + GroupRemovedEvent g = (GroupRemovedEvent) e; + if (g.getGroup().getClientId().equals(CLIENT_ID)) { + LOG.info("Forum removed, removing from list"); + onGroupRemoved(g.getGroup().getId()); + } + } else if (e instanceof ForumPostReceivedEvent) { + ForumPostReceivedEvent f = (ForumPostReceivedEvent) e; + LOG.info("Forum post added, updating item"); + onForumPostReceived(f.getGroupId(), f.getHeader()); + } + } + + public void loadForums() { + loadList(this::loadForums, forumItems::setValue); + } + + @DatabaseExecutor + private List loadForums(Transaction txn) throws DbException { + long start = now(); + List forums = new ArrayList<>(); + for (Forum f : forumManager.getForums(txn)) { + GroupCount count = forumManager.getGroupCount(txn, f.getId()); + forums.add(new ForumListItem(f, count)); + } + Collections.sort(forums); + logDuration(LOG, "Loading forums", start); + return forums; + } + + @UiThread + private void onForumPostReceived(GroupId g, ForumPostHeader header) { + List list = updateListItems(forumItems, + itemToTest -> itemToTest.getForum().getId().equals(g), + itemToUpdate -> new ForumListItem(itemToUpdate, header)); + if (list == null) return; + // re-sort as the order of items may have changed + Collections.sort(list); + forumItems.setValue(new LiveResult<>(list)); + } + + @UiThread + private void onGroupRemoved(GroupId groupId) { + List list = removeListItems(forumItems, i -> + i.getForum().getId().equals(groupId) + ); + if (list == null) return; + forumItems.setValue(new LiveResult<>(list)); + } + + void loadForumInvitations() { + runOnDbThread(() -> { + try { + long start = now(); + int available = forumSharingManager.getInvitations().size(); + logDuration(LOG, "Loading available", start); + numInvitations.postValue(available); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + LiveData>> getForumListItems() { + return forumItems; + } + + LiveData getNumInvitations() { + return numInvitations; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java index 06ebbd1f9..62394c76d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java @@ -2,13 +2,25 @@ package org.briarproject.briar.android.forum; import org.briarproject.briar.android.activity.ActivityScope; import org.briarproject.briar.android.activity.BaseActivity; +import org.briarproject.briar.android.viewmodel.ViewModelKey; +import androidx.lifecycle.ViewModel; +import dagger.Binds; import dagger.Module; import dagger.Provides; +import dagger.multibindings.IntoMap; @Module public class ForumModule { + @Module + public interface BindsModule { + @Binds + @IntoMap + @ViewModelKey(ForumListViewModel.class) + ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel); + } + @ActivityScope @Provides ForumController provideForumController(BaseActivity activity, diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewHolder.java new file mode 100644 index 000000000..78129ffb6 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewHolder.java @@ -0,0 +1,77 @@ +package org.briarproject.briar.android.forum; + +import android.content.Context; +import android.content.Intent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.briarproject.briar.R; +import org.briarproject.briar.android.util.UiUtils; +import org.briarproject.briar.android.view.TextAvatarView; +import org.briarproject.briar.api.forum.Forum; + +import androidx.recyclerview.widget.RecyclerView; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; +import static org.briarproject.briar.android.activity.BriarActivity.GROUP_NAME; + +class ForumViewHolder extends RecyclerView.ViewHolder { + + private final Context ctx; + private final ViewGroup layout; + private final TextAvatarView avatar; + private final TextView name; + private final TextView postCount; + private final TextView date; + + ForumViewHolder(View v) { + super(v); + ctx = v.getContext(); + layout = (ViewGroup) v; + avatar = v.findViewById(R.id.avatarView); + name = v.findViewById(R.id.forumNameView); + postCount = v.findViewById(R.id.postCountView); + date = v.findViewById(R.id.dateView); + } + + void bind(ForumListItem item) { + // Avatar + avatar.setText(item.getForum().getName().substring(0, 1)); + avatar.setBackgroundBytes(item.getForum().getId().getBytes()); + avatar.setUnreadCount(item.getUnreadCount()); + + // Forum Name + name.setText(item.getForum().getName()); + + // Post Count + int count = item.getPostCount(); + if (count > 0) { + postCount.setText(ctx.getResources() + .getQuantityString(R.plurals.posts, count, count)); + } else { + postCount.setText(ctx.getString(R.string.no_posts)); + } + + // Date + if (item.isEmpty()) { + date.setVisibility(GONE); + } else { + long timestamp = item.getTimestamp(); + date.setText(UiUtils.formatDate(ctx, timestamp)); + date.setVisibility(VISIBLE); + } + + // Open Forum on Click + layout.setOnClickListener(v -> { + Intent i = new Intent(ctx, ForumActivity.class); + Forum f = item.getForum(); + i.putExtra(GROUP_ID, f.getId().getBytes()); + i.putExtra(GROUP_NAME, f.getName()); + ctx.startActivity(i); + }); + } + +}