Merge branch '408-forum-list-unread-posts-badges' into 'master'

Adds badges to the forum list that indicate unread posts

This MR adds  badges to the forum list that indicate unread posts. It does so by extending the compound view `TextAvatarView` and provides convenient setters that take care of the required UI changes. The new badge can also be used to indicate a problem with the forum.

![device-2016-06-02-134048](/uploads/133f365f5654be304f590a4f5b667d06/device-2016-06-02-134048.png)![device-2016-06-02-134453](/uploads/423a88f6306a558828181f0c539aa1cb/device-2016-06-02-134453.png)

I decided not to check if the forum is shared from someone or with someone and indicate a problem in this case, because this requires iterating two times over all contacts and their group metadata for each forum. If someone considers it important to check for this information, we can create a ticket for doing this within the forum and not in the forum list.

See merge request !207
This commit is contained in:
Torsten Grote
2016-06-07 14:36:42 +00:00
12 changed files with 142 additions and 58 deletions

View File

@@ -8,7 +8,8 @@
<padding
android:left="@dimen/unread_bubble_padding_horizontal"
android:right="@dimen/unread_bubble_padding_horizontal"/>
android:right="@dimen/unread_bubble_padding_horizontal"
android:bottom="1px"/>
<solid
android:color="@color/briar_primary"/>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:radius="@dimen/unread_bubble_size"/>
<padding
android:left="@dimen/unread_bubble_padding_horizontal"
android:right="@dimen/unread_bubble_padding_horizontal"/>
<solid
android:color="@color/briar_gold"/>
<stroke
android:color="@color/briar_primary"
android:width="@dimen/avatar_border_width"/>
</shape>

View File

@@ -11,8 +11,8 @@
<org.briarproject.android.util.TextAvatarView
android:id="@+id/avatarView"
android:layout_width="@dimen/avatar_forum_size"
android:layout_height="@dimen/avatar_forum_size"
android:layout_width="@dimen/listitem_picture_frame_size"
android:layout_height="@dimen/listitem_picture_frame_size"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginRight="@dimen/listitem_horizontal_margin"

View File

@@ -1,68 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android: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">
<org.briarproject.android.util.TextAvatarView
android:id="@+id/avatarView"
android:layout_height="@dimen/avatar_forum_size"
android:layout_width="@dimen/avatar_forum_size"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:layout_width="@dimen/listitem_picture_frame_size"
android:layout_height="@dimen/listitem_picture_frame_size"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
/>
android:layout_centerVertical="true"
android:layout_marginRight="@dimen/listitem_horizontal_margin"/>
<TextView
android:id="@+id/forumNameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginTop="@dimen/listitem_horizontal_margin"
android:layout_toEndOf="@+id/avatarView"
android:layout_toRightOf="@+id/avatarView"
android:maxLines="2"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_medium"
tools:text="This is a name of a forum"
android:layout_alignParentTop="true"
android:layout_toRightOf="@+id/avatarView"
android:layout_toEndOf="@+id/avatarView"/>
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:paddingTop="@dimen/margin_medium"
android:layout_below="@+id/forumNameView"
android:layout_toEndOf="@+id/avatarView"
android:layout_toRightOf="@+id/avatarView"
android:paddingBottom="@dimen/listitem_horizontal_margin"
android:paddingTop="@dimen/margin_medium"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_small"
android:text="@string/no_unread_posts"
android:layout_below="@+id/forumNameView"
android:layout_toRightOf="@+id/avatarView"
android:layout_toEndOf="@+id/avatarView"/>
tools:text="1337 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_alignParentRight="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:layout_marginRight="@dimen/listitem_horizontal_margin"
android:paddingBottom="@dimen/listitem_horizontal_margin"
android:paddingTop="@dimen/margin_medium"
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"/>
<View
style="@style/Divider.ForumList"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/unreadView"/>
</RelativeLayout>

View File

@@ -2,14 +2,15 @@
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="@layout/list_item_forum">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatarBackground"
style="@style/BriarAvatar"
android:layout_width="@dimen/avatar_forum_size"
android:layout_height="@dimen/avatar_forum_size"
android:layout_gravity="center"
android:layout_gravity="bottom|left"
android:src="@android:color/transparent"
app:civ_fill_color="@color/briar_button_positive"/>
@@ -18,13 +19,28 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginRight="@dimen/listitem_picture_frame_offset"
android:layout_marginTop="@dimen/listitem_picture_frame_offset"
android:maxLength="1"
android:shadowColor="@color/forum_avatar_shadow"
android:shadowDx="0"
android:shadowDy="1.5"
android:shadowRadius="1.5"
android:textColor="@color/briar_text_primary_inverse"
android:textSize="30sp"
android:textSize="@dimen/avatar_text_size"
tools:text="T"/>
<TextView
android:id="@+id/unreadCountView"
android:layout_width="wrap_content"
android:layout_height="@dimen/unread_bubble_size"
android:layout_gravity="right|top"
android:background="@drawable/bubble"
android:gravity="center"
android:minWidth="@dimen/unread_bubble_size"
android:textColor="@color/briar_text_primary_inverse"
android:textSize="@dimen/unread_bubble_text_size"
android:textStyle="bold"
tools:text="12"/>
</merge>

View File

@@ -30,7 +30,7 @@
<color name="briar_text_primary">@color/briar_primary</color>
<color name="briar_text_primary_inverse">#ffffff</color>
<color name="briar_text_secondary">#333333</color>
<color name="briar_text_tertiary">#333333</color>
<color name="briar_text_tertiary">#FF78909C</color>
<color name="briar_button_positive">#06b9ff</color>
<color name="briar_button_negative">#ff0000</color>

View File

@@ -25,11 +25,13 @@
<dimen name="listitem_height_contact_selector">68dp</dimen>
<dimen name="listitem_picture_size">48dp</dimen>
<dimen name="listitem_picture_size_small">23dp</dimen>
<dimen name="listitem_picture_frame_size">50dp</dimen>
<dimen name="listitem_picture_frame_size">53dp</dimen>
<dimen name="listitem_picture_frame_offset">2dp</dimen>
<dimen name="listitem_selectable_picture_size">40dp</dimen>
<dimen name="dropdown_picture_size">32dp</dimen>
<dimen name="avatar_forum_size">48dp</dimen>
<dimen name="avatar_border_width">2dp</dimen>
<dimen name="avatar_text_size">30sp</dimen>
<dimen name="unread_bubble_text_size">12sp</dimen>
<dimen name="unread_bubble_border_width">2dp</dimen>

View File

@@ -80,12 +80,15 @@
<string name="forum_leave">Leave Forum</string>
<string name="forum_left_toast">Left Forum</string>
<string name="forum_sharing_status">Sharing Status</string>
<string name="no_forum_posts">No posts</string>
<string name="no_unread_posts">no unread posts</string>
<string name="forum_no_posts">No posts</string>
<plurals name="unread_posts">
<item quantity="one">%d unread post</item>
<item quantity="other">%d unread posts</item>
</plurals>
<plurals name="forum_posts">
<item quantity="one">%d post</item>
<item quantity="other">%d 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>

View File

@@ -19,10 +19,12 @@ import org.briarproject.api.sync.GroupId;
import java.util.Collection;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.android.BriarActivity.GROUP_ID;
import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
public class ForumListAdapter extends
class ForumListAdapter extends
RecyclerView.Adapter<ForumListAdapter.ForumViewHolder> {
private SortedList<ForumListItem> forums = new SortedList<>(
@@ -76,7 +78,7 @@ public class ForumListAdapter extends
private final Context ctx;
public ForumListAdapter(Context ctx) {
ForumListAdapter(Context ctx) {
this.ctx = ctx;
}
@@ -94,31 +96,36 @@ public class ForumListAdapter extends
// 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());
// Unread Count
int unread = item.getUnreadCount();
if (unread > 0) {
// Post Count
int postCount = item.getPostCount();
if (postCount > 0) {
ui.unread.setText(ctx.getResources()
.getQuantityString(R.plurals.unread_posts, unread, unread));
.getQuantityString(R.plurals.forum_posts, postCount,
postCount));
ui.unread.setTextColor(
ContextCompat.getColor(ctx, R.color.briar_button_positive));
ContextCompat
.getColor(ctx, R.color.briar_text_secondary));
} else {
ui.unread.setText(ctx.getString(R.string.no_unread_posts));
ui.avatar.setProblem(true);
ui.unread.setText(ctx.getString(R.string.forum_no_posts));
ui.unread.setTextColor(
ContextCompat.getColor(ctx, R.color.briar_text_secondary));
ContextCompat
.getColor(ctx, R.color.briar_text_tertiary));
}
// Date or "No Posts"
// Date
if (item.isEmpty()) {
ui.date.setVisibility(View.GONE);
ui.date.setVisibility(GONE);
} else {
long timestamp = item.getTimestamp();
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, timestamp));
ui.date.setVisibility(View.VISIBLE);
ui.date.setVisibility(VISIBLE);
}
// Open Forum on Click
@@ -158,7 +165,7 @@ public class ForumListAdapter extends
forums.addAll(items);
}
public void updateItem(ForumListItem item) {
void updateItem(ForumListItem item) {
ForumListItem oldItem = getItem(item.getForum().getGroup().getId());
int position = forums.indexOf(oldItem);
forums.updateItemAt(position, item);
@@ -176,7 +183,7 @@ public class ForumListAdapter extends
return forums.size() == 0;
}
protected static class ForumViewHolder extends RecyclerView.ViewHolder {
static class ForumViewHolder extends RecyclerView.ViewHolder {
private final ViewGroup layout;
private final TextAvatarView avatar;
@@ -184,7 +191,7 @@ public class ForumListAdapter extends
private final TextView unread;
private final TextView date;
public ForumViewHolder(View v) {
ForumViewHolder(View v) {
super(v);
layout = (ViewGroup) v;

View File

@@ -14,8 +14,6 @@ import android.view.View;
import android.view.ViewGroup;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.fragment.BaseEventFragment;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.db.DbException;

View File

@@ -9,6 +9,7 @@ class ForumListItem {
private final Forum forum;
private final boolean empty;
private final int postCount;
private final long timestamp;
private final int unread;
@@ -16,6 +17,7 @@ class ForumListItem {
this.forum = forum;
empty = headers.isEmpty();
if (empty) {
postCount = 0;
timestamp = 0;
unread = 0;
} else {
@@ -29,6 +31,7 @@ class ForumListItem {
}
if (!h.isRead()) unread++;
}
this.postCount = headers.size();
this.timestamp = newest.getTimestamp();
this.unread = unread;
}
@@ -42,6 +45,10 @@ class ForumListItem {
return empty;
}
int getPostCount() {
return postCount;
}
long getTimestamp() {
return timestamp;
}

View File

@@ -1,13 +1,13 @@
package org.briarproject.android.util;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.briarproject.R;
@@ -15,8 +15,10 @@ import de.hdodenhof.circleimageview.CircleImageView;
public class TextAvatarView extends FrameLayout {
final private AppCompatTextView textView;
final private CircleImageView backgroundView;
final private AppCompatTextView character;
final private CircleImageView background;
final private TextView badge;
private int unreadCount;
public TextAvatarView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -25,8 +27,10 @@ public class TextAvatarView extends FrameLayout {
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater
.inflate(R.layout.text_avatar_view, this, true);
textView = (AppCompatTextView) findViewById(R.id.textAvatarView);
backgroundView = (CircleImageView) findViewById(R.id.avatarBackground);
character = (AppCompatTextView) findViewById(R.id.textAvatarView);
background = (CircleImageView) findViewById(R.id.avatarBackground);
badge = (TextView) findViewById(R.id.unreadCountView);
badge.setVisibility(INVISIBLE);
}
public TextAvatarView(Context context) {
@@ -34,7 +38,32 @@ public class TextAvatarView extends FrameLayout {
}
public void setText(String text) {
textView.setText(text);
character.setText(text);
}
public void setUnreadCount(int count) {
if (count > 0) {
this.unreadCount = count;
badge.setBackgroundResource(R.drawable.bubble);
badge.setText(String.valueOf(count));
badge.setTextColor(ContextCompat.getColor(getContext(), R.color.briar_text_primary_inverse));
badge.setVisibility(VISIBLE);
} else {
badge.setVisibility(INVISIBLE);
}
}
public void setProblem(boolean problem) {
if (problem) {
badge.setBackgroundResource(R.drawable.bubble_problem);
badge.setText("!");
badge.setTextColor(ContextCompat.getColor(getContext(), R.color.briar_primary));
badge.setVisibility(VISIBLE);
} else if (unreadCount > 0) {
setUnreadCount(unreadCount);
} else {
badge.setVisibility(INVISIBLE);
}
}
public void setBackgroundBytes(byte[] bytes) {
@@ -43,7 +72,7 @@ public class TextAvatarView extends FrameLayout {
int b = getByte(bytes, 2) * 3 / 4 + 96;
int color = Color.rgb(r, g, b);
backgroundView.setFillColor(color);
background.setFillColor(color);
}
private byte getByte(byte[] bytes, int index) {