Introduce ForumListViewModel

This commit is contained in:
Torsten Grote
2021-01-05 11:58:25 -03:00
parent a9cd40faeb
commit e2e67edbbe
7 changed files with 382 additions and 289 deletions

View File

@@ -28,6 +28,7 @@ import org.briarproject.bramble.plugin.tor.AndroidTorPluginFactory;
import org.briarproject.bramble.util.AndroidUtils; import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.android.account.LockManagerImpl; 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.keyagreement.ContactExchangeModule;
import org.briarproject.briar.android.login.LoginModule; import org.briarproject.briar.android.login.LoginModule;
import org.briarproject.briar.android.navdrawer.NavDrawerModule; import org.briarproject.briar.android.navdrawer.NavDrawerModule;
@@ -68,6 +69,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
ViewModelModule.class, ViewModelModule.class,
DevReportModule.class, DevReportModule.class,
// below need to be within same scope as ViewModelProvider.Factory // below need to be within same scope as ViewModelProvider.Factory
ForumModule.BindsModule.class,
GroupListModule.class, GroupListModule.class,
}) })
public class AppModule { public class AppModule {

View File

@@ -1,134 +1,47 @@
package org.briarproject.briar.android.forum; package org.briarproject.briar.android.forum;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; 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.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; @NotNullByDefault
import static android.view.View.VISIBLE; class ForumListAdapter extends ListAdapter<ForumListItem, ForumViewHolder> {
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;
class ForumListAdapter ForumListAdapter() {
extends BriarAdapter<ForumListItem, ForumListAdapter.ForumViewHolder> { super(new ForumListCallback());
ForumListAdapter(Context ctx) {
super(ctx, ForumListItem.class);
} }
@Override @Override
public ForumViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 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); R.layout.list_item_forum, parent, false);
return new ForumViewHolder(v); return new ForumViewHolder(v);
} }
@Override @Override
public void onBindViewHolder(ForumViewHolder ui, int position) { public void onBindViewHolder(ForumViewHolder viewHolder, int position) {
ForumListItem item = getItemAt(position); viewHolder.bind(getItem(position));
if (item == null) return; }
// Avatar @NotNullByDefault
ui.avatar.setText(item.getForum().getName().substring(0, 1)); private static class ForumListCallback extends ItemCallback<ForumListItem> {
ui.avatar.setBackgroundBytes(item.getForum().getId().getBytes()); @Override
ui.avatar.setUnreadCount(item.getUnreadCount()); public boolean areItemsTheSame(ForumListItem a, ForumListItem b) {
return a.equals(b);
// 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));
} }
// Date @Override
if (item.isEmpty()) { public boolean areContentsTheSame(ForumListItem a, ForumListItem b) {
ui.date.setVisibility(GONE); return a.isEmpty() == b.isEmpty() &&
} else { a.getTimestamp() == b.getTimestamp() &&
long timestamp = item.getTimestamp(); a.getUnreadCount() == b.getUnreadCount();
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);
} }
} }
} }

View File

