mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-15 04:18:53 +01:00
Introduce ForumListViewModel
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user