New List of Forums

The adapter of the `ForumListFragment` has been changed into a
`BriarRecyclerView` and all its code has been adapted and simplified
accordingly.

All UI of the forum list is now defined in XML layouts.
parent 9bef114c
......@@ -24,6 +24,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="@dimen/margin_activity_horizontal"
android:textSize="@dimen/text_size_large"
android:text="@string/no_data"/>
......
<?xml version="1.0" encoding="utf-8"?>
<org.briarproject.android.util.BriarRecyclerView
android:id="@+id/forumList"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
android:layout_marginStart="@dimen/listitem_horizontal_margin"
android:paddingTop="@dimen/listitem_horizontal_margin"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/forumNameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_medium"
tools:text="This is a name of a forum"/>
<TextView
android:id="@+id/unreadView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/forumNameView"
android:layout_toLeftOf="@+id/dateView"
android:paddingTop="@dimen/margin_medium"
android:paddingBottom="@dimen/listitem_horizontal_margin"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_small"
android:text="@string/no_unread_posts"/>
<TextView
android:id="@+id/dateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_below="@+id/forumNameView"
android:paddingTop="@dimen/margin_medium"
android:paddingBottom="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:layout_marginEnd="@dimen/listitem_horizontal_margin"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_small"
tools:text="Dec 24"/>
<View style="@style/Divider.ForumList"
android:layout_below="@+id/unreadView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"/>
</RelativeLayout>
......@@ -72,7 +72,7 @@
<string name="private_message_hint">Type message</string>
<string name="message_sent_toast">Message sent</string>
<string name="forums_title">Forums</string>
<string name="no_forums">No forums</string>
<string name="no_forums">You don\'t have any forums.\n\nWhy don\'t you create a new one yourself or ask your contacts to share one with you?</string>
<plurals name="forums_shared">
<item quantity="one">%d forum shared by contacts</item>
<item quantity="other">%d forums shared by contacts</item>
......@@ -81,6 +81,11 @@
<string name="forum_leave">Leave Forum</string>
<string name="forum_left_toast">Left Forum</string>
<string name="no_forum_posts">No posts</string>
<string name="no_unread_posts">no unread posts</string>
<plurals name="unread_posts">
<item quantity="one">%d unread post</item>
<item quantity="other">%d unread posts</item>
</plurals>
<string name="create_forum_title">New Forum</string>
<string name="choose_forum_name">Choose a name for your forum:</string>
<string name="create_forum_button">Create Forum</string>
......
......@@ -107,6 +107,11 @@
<item name="android:layout_marginLeft">@dimen/margin_large</item>
</style>
<style name="Divider.ForumList" parent="Divider">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">1dp</item>
</style>
<style name="NavMenuButton" parent="Widget.AppCompat.Button.Borderless.Colored">
<item name="android:textSize">@dimen/text_size_medium</item>
<item name="android:textColor">@android:color/tertiary_text_light</item>
......
package org.briarproject.android.forum;
import android.content.Context;
import android.content.res.Resources;
import android.content.Intent;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.sync.GroupId;
import java.util.ArrayList;
import java.util.Collection;
import static android.text.TextUtils.TruncateAt.END;
import static android.widget.LinearLayout.HORIZONTAL;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import static org.briarproject.android.BriarActivity.GROUP_ID;
import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
public class ForumListAdapter extends ArrayAdapter<ForumListItem> {
public class ForumListAdapter extends
RecyclerView.Adapter<ForumListAdapter.ForumViewHolder> {
private final int pad;
private SortedList<ForumListItem> forums = new SortedList<>(
ForumListItem.class, new SortedList.Callback<ForumListItem>() {
@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 void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
@Override
public boolean areContentsTheSame(ForumListItem a, ForumListItem b) {
return a.getForum().equals(b.getForum()) &&
a.getTimestamp() == b.getTimestamp() &&
a.getUnreadCount() == b.getUnreadCount();
}
@Override
public boolean areItemsTheSame(ForumListItem a, ForumListItem b) {
return a.getForum().equals(b.getForum());
}
});
private final Context ctx;
public ForumListAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<ForumListItem>());
pad = LayoutUtils.getPadding(ctx);
this.ctx = ctx;
}
@Override
public ForumViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(ctx)
.inflate(R.layout.list_item_forum, parent, false);
return new ForumViewHolder(v);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ForumListItem item = getItem(position);
Context ctx = getContext();
Resources res = ctx.getResources();
public void onBindViewHolder(ForumViewHolder ui, int position) {
final ForumListItem item = getItem(position);
// TODO add avatar. See #337
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(HORIZONTAL);
// Forum Name
ui.name.setText(item.getForum().getName());
// Unread Count
int unread = item.getUnreadCount();
if (unread > 0)
layout.setBackgroundColor(res.getColor(R.color.unread_background));
TextView name = new TextView(ctx);
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setSingleLine();
name.setEllipsize(END);
name.setPadding(pad, pad, pad, pad);
String forumName = item.getForum().getName();
if (unread > 0) name.setText(forumName + " (" + unread + ")");
else name.setText(forumName);
layout.addView(name);
if (unread > 0) {
ui.unread.setText(ctx.getResources()
.getQuantityString(R.plurals.unread_posts, unread, unread));
ui.unread.setTextColor(
ContextCompat.getColor(ctx, R.color.briar_button_positive));
} else {
ui.unread.setText(ctx.getString(R.string.no_unread_posts));
ui.unread.setTextColor(
ContextCompat.getColor(ctx, R.color.briar_text_secondary));
}
// Date or "No Posts"
if (item.isEmpty()) {
TextView noPosts = new TextView(ctx);
noPosts.setPadding(pad, 0, pad, pad);
noPosts.setTextColor(res.getColor(R.color.no_posts));
noPosts.setText(R.string.no_forum_posts);
layout.addView(noPosts);
ui.date.setVisibility(View.GONE);
} else {
TextView date = new TextView(ctx);
date.setPadding(pad, 0, pad, pad);
long timestamp = item.getTimestamp();
date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
layout.addView(date);
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, timestamp));
ui.date.setVisibility(View.VISIBLE);
}
// Open Forum on Click
ui.layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(ctx, ForumActivity.class);
Forum f = item.getForum();
i.putExtra(GROUP_ID, f.getId().getBytes());
i.putExtra(FORUM_NAME, f.getName());
ctx.startActivity(i);
}
});
}
@Override
public int getItemCount() {
return forums.size();
}
public ForumListItem getItem(int position) {
return forums.get(position);
}
@Nullable
public ForumListItem getItem(GroupId g) {
for (int i = 0; i < forums.size(); i++) {
ForumListItem item = forums.get(i);
if (item.getForum().getGroup().getId().equals(g)) {
return item;
}
}
return null;
}
public void addAll(Collection<ForumListItem> items) {
forums.addAll(items);
}
public void updateItem(ForumListItem item) {
ForumListItem oldItem = getItem(item.getForum().getGroup().getId());
int position = forums.indexOf(oldItem);
forums.updateItemAt(position, item);
}
return layout;
public void remove(ForumListItem item) {
forums.remove(item);
}
public void clear() {
forums.clear();
}
public boolean isEmpty() {
return forums.size() == 0;
}
protected static class ForumViewHolder extends RecyclerView.ViewHolder {
public final ViewGroup layout;
public final TextView name;
public final TextView unread;
public final TextView date;
public ForumViewHolder(View v) {
super(v);
layout = (ViewGroup) v;
name = (TextView) v.findViewById(R.id.forumNameView);
unread = (TextView) v.findViewById(R.id.unreadView);
date = (TextView) v.findViewById(R.id.dateView);
}
}
}
......@@ -5,25 +5,23 @@ import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.fragment.BaseEventFragment;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchGroupException;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.ForumInvitationReceivedEvent;
import org.briarproject.api.event.GroupAddedEvent;
import org.briarproject.api.event.GroupRemovedEvent;
import org.briarproject.api.event.MessageValidatedEvent;
......@@ -34,26 +32,18 @@ import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.BriarActivity.GROUP_ID;
import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
public class ForumListFragment extends BaseEventFragment implements
AdapterView.OnItemClickListener, View.OnClickListener {
View.OnClickListener {
public final static String TAG = "ForumListFragment";
......@@ -69,10 +59,8 @@ public class ForumListFragment extends BaseEventFragment implements
return fragment;
}
private TextView empty = null;
private ForumListAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
private BriarRecyclerView list;
private ForumListAdapter adapter;
private Snackbar snackbar;
// Fields that are accessed from background threads must be volatile
......@@ -86,32 +74,24 @@ public class ForumListFragment extends BaseEventFragment implements
setHasOptionsMenu(true);
LinearLayout layout = new LinearLayout(getContext());
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
empty = new TextView(getContext());
empty.setLayoutParams(MATCH_WRAP_1);
empty.setGravity(CENTER);
empty.setTextSize(18);
empty.setText(R.string.no_forums);
empty.setVisibility(GONE);
layout.addView(empty);
adapter = new ForumListAdapter(getContext());
list = new ListView(getContext());
list.setLayoutParams(MATCH_WRAP_1);
View contentView =
inflater.inflate(R.layout.fragment_forum_list, container,
false);
adapter = new ForumListAdapter(getActivity());
list = (BriarRecyclerView) contentView.findViewById(R.id.forumList);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
list.setOnItemClickListener(this);
list.setVisibility(GONE);
layout.addView(list);
list.setEmptyText(getString(R.string.no_forums));
// Show a progress bar while the list is loading
loading = new ListLoadingProgressBar(getContext());
layout.addView(loading);
snackbar = Snackbar.make(contentView, "", LENGTH_INDEFINITE);
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.setAction(R.string.show_forums, this);
snackbar.setActionTextColor(ContextCompat
.getColor(getContext(), R.color.briar_button_positive));
return layout;
return contentView;
}
@Override
......@@ -128,13 +108,15 @@ public class ForumListFragment extends BaseEventFragment implements
public void onResume() {
super.onResume();
snackbar = Snackbar.make(getView(), "", LENGTH_INDEFINITE);
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.setAction(R.string.show_forums, this);
snackbar.setActionTextColor(ContextCompat
.getColor(getContext(), R.color.briar_button_positive));
loadForumHeaders();
loadAvailableForums();
}
@Override
public void onPause() {
super.onPause();
loadHeaders();
adapter.clear();
}
@Override
......@@ -157,30 +139,26 @@ public class ForumListFragment extends BaseEventFragment implements
}
}
private void loadHeaders() {
clearHeaders();
private void loadForumHeaders() {
listener.runOnDbThread(new Runnable() {
public void run() {
try {
// load forums
long now = System.currentTimeMillis();
boolean displayedHeaders = false;
Collection<ForumListItem> forums = new ArrayList<>();
for (Forum f : forumManager.getForums()) {
try {
Collection<ForumPostHeader> headers =
forumManager.getPostHeaders(f.getId());
displayHeaders(f, headers);
displayedHeaders = true;
forums.add(new ForumListItem(f, headers));
} catch (NoSuchGroupException e) {
// Continue
}
}
int available =
forumSharingManager.getAvailableForums().size();
displayForumHeaders(forums);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Full load took " + duration + " ms");
if (!displayedHeaders) displayEmpty();
displayAvailable(available);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
......@@ -189,45 +167,35 @@ public class ForumListFragment extends BaseEventFragment implements
});
}
private void clearHeaders() {
private void displayForumHeaders(final Collection<ForumListItem> forums) {
listener.runOnUiThread(new Runnable() {
public void run() {
empty.setVisibility(GONE);
list.setVisibility(GONE);
snackbar.dismiss();
loading.setVisibility(VISIBLE);
adapter.clear();
if (forums.size() > 0) adapter.addAll(forums);
else list.showData();
}
});
}
private void displayHeaders(final Forum f,
final Collection<ForumPostHeader> headers) {
listener.runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
// Remove the old item, if any
ForumListItem item = findForum(f.getId());
if (item != null) adapter.remove(item);
// Add a new item
adapter.add(new ForumListItem(f, headers));
adapter.sort(ForumListItemComparator.INSTANCE);
selectFirstUnread();
}
});
}
private void displayEmpty() {
listener.runOnUiThread(new Runnable() {
private void loadAvailableForums() {
listener.runOnDbThread(new Runnable() {
public void run() {
empty.setVisibility(VISIBLE);
loading.setVisibility(GONE);
try {
long now = System.currentTimeMillis();
int available =
forumSharingManager.getAvailableForums().size();
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading available took " + duration + " ms");
displayAvailableForums(available);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayAvailable(final int availableCount) {
private void displayAvailableForums(final int availableCount) {
listener.runOnUiThread(new Runnable() {
public void run() {
if (availableCount == 0) {
......@@ -242,42 +210,21 @@ public class ForumListFragment extends BaseEventFragment implements
});