@@ -12,80 +12,48 @@ import android.view.ViewGroup;
import com.google.android.material.snackbar.Snackbar; 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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; 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.R;
import org.briarproject.briar.android.activity.ActivityComponent; 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.sharing.ForumInvitationActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; 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.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.UiThread; import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE; import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
import static java.util.logging.Level.WARNING; import static java.util.Objects.requireNonNull;
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 @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ForumListFragment extends BaseEventFragment implements public class ForumListFragment extends BaseFragment implements
OnClickListener { OnClickListener {
public final static String TAG = ForumListFragment.class.getName(); public final static String TAG = ForumListFragment.class.getName();
private final static Logger LOG = Logger.getLogger(TAG);
private ForumListViewModel viewModel;
private BriarRecyclerView list; private BriarRecyclerView list;
private ForumListAdapter adapter;
private Snackbar snackbar; private Snackbar snackbar;
private final ForumListAdapter adapter = new ForumListAdapter();
@Inject @Inject
AndroidNotificationManager notificationManager; ViewModelProvider.Factory viewModelFactory;
// Fields that are accessed from background threads must be volatile
@Inject
volatile ForumManager forumManager;
@Inject
volatile ForumSharingManager forumSharingManager;
public static ForumListFragment newInstance() { public static ForumListFragment newInstance() {
return new ForumListFragment();
Bundle args = new Bundle();
ForumListFragment fragment = new ForumListFragment();
fragment.setArguments(args);
return fragment;
} }
@Override @Override
public void injectFragment(ActivityComponent component) { public void injectFragment(ActivityComponent component) {
component.inject(this); component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(ForumListViewModel.class);
} }
@Nullable @Nullable
@@ -93,24 +61,35 @@ public class ForumListFragment extends BaseEventFragment implements
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
requireActivity().setTitle(R.string.forums_button); requireActivity().setTitle(R.string.forums_button);
View contentView = View v = inflater.inflate(R.layout.fragment_forum_list, container,
inflater.inflate(R.layout.fragment_forum_list, container, false);
false);
adapter = new ForumListAdapter(getActivity()); list = v.findViewById(R.id.forumList);
list = contentView.findViewById(R.id.forumList);
list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter); 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() snackbar = new BriarSnackbarBuilder()
.setAction(R.string.show, this) .setAction(R.string.show, this)
.make(list, "", LENGTH_INDEFINITE); .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 @Override
@@ -121,19 +100,23 @@ public class ForumListFragment extends BaseEventFragment implements
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
// TODO block all forum post notifications as well viewModel.blockAllForumPostNotifications();
notificationManager.clearAllForumPostNotifications(); viewModel.clearAllForumPostNotifications();
loadForums(); // The attributes and sorting of the forums may have changed while we
loadAvailableForums(); // 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(); list.startPeriodicUpdate();
} }
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
adapter.clear();
list.showProgressBar();
list.stopPeriodicUpdate(); list.stopPeriodicUpdate();
viewModel.unblockAllForumPostNotifications();
} }
@Override @Override
@@ -145,123 +128,12 @@ public class ForumListFragment extends BaseEventFragment implements
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
// Handle presses on the action bar items // Handle presses on the action bar items
switch (item.getItemId()) { if (item.getItemId() == R.id.action_create_forum) {
case R.id.action_create_forum: Intent intent = new Intent(getContext(), CreateForumActivity.class);
Intent intent = startActivity(intent);
new Intent(getContext(), CreateForumActivity.class); return true;
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
} }
} return super.onOptionsItemSelected(item);
private void loadForums() {
int revision = adapter.getRevision();
listener.runOnDbThread(() -> {
try {
long start = now();
Collection<ForumListItem> 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<ForumListItem> 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);
} }
@Override @Override

View File

@@ -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.Forum;
import org.briarproject.briar.api.forum.ForumPostHeader; import org.briarproject.briar.api.forum.ForumPostHeader;
// This class is NOT thread-safe import javax.annotation.concurrent.Immutable;
class ForumListItem {
import androidx.annotation.Nullable;
@Immutable
class ForumListItem implements Comparable<ForumListItem> {
private final Forum forum; private final Forum forum;
private int postCount, unread; private final int postCount, unread;
private long timestamp; private final long timestamp;
ForumListItem(Forum forum, GroupCount count) { ForumListItem(Forum forum, GroupCount count) {
this.forum = forum; this.forum = forum;
@@ -18,10 +22,11 @@ class ForumListItem {
this.timestamp = count.getLatestMsgTime(); this.timestamp = count.getLatestMsgTime();
} }
void addHeader(ForumPostHeader h) { ForumListItem(ForumListItem item, ForumPostHeader h) {
postCount++; this.forum = item.forum;
if (!h.isRead()) unread++; this.postCount = item.postCount + 1;
if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp(); this.unread = item.unread + (h.isRead() ? 0 : 1);
this.timestamp = Math.max(item.timestamp, h.getTimestamp());
} }
Forum getForum() { Forum getForum() {
@@ -43,4 +48,29 @@ class ForumListItem {
int getUnreadCount() { int getUnreadCount() {
return unread; 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);
}
} }

View File

@@ -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<LiveResult<List<ForumListItem>>> forumItems =
new MutableLiveData<>();
private final MutableLiveData<Integer> 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<ForumListItem> loadForums(Transaction txn) throws DbException {
long start = now();
List<ForumListItem> 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<ForumListItem> 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<ForumListItem> 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<LiveResult<List<ForumListItem>>> getForumListItems() {
return forumItems;
}
LiveData<Integer> getNumInvitations() {
return numInvitations;
}
}

View File

@@ -2,13 +2,25 @@ package org.briarproject.briar.android.forum;
import org.briarproject.briar.android.activity.ActivityScope; import org.briarproject.briar.android.activity.ActivityScope;
import org.briarproject.briar.android.activity.BaseActivity; 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.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoMap;
@Module @Module
public class ForumModule { public class ForumModule {
@Module
public interface BindsModule {
@Binds
@IntoMap
@ViewModelKey(ForumListViewModel.class)
ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel);
}
@ActivityScope @ActivityScope
@Provides @Provides
ForumController provideForumController(BaseActivity activity, ForumController provideForumController(BaseActivity activity,

View File

@@ -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);
});
}
}