Merge branch '410-my-blogs-tab-with-option-to-add-new-blogs' into 'master'

Micro Blogs UI

**Attention:** This MR includes several other commits which are supposed to end up in separate MRs. I suggest that you review **per commit**. Once the first two commits have green light, I can split out the other commits into other MRs. This way I don't have to work myself through a long rebase chain every time I make a change to the bottom MR.

This MR is full of commits that introduce features that we will not be using initially. The last commit implements the Micro Blogs UI on top of the framework the first commits establish and hides/disables all future features for now.

I suggest we merge this as is and clean things up later when we have a clearer idea what features we will be doing eventually.

![device-2016-06-23-135016](/uploads/600fc7c28c0c6c4a60d8273ffb55f30a/device-2016-06-23-135016.png)
![device-2016-06-22-181934](/uploads/e57cfbc162150bcd01d4683d4164406e/device-2016-06-22-181934.png)
![device-2016-06-23-142422](/uploads/5814f1e13b6d2230f2e4c12a8c5cd599/device-2016-06-23-142422.png)
![device-2016-06-23-142506](/uploads/d3ac268082b98bbcd068f3d6fe0c6712/device-2016-06-23-142506.png)
![device-2016-06-22-181913](/uploads/f5dcc1ed9a40ec5fa8f634864903f4cc/device-2016-06-22-181913.png)

Closes #436 

See merge request !214
This commit is contained in:
str4d
2016-07-17 03:35:36 +00:00
58 changed files with 3106 additions and 84 deletions

View File

@@ -150,6 +150,36 @@
/>
</activity>
<activity
android:name=".android.blogs.CreateBlogActivity"
android:label="@string/blogs_my_blogs_label"
android:parentActivityName=".android.NavDrawerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".android.NavDrawerActivity"
/>
</activity>
<activity
android:name=".android.blogs.BlogActivity"
android:parentActivityName=".android.NavDrawerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".android.NavDrawerActivity"
/>
</activity>
<activity
android:name=".android.blogs.WriteBlogPostActivity"
android:label="@string/blogs_write_blog_post"
android:parentActivityName=".android.blogs.BlogActivity"
android:windowSoftInputMode="stateVisible|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".android.blogs.BlogActivity"
/>
</activity>
<activity
android:name=".android.identity.CreateIdentityActivity"
android:label="@string/new_identity_title"

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:alpha="0.54"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:alpha="0.54"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
</vector>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="15dp"
android:width="31dp"
android:height="12dp"
android:viewportHeight="20"
android:viewportWidth="49">
<path

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="15dp"
android:width="31dp"
android:height="12dp"
android:viewportHeight="20"
android:viewportWidth="49">
<path

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="15dp"
android:width="31dp"
android:height="12dp"
android:viewportHeight="20"
android:viewportWidth="49">
<path

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="15dp"
android:width="31dp"
android:height="12dp"
android:viewportHeight="20"
android:viewportWidth="49">
<path

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".android.blogs.BlogActivity"/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</FrameLayout>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
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="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="@dimen/margin_activity_horizontal"
tools:context=".android.blogs.CreateBlogActivity">
<android.support.design.widget.TextInputLayout
android:id="@+id/titleLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:counterEnabled="true"
app:counterOverflowTextAppearance="@style/BriarTextCounter.Overflow">
<android.support.design.widget.TextInputEditText
android:id="@+id/titleInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/blogs_my_blogs_create_hint_title"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/descLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:counterEnabled="true"
app:counterOverflowTextAppearance="@style/BriarTextCounter.Overflow">
<android.support.design.widget.TextInputEditText
android:id="@+id/descInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/blogs_my_blogs_create_hint_desc"/>
</android.support.design.widget.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/blogs_my_blogs_create_hint_desc_explanation"/>
<Button
android:id="@+id/createBlogButton"
style="@style/BriarButton"
android:layout_marginTop="@dimen/margin_activity_vertical"
android:enabled="false"
android:text="@string/blogs_my_blogs_create"/>
<ProgressBar
android:id="@+id/createBlogProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_activity_vertical"
android:indeterminate="true"
android:visibility="gone"/>
</LinearLayout>
</ScrollView>

View File

@@ -32,9 +32,6 @@
<Button
style="@style/BriarButton"
android:id="@+id/createForumButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="@string/create_forum_button" />
<ProgressBar
@@ -42,8 +39,6 @@
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:indeterminate="true"
android:layout_centerHorizontal="true"
android:visibility="gone" />
</LinearLayout>

View File

@@ -34,8 +34,6 @@
<Button
android:id="@+id/createIdentityButton"
style="@style/BriarButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:enabled="false"
android:text="@string/create_identity_button"/>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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="match_parent"
android:orientation="vertical"
android:padding="@dimen/margin_small"
tools:context=".android.blogs.WriteBlogPostActivity">
<android.support.design.widget.TextInputLayout
android:id="@+id/titleLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:counterEnabled="true"
app:counterOverflowTextAppearance="@style/BriarTextCounter.Overflow">
<android.support.design.widget.TextInputEditText
android:id="@+id/titleInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/blogs_write_blog_post_title_hint"
android:inputType="textCapWords|textCapSentences|textAutoCorrect"/>
</android.support.design.widget.TextInputLayout>
<EditText
android:id="@+id/bodyInput"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="bottom"
android:hint="@string/blogs_write_blog_post_body_hint"
android:inputType="textMultiLine|textLongMessage|textCapSentences|textAutoCorrect">
<requestFocus/>
</EditText>
<Button
android:id="@+id/publishButton"
style="@style/BriarButton"
android:enabled="false"
android:text="@string/blogs_publish_blog_post"/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
</LinearLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<org.briarproject.android.util.BriarRecyclerView
android:id="@+id/postList"
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="match_parent"
app:scrollToEnd="false"
tools:context=".android.blogs.BlogActivity"/>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
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">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_activity_horizontal">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
style="@style/BriarAvatar"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginRight="@dimen/margin_medium"
tools:src="@drawable/ic_launcher"/>
<TextView
android:id="@+id/authorName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:textSize="@dimen/text_size_tiny"
tools:text="Author Name"/>
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/avatar"
android:layout_below="@+id/authorName"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:gravity="bottom"
android:textSize="@dimen/text_size_tiny"
tools:text="yesterday"/>
<org.briarproject.android.util.TrustIndicatorView
android:id="@+id/trustIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_small"
android:layout_toRightOf="@+id/authorName"
tools:src="@drawable/trust_indicator_verified"/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/avatar"
android:layout_marginTop="@dimen/margin_medium"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="This Is A Blog Post Title"/>
<TextView
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/title"
android:layout_marginTop="@dimen/margin_medium"
tools:text="Body of Blog Post. This could be insanely large or just a short text as well."/>
</RelativeLayout>
</ScrollView>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This is just a placeholder to be replaced by the real My Blogs list -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/num"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="@dimen/margin_activity_horizontal"
android:textSize="128sp"
tools:text="1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/margin_activity_horizontal"
android:text="There is nothing for you to see here.\n\nMove along and come back later."
android:textSize="@dimen/text_size_large"/>
</LinearLayout>

View File

@@ -1,26 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This is just a placeholder to be replaced by the real My Blogs list -->
<LinearLayout
<org.briarproject.android.util.BriarRecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/num"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="@dimen/margin_activity_horizontal"
android:textSize="128sp"
tools:text="1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/margin_activity_horizontal"
android:text="There is nothing for you to see here.\n\nMove along and come back later."
android:textSize="@dimen/text_size_large"/>
</LinearLayout>
tools:listitem="@layout/list_item_blog"/>

View File

@@ -123,10 +123,7 @@
<Button
android:id="@+id/makeIntroductionButton"
style="@style/BriarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/introduction_button"
/>
android:text="@string/introduction_button"/>
</LinearLayout>

View File

@@ -0,0 +1,79 @@
<?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:background="?attr/selectableItemBackground">
<org.briarproject.android.util.TextAvatarView
android:id="@+id/avatarView"
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_marginEnd="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:layout_marginTop="@dimen/margin_medium"/>
<TextView
android:id="@+id/nameView"
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 blog"/>
<TextView
android:id="@+id/postCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/nameView"
android:layout_marginBottom="@dimen/margin_small"
android:layout_toEndOf="@+id/avatarView"
android:layout_toRightOf="@+id/avatarView"
android:paddingTop="@dimen/margin_small"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_small"
tools:text="1337 posts"/>
<TextView
android:id="@+id/dateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/nameView"
android:layout_marginEnd="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:paddingTop="@dimen/margin_small"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_small"
tools:text="Dec 24"/>
<TextView
android:id="@+id/statusView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/postCountView"
android:layout_toEndOf="@+id/avatarView"
android:layout_toRightOf="@+id/avatarView"
android:textColor="@color/briar_text_tertiary"
tools:text="@string/blogs_blog_is_empty"/>
<View
style="@style/Divider.ForumList"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/statusView"
android:layout_marginTop="@dimen/listitem_horizontal_margin"/>
</RelativeLayout>

View File

@@ -0,0 +1,118 @@
<?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:layout_marginTop="@dimen/listitem_vertical_margin"
android:background="?attr/selectableItemBackground">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
style="@style/BriarAvatar"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginBottom="@dimen/margin_medium"
android:layout_marginRight="@dimen/margin_medium"
tools:src="@drawable/ic_launcher"/>
<TextView
android:id="@+id/authorName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_tiny"
tools:text="Author Name"/>
<TextView
android:id="@+id/dateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/avatar"
android:layout_below="@+id/authorName"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:gravity="bottom"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_tiny"
tools:text="yesterday"/>
<TextView
android:id="@+id/newView"
style="@style/BriarTag"
android:layout_alignBottom="@+id/dateView"
android:layout_marginLeft="@dimen/margin_small"
android:layout_toRightOf="@+id/dateView"
android:text="@string/tag_new"
android:visibility="gone"/>
<org.briarproject.android.util.TrustIndicatorView
android:id="@+id/trustIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/authorName"
android:layout_alignTop="@+id/authorName"
android:layout_marginLeft="@dimen/margin_small"
android:layout_toRightOf="@+id/authorName"
android:scaleType="center"
tools:src="@drawable/trust_indicator_verified"/>
<ImageView
android:id="@+id/chatView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/commentView"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_chat"
android:visibility="gone"/>
<ImageView
android:id="@+id/commentView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_repeat"
android:visibility="gone"/>
<TextView
android:id="@+id/titleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/avatar"
android:layout_marginBottom="@dimen/margin_medium"
android:layout_marginEnd="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:ellipsize="end"
android:maxLines="3"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_large"
android:visibility="gone"
tools:text="This is a blog post title which can also be longer"/>
<TextView
android:id="@+id/bodyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/titleView"
android:layout_marginEnd="@dimen/margin_medium"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_medium"
tools:text="This is a body text that shows the content of a blog post. This one is not short, but it is also not too long."/>
<View
style="@style/Divider.ForumList"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/bodyView"
android:layout_marginTop="@dimen/listitem_vertical_margin"/>
</RelativeLayout>

View File

@@ -31,7 +31,7 @@
tools:text="This is a name of a forum"/>
<TextView
android:id="@+id/unreadView"
android:id="@+id/postCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/forumNameView"
@@ -62,7 +62,7 @@
style="@style/Divider.ForumList"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/unreadView"/>
android:layout_below="@+id/postCountView"/>
</RelativeLayout>

View File

@@ -34,10 +34,7 @@
<Button
android:id="@+id/shareForumButton"
style="@style/BriarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/forum_share_button"
/>
android:text="@string/forum_share_button"/>
</LinearLayout>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_write_blog_post"
android:icon="@drawable/forum_item_create_white"
android:title="@string/blogs_write_blog_post"
app:showAsAction="always"/>
</menu>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_create_blog"
android:icon="@drawable/ic_add_white"
android:title="@string/blogs_my_blogs_create"
app:showAsAction="ifRoom"/>
</menu>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_write_blog_post"
android:icon="@drawable/forum_item_create_white"
android:title="@string/blogs_write_blog_post"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_delete_blog"
android:icon="@drawable/action_delete_white"
android:title="@string/blogs_delete_blog"
app:showAsAction="never"/>
</menu>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_publish_blog_post"
android:title="@string/blogs_publish_blog_post"
android:icon="@drawable/social_send_now_white"
app:showAsAction="always"/>
</menu>

View File

@@ -20,6 +20,7 @@
<dimen name="text_size_xlarge">34sp</dimen>
<dimen name="listitem_horizontal_margin">16dp</dimen>
<dimen name="listitem_vertical_margin">10dp</dimen>
<dimen name="listitem_text_left_margin">72dp</dimen>
<dimen name="listitem_height_one_line_avatar">56dp</dimen>
<dimen name="listitem_height_contact_selector">68dp</dimen>

View File

@@ -84,12 +84,12 @@
<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="forum_no_posts">No posts</string>
<string name="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">
<plurals name="posts">
<item quantity="one">%d post</item>
<item quantity="other">%d posts</item>
</plurals>
@@ -251,9 +251,39 @@
<string name="progress_title_please_wait">Please wait..</string>
<!-- Blogs -->
<string name="blogs_button">Blogs</string>
<string name="blogs_button">Micro Blogs</string>
<string name="blogs_feed">Feed</string>
<string name="blogs_my_blogs">My Blogs</string>
<string name="blogs_my_blogs_create">Create Blog</string>
<string name="blogs_my_blogs_label">Add new Blog</string>
<string name="blogs_my_blogs_create_hint_title">Blog title (cannot be changed later)</string>
<string name="blogs_my_blogs_create_hint_desc">A short description of your new blog</string>
<string name="blogs_my_blogs_create_hint_desc_explanation">Potential readers may or may not subscribe to your blog based on the content of the description.</string>
<string name="blogs_my_blogs_empty_state">You don\'t have any blogs.\n\nWhy don\'t you create one now by clicking the plus in the top right screen corner?</string>
<string name="blogs_my_blogs_blog_empty_state">This is the place for content of your blog.\n\nIt seems like you haven\'t written anything yet.\n\nPlease tap the pen icon to compose a new blog post.\n\nDon\'t forget to go public and share your blog.</string>
<string name="blogs_my_blogs_created">Blog created</string>
<string name="blogs_blog_is_empty">This blog is empty</string>
<string name="blogs_other_blog_empty_state">This blog is currently empty.\n\nEither the author hasn\'t written anything yet, or the person who shared this blog with you needs to come online, so posts can be synchronized.</string>
<string name="tag_new">NEW</string>
<string name="blogs_post_more">more</string>
<string name="blogs_write_blog_post">Write Blog Post</string>
<string name="blogs_write_blog_post_title_hint">Add a title (optional)</string>
<string name="blogs_write_blog_post_body_hint">Type your blog post here</string>
<string name="blogs_publish_blog_post">Publish</string>
<string name="blogs_blog_post_created">Blog Post Created</string>
<string name="blogs_blog_post_received">New Blog Post Received</string>
<string name="blogs_blog_post_scroll_to">Scroll To</string>
<string name="blogs_blog_failed_to_load">Blog failed to load</string>
<string name="blogs_blog_post_failed_to_load">Blog Post failed to load</string>
<string name="blogs_feed_empty_state">This is the global blog feed.\n\nIt looks like nobody blogged anything, yet.\n\nBe the first and tap the pen icon to write a new blog post.</string>
<string name="blogs_delete_blog">Delete Blog</string>
<string name="blogs_delete_blog_dialog_message">Are you sure that you want to delete this Blog and all posts?\nNote that this will not delete the blog from other people\'s devices.</string>
<string name="blogs_delete_blog_ok">Delete Blog</string>
<string name="blogs_delete_blog_cancel">Keep</string>
<string name="blogs_blog_deleted">Blog Deleted</string>
<string name="blogs_remove_blog">Remove Blog</string>
<string name="blogs_blog_list">Blog List</string>
<string name="blogs_available_blogs">Available Blogs</string>
<string name="blogs_drafts">Drafts</string>

View File

@@ -23,6 +23,8 @@
</style>
<style name="BriarButton" parent="Widget.AppCompat.Button.Colored">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">@dimen/text_size_medium</item>
<item name="android:padding">@dimen/margin_large</item>
</style>
@@ -55,6 +57,17 @@
<item name="android:textColor">@android:color/primary_text_light</item>
</style>
<style name="BriarTag">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginRight">@dimen/margin_medium</item>
<item name="android:paddingLeft">3dp</item>
<item name="android:paddingRight">3dp</item>
<item name="android:background">@color/briar_primary</item>
<item name="android:textSize">@dimen/text_size_tiny</item>
<item name="android:textColor">@color/briar_text_primary_inverse</item>
</style>
<style name="Divider">
<item name="android:background">@color/divider</item>
</style>
@@ -109,4 +122,9 @@
<item name="tabTextColor">@color/briar_text_primary_inverse</item>
</style>
<!-- This fixes the missing TextAppearance.Design.Counter.Overflow style -->
<style name="BriarTextCounter.Overflow" parent="TextAppearance.Design.Counter">
<item name="android:textColor">@color/briar_button_negative</item>
</style>
</resources>

View File

@@ -42,6 +42,8 @@
<item name="colorPrimary">@color/briar_primary</item>
<item name="colorPrimaryDark">@color/briar_primary_dark</item>
<item name="colorAccent">@color/briar_accent</item>
<item name="buttonBarPositiveButtonStyle">@style/BriarButtonFlat.Positive</item>
<item name="buttonBarNegativeButtonStyle">@style/BriarButtonFlat.Negative</item>
<item name="android:textColorPrimary">@color/briar_text_primary</item>
<item name="android:textColorPrimaryInverse">@color/briar_text_primary_inverse</item>
<item name="android:textColorSecondary">@color/briar_text_secondary</item>
@@ -49,6 +51,12 @@
<item name="android:textColorTertiary">@color/briar_text_tertiary</item>
<item name="android:textColorTertiaryInverse">@color/briar_text_tertiary_inverse</item>
<item name="android:textColorLink">@color/briar_text_link</item>
<item name="android:windowAnimationStyle">@style/DialogAnimation</item>
</style>
<style name="DialogAnimation" parent="@android:style/Animation.Dialog">
<item name="android:windowEnterAnimation">@android:anim/fade_in</item>
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>
<!-- This fixes a UI bug in the support preference library -->

View File

@@ -2,7 +2,15 @@ package org.briarproject.android;
import android.app.Activity;
import org.briarproject.android.blogs.BlogActivity;
import org.briarproject.android.blogs.BlogFragment;
import org.briarproject.android.blogs.BlogListFragment;
import org.briarproject.android.blogs.BlogPostFragment;
import org.briarproject.android.blogs.BlogsFragment;
import org.briarproject.android.blogs.CreateBlogActivity;
import org.briarproject.android.blogs.FeedFragment;
import org.briarproject.android.blogs.MyBlogsFragment;
import org.briarproject.android.blogs.WriteBlogPostActivity;
import org.briarproject.android.contact.ContactListFragment;
import org.briarproject.android.contact.ConversationActivity;
import org.briarproject.android.forum.ForumInvitationsActivity;
@@ -13,7 +21,6 @@ import org.briarproject.android.forum.ForumListFragment;
import org.briarproject.android.forum.ForumSharingStatusActivity;
import org.briarproject.android.forum.ShareForumActivity;
import org.briarproject.android.forum.ShareForumMessageFragment;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.introduction.ContactChooserFragment;
import org.briarproject.android.introduction.IntroductionActivity;
@@ -64,6 +71,16 @@ public interface ActivityComponent {
void inject(ForumActivity activity);
void inject(CreateBlogActivity activity);
void inject(BlogActivity activity);
void inject(WriteBlogPostActivity activity);
void inject(BlogFragment fragment);
void inject(BlogPostFragment fragment);
void inject(SettingsActivity activity);
void inject(ChangePasswordActivity activity);
@@ -73,7 +90,9 @@ public interface ActivityComponent {
// Fragments
void inject(ContactListFragment fragment);
void inject(ForumListFragment fragment);
void inject(BaseFragment fragment);
void inject(BlogsFragment fragment);
void inject(BlogListFragment fragment);
void inject(FeedFragment fragment);
void inject(MyBlogsFragment fragment);
void inject(ChooseIdentityFragment fragment);
void inject(ShowQrCodeFragment fragment);

View File

@@ -4,6 +4,10 @@ import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import org.briarproject.android.blogs.BlogController;
import org.briarproject.android.blogs.BlogControllerImpl;
import org.briarproject.android.blogs.FeedController;
import org.briarproject.android.blogs.FeedControllerImpl;
import org.briarproject.android.controller.BriarController;
import org.briarproject.android.controller.BriarControllerImpl;
import org.briarproject.android.controller.ConfigController;
@@ -107,6 +111,20 @@ public class ActivityModule {
return forumController;
}
@ActivityScope
@Provides
BlogController provideBlogController(BlogControllerImpl blogController) {
activity.addLifecycleController(blogController);
return blogController;
}
@ActivityScope
@Provides
protected FeedController provideFeedController(
FeedControllerImpl feedController) {
return feedController;
}
@ActivityScope
@Provides
protected NavDrawerController provideNavDrawerController(

View File

@@ -5,8 +5,11 @@ import org.briarproject.CoreModule;
import org.briarproject.android.api.AndroidExecutor;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.api.ReferenceManager;
import org.briarproject.android.blogs.BlogPersistentData;
import org.briarproject.android.forum.ForumPersistentData;
import org.briarproject.android.report.BriarReportSender;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogPostFactory;
import org.briarproject.api.contact.ContactExchangeTask;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
@@ -96,6 +99,10 @@ public interface AndroidComponent extends CoreEagerSingletons {
ForumPostFactory forumPostFactory();
BlogManager blogManager();
BlogPostFactory blogPostFactory();
SettingsManager settingsManager();
ContactExchangeTask contactExchangeTask();
@@ -112,6 +119,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
ForumPersistentData forumPersistentData();
BlogPersistentData blogPersistentData();
@IoExecutor
Executor ioExecutor();

View File

@@ -4,6 +4,7 @@ import android.app.Application;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.api.ReferenceManager;
import org.briarproject.android.blogs.BlogPersistentData;
import org.briarproject.android.forum.ForumPersistentData;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.PublicKey;
@@ -143,4 +144,10 @@ public class AppModule {
ForumPersistentData provideForumPersistence() {
return new ForumPersistentData();
}
@Provides
@Singleton
BlogPersistentData provideBlogPersistence() {
return new BlogPersistentData();
}
}

View File

@@ -0,0 +1,261 @@
package org.briarproject.android.blogs;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.blogs.BlogController.BlogPostListener;
import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
public class BlogActivity extends BriarActivity implements BlogPostListener,
OnBlogPostClickListener, BaseFragmentListener {
static final int REQUEST_WRITE_POST = 1;
static final String BLOG_NAME = "briar.BLOG_NAME";
static final String IS_MY_BLOG = "briar.IS_MY_BLOG";
static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG";
private static final String BLOG_PAGER_ADAPTER = "briar.BLOG_PAGER_ADAPTER";
private static final Logger LOG =
Logger.getLogger(BlogActivity.class.getName());
private ProgressBar progressBar;
private ViewPager pager;
private BlogPagerAdapter blogPagerAdapter;
private BlogPostPagerAdapter postPagerAdapter;
private String blogName;
private boolean myBlog, isNew;
// Fields that are accessed from background threads must be volatile
private volatile GroupId groupId = null;
@Inject
BlogController blogController;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
// GroupId from Intent
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No Group in intent.");
groupId = new GroupId(b);
// Name of the Blog from Intent
blogName = i.getStringExtra(BLOG_NAME);
if (blogName != null) setTitle(blogName);
// Is this our blog and was it just created?
myBlog = i.getBooleanExtra(IS_MY_BLOG, false);
isNew = i.getBooleanExtra(IS_NEW_BLOG, false);
setContentView(R.layout.activity_blog);
pager = (ViewPager) findViewById(R.id.pager);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
hideLoadingScreen();
blogPagerAdapter = new BlogPagerAdapter(getSupportFragmentManager());
if (state == null || state.getBoolean(BLOG_PAGER_ADAPTER, true)) {
pager.setAdapter(blogPagerAdapter);
} else {
// this initializes and restores the postPagerAdapter
loadBlogPosts();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// remember which adapter we had active
outState.putBoolean(BLOG_PAGER_ADAPTER,
pager.getAdapter() == blogPagerAdapter);
}
@Override
public void onBackPressed() {
if (pager.getAdapter() == postPagerAdapter) {
pager.setAdapter(blogPagerAdapter);
} else {
super.onBackPressed();
}
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public void showLoadingScreen(boolean isBlocking, int stringId) {
progressBar.setVisibility(VISIBLE);
}
private void showLoadingScreen() {
showLoadingScreen(false, 0);
}
@Override
public void hideLoadingScreen() {
progressBar.setVisibility(GONE);
}
@Override
public void onFragmentCreated(String tag) {
}
@Override
public void onBlogPostClick(final int position) {
loadBlogPosts(position, true);
}
private void loadBlogPosts() {
loadBlogPosts(0, false);
}
private void loadBlogPosts(final int position, final boolean setItem) {
showLoadingScreen();
blogController
.loadBlog(groupId, false, new UiResultHandler<Boolean>(this) {
@Override
public void onResultUi(Boolean result) {
if (result) {
Collection<BlogPostItem> posts =
blogController.getBlogPosts();
if (postPagerAdapter == null) {
postPagerAdapter = new BlogPostPagerAdapter(
getSupportFragmentManager(),
posts.size());
} else {
postPagerAdapter.setSize(posts.size());
}
pager.setAdapter(postPagerAdapter);
if (setItem) pager.setCurrentItem(position);
} else {
Toast.makeText(BlogActivity.this,
R.string.blogs_blog_post_failed_to_load,
LENGTH_SHORT).show();
}
}
});
}
@Override
public void onBlogPostAdded(final BlogPostItem post, final boolean local) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (blogPagerAdapter != null) {
BlogFragment f = blogPagerAdapter.getFragment();
if (f != null && f.isVisible()) {
f.onBlogPostAdded(post, local);
}
}
if (postPagerAdapter != null) {
postPagerAdapter.onBlogPostAdded();
postPagerAdapter.notifyDataSetChanged();
}
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
// The BlogPostAddedEvent arrives when the controller is not listening,
// so we need to manually reload the blog posts :(
if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) {
BlogFragment f = blogPagerAdapter.getFragment();
if (f != null && f.isVisible()) {
f.reload();
}
}
}
private class BlogPagerAdapter extends FragmentStatePagerAdapter {
private BlogFragment fragment = null;
BlogPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public int getCount() {
return 1;
}
@Override
public Fragment getItem(int position) {
return BlogFragment.newInstance(groupId, blogName, myBlog, isNew);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
// save a reference to the single fragment here for later
fragment =
(BlogFragment) super.instantiateItem(container, position);
return fragment;
}
BlogFragment getFragment() {
return fragment;
}
}
private class BlogPostPagerAdapter extends FragmentStatePagerAdapter {
private int size;
BlogPostPagerAdapter(FragmentManager fm, int size) {
super(fm);
this.size = size;
}
@Override
public int getCount() {
return size;
}
@Override
public Fragment getItem(int position) {
MessageId postIdOfPos = blogController.getBlogPostId(position);
return BlogPostFragment.newInstance(groupId, postIdOfPos);
}
void onBlogPostAdded() {
size++;
}
void setSize(int size) {
this.size = size;
}
}
}

View File

@@ -0,0 +1,31 @@
package org.briarproject.android.blogs;
import android.support.annotation.Nullable;
import org.briarproject.android.controller.ActivityLifecycleController;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import java.util.TreeSet;
public interface BlogController extends ActivityLifecycleController {
void loadBlog(final GroupId groupId, final boolean reload,
final UiResultHandler<Boolean> resultHandler);
TreeSet<BlogPostItem> getBlogPosts();
@Nullable
BlogPostItem getBlogPost(MessageId postId);
@Nullable
MessageId getBlogPostId(int position);
void deleteBlog(final UiResultHandler<Boolean> resultHandler);
interface BlogPostListener {
void onBlogPostAdded(final BlogPostItem post, final boolean local);
}
}

View File

@@ -0,0 +1,194 @@
package org.briarproject.android.blogs;
import android.app.Activity;
import android.support.annotation.Nullable;
import org.briarproject.android.controller.DbControllerImpl;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.BlogPostAddedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.GroupRemovedEvent;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.TreeSet;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
public class BlogControllerImpl extends DbControllerImpl
implements BlogController, EventListener {
private static final Logger LOG =
Logger.getLogger(BlogControllerImpl.class.getName());
@Inject
protected Activity activity;
@Inject
protected volatile BlogManager blogManager;
@Inject
protected volatile EventBus eventBus;
@Inject
protected BlogPersistentData data;
private volatile BlogPostListener listener;
@Inject
BlogControllerImpl() {
}
@Override
public void onActivityCreate() {
if (activity instanceof BlogPostListener) {
listener = (BlogPostListener) activity;
} else {
throw new IllegalStateException(
"An activity that injects the BlogController must " +
"implement the BlogPostListener");
}
}
@Override
public void onActivityResume() {
eventBus.addListener(this);
}
@Override
public void onActivityPause() {
eventBus.removeListener(this);
}
@Override
public void onActivityDestroy() {
if (activity.isFinishing()) {
data.clearAll();
}
}
@Override
public void eventOccurred(Event e) {
if (e instanceof BlogPostAddedEvent) {
final BlogPostAddedEvent m = (BlogPostAddedEvent) e;
if (m.getGroupId().equals(data.getGroupId())) {
LOG.info("New blog post added");
final BlogPostHeader header = m.getHeader();
try {
final byte[] body = blogManager.getPostBody(header.getId());
final BlogPostItem post = new BlogPostItem(header, body);
data.addPost(post);
listener.onBlogPostAdded(post, m.isLocal());
} catch (DbException ex) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, ex.toString(), ex);
}
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent s = (GroupRemovedEvent) e;
if (s.getGroup().getId().equals(data.getGroupId())) {
LOG.info("Blog removed");
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.finish();
}
});
}
}
}
@Override
public void loadBlog(final GroupId groupId, final boolean reload,
final UiResultHandler<Boolean> resultHandler) {
LOG.info("Loading blog...");
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
if (reload || data.getGroupId() == null ||
!data.getGroupId().equals(groupId)) {
data.setGroupId(groupId);
// load blog posts
long now = System.currentTimeMillis();
Collection<BlogPostItem> posts = new ArrayList<>();
Collection<BlogPostHeader> header =
blogManager.getPostHeaders(groupId);
for (BlogPostHeader h : header) {
byte[] body = blogManager.getPostBody(h.getId());
posts.add(new BlogPostItem(h, body));
}
data.setPosts(posts);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Post header load took " + duration +
" ms");
}
resultHandler.onResult(true);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false);
}
}
});
}
@Override
public TreeSet<BlogPostItem> getBlogPosts() {
return data.getBlogPosts();
}
@Override
@Nullable
public BlogPostItem getBlogPost(MessageId id) {
for (BlogPostItem item : getBlogPosts()) {
if (item.getId().equals(id)) return item;
}
return null;
}
@Override
@Nullable
public MessageId getBlogPostId(int position) {
int i = 0;
for (BlogPostItem post : getBlogPosts()) {
if (i == position) return post.getId();
i++;
}
return null;
}
@Override
public void deleteBlog(final UiResultHandler<Boolean> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
if (data.getGroupId() == null) {
resultHandler.onResult(false);
return;
}
try {
Blog b = blogManager.getBlog(data.getGroupId());
blogManager.removeBlog(b);
resultHandler.onResult(true);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false);
}
}
});
}
}

View File

@@ -0,0 +1,228 @@
package org.briarproject.android.blogs;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v7.app.AlertDialog;
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.Toast;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.blogs.BlogController.BlogPostListener;
import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.sync.GroupId;
import java.util.Collection;
import javax.inject.Inject;
import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
import static android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.android.BriarActivity.GROUP_ID;
import static org.briarproject.android.blogs.BlogActivity.BLOG_NAME;
import static org.briarproject.android.blogs.BlogActivity.IS_MY_BLOG;
import static org.briarproject.android.blogs.BlogActivity.IS_NEW_BLOG;
import static org.briarproject.android.blogs.BlogActivity.REQUEST_WRITE_POST;
public class BlogFragment extends BaseFragment implements BlogPostListener {
public final static String TAG = BlogFragment.class.getName();
@Inject
BlogController blogController;
private GroupId groupId;
private String blogName;
private boolean myBlog;
private BlogPostAdapter adapter;
private BriarRecyclerView list;
static BlogFragment newInstance(GroupId groupId, String name,
boolean myBlog, boolean isNew) {
BlogFragment f = new BlogFragment();
Bundle bundle = new Bundle();
bundle.putByteArray(GROUP_ID, groupId.getBytes());
bundle.putString(BLOG_NAME, name);
bundle.putBoolean(IS_MY_BLOG, myBlog);
bundle.putBoolean(IS_NEW_BLOG, isNew);
f.setArguments(bundle);
return f;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
setHasOptionsMenu(true);
Bundle args = getArguments();
byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No Group found.");
groupId = new GroupId(b);
blogName = args.getString(BLOG_NAME);
myBlog = args.getBoolean(IS_MY_BLOG);
boolean isNew = args.getBoolean(IS_NEW_BLOG);
View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter = new BlogPostAdapter(getActivity(),
(OnBlogPostClickListener) getActivity());
list = (BriarRecyclerView) v.findViewById(R.id.postList);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
if (myBlog) {
list.setEmptyText(
getString(R.string.blogs_my_blogs_blog_empty_state));
} else {
list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
}
// show snackbar if this blog was just created
if (isNew) {
Snackbar s = Snackbar.make(list, R.string.blogs_my_blogs_created,
LENGTH_LONG);
s.getView().setBackgroundResource(R.color.briar_primary);
s.show();
// show only once
args.putBoolean(IS_NEW_BLOG, false);
}
return v;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public void onStart() {
super.onStart();
loadData(false);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (myBlog) {
inflater.inflate(R.menu.blogs_my_blog_actions, menu);
}
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
getActivity().onBackPressed();
return true;
case R.id.action_write_blog_post:
Intent i =
new Intent(getActivity(), WriteBlogPostActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
i.putExtra(BLOG_NAME, blogName);
ActivityOptionsCompat options =
makeCustomAnimation(getActivity(),
android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
ActivityCompat.startActivityForResult(getActivity(), i,
REQUEST_WRITE_POST, options.toBundle());
return true;
case R.id.action_delete_blog:
showDeleteDialog();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void onBlogPostAdded(BlogPostItem post, boolean local) {
adapter.add(post);
if (local) list.scrollToPosition(0);
}
private void loadData(final boolean reload) {
blogController.loadBlog(groupId, reload,
new UiResultHandler<Boolean>(getActivity()) {
@Override
public void onResultUi(Boolean result) {
if (result) {
Collection<BlogPostItem> posts =
blogController.getBlogPosts();
if (posts.size() > 0) {
adapter.addAll(posts);
if (reload) list.scrollToPosition(0);
} else {
list.showData();
}
} else {
Toast.makeText(getActivity(),
R.string.blogs_blog_failed_to_load,
LENGTH_SHORT).show();
getActivity().supportFinishAfterTransition();
}
}
});
}
void reload() {
loadData(true);
}
private void showDeleteDialog() {
DialogInterface.OnClickListener okListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
deleteBlog();
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.blogs_delete_blog));
builder.setMessage(
getString(R.string.blogs_delete_blog_dialog_message));
builder.setPositiveButton(R.string.blogs_delete_blog_cancel, null);
builder.setNegativeButton(R.string.blogs_delete_blog_ok, okListener);
builder.show();
}
private void deleteBlog() {
blogController.deleteBlog(
new UiResultHandler<Boolean>(getActivity()) {
@Override
public void onResultUi(Boolean result) {
if (!result) return;
Toast.makeText(getActivity(),
R.string.blogs_blog_deleted, LENGTH_SHORT)
.show();
getActivity().supportFinishAfterTransition();
}
});
}
}

View File

@@ -0,0 +1,210 @@
package org.briarproject.android.blogs;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
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.TextView;
import org.briarproject.R;
import org.briarproject.android.util.TextAvatarView;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.sync.GroupId;
import java.util.Collection;
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
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.blogs.BlogActivity.BLOG_NAME;
import static org.briarproject.android.blogs.BlogActivity.IS_MY_BLOG;
class BlogListAdapter extends
RecyclerView.Adapter<BlogListAdapter.BlogViewHolder> {
private SortedList<BlogListItem> blogs = new SortedList<>(
BlogListItem.class, new SortedList.Callback<BlogListItem>() {
@Override
public int compare(BlogListItem a, BlogListItem b) {
if (a == b) return 0;
// The blog 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 blog name
String aName = a.getName();
String bName = b.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(BlogListItem a, BlogListItem b) {
return a.getBlog().equals(b.getBlog()) &&
a.getTimestamp() == b.getTimestamp() &&
a.getUnreadCount() == b.getUnreadCount();
}
@Override
public boolean areItemsTheSame(BlogListItem a, BlogListItem b) {
return a.getBlog().equals(b.getBlog());
}
});
private final Activity ctx;
BlogListAdapter(Activity ctx) {
this.ctx = ctx;
}
@Override
public BlogViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(ctx).inflate(
R.layout.list_item_blog, parent, false);
return new BlogViewHolder(v);
}
@Override
public void onBindViewHolder(BlogViewHolder ui, int position) {
final BlogListItem item = getItem(position);
// Avatar
ui.avatar.setText(item.getName().substring(0, 1));
ui.avatar.setBackgroundBytes(item.getBlog().getId().getBytes());
ui.avatar.setUnreadCount(item.getUnreadCount());
// Blog Name
ui.name.setText(item.getName());
// Post Count
int postCount = item.getPostCount();
ui.postCount.setText(ctx.getResources()
.getQuantityString(R.plurals.posts, postCount, postCount));
ui.postCount.setTextColor(
ContextCompat.getColor(ctx, R.color.briar_text_secondary));
// Date and Status
if (item.isEmpty()) {
ui.date.setVisibility(GONE);
ui.avatar.setProblem(true);
ui.status.setText(ctx.getString(R.string.blogs_blog_is_empty));
ui.status.setVisibility(VISIBLE);
} else {
long timestamp = item.getTimestamp();
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, timestamp));
ui.date.setVisibility(VISIBLE);
ui.avatar.setProblem(false);
ui.status.setVisibility(GONE);
}
// Open Blog on Click
ui.layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(ctx, BlogActivity.class);
Blog b = item.getBlog();
i.putExtra(GROUP_ID, b.getId().getBytes());
i.putExtra(BLOG_NAME, b.getName());
i.putExtra(IS_MY_BLOG, item.isOurs());
ActivityOptionsCompat options = ActivityOptionsCompat
.makeCustomAnimation(ctx, android.R.anim.fade_in,
android.R.anim.fade_out);
ActivityCompat.startActivity(ctx, i, options.toBundle());
}
});
}
@Override
public int getItemCount() {
return blogs.size();
}
public BlogListItem getItem(int position) {
return blogs.get(position);
}
@Nullable
public BlogListItem getItem(GroupId g) {
for (int i = 0; i < blogs.size(); i++) {
BlogListItem item = blogs.get(i);
if (item.getBlog().getGroup().getId().equals(g)) {
return item;
}
}
return null;
}
public void addAll(Collection<BlogListItem> items) {
blogs.addAll(items);
}
void updateItem(BlogListItem item) {
BlogListItem oldItem = getItem(item.getBlog().getGroup().getId());
int position = blogs.indexOf(oldItem);
blogs.updateItemAt(position, item);
}
public void remove(BlogListItem item) {
blogs.remove(item);
}
public void clear() {
blogs.clear();
}
public boolean isEmpty() {
return blogs.size() == 0;
}
static class BlogViewHolder extends RecyclerView.ViewHolder {
private final ViewGroup layout;
private final TextAvatarView avatar;
private final TextView name;
private final TextView postCount;
private final TextView date;
private final TextView status;
BlogViewHolder(View v) {
super(v);
layout = (ViewGroup) v;
avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
name = (TextView) v.findViewById(R.id.nameView);
postCount = (TextView) v.findViewById(R.id.postCountView);
date = (TextView) v.findViewById(R.id.dateView);
status = (TextView) v.findViewById(R.id.statusView);
}
}
}

View File

@@ -0,0 +1,53 @@
package org.briarproject.android.blogs;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.fragment.BaseFragment;
public class BlogListFragment extends BaseFragment {
public final static String TAG = BlogListFragment.class.getName();
static BlogListFragment newInstance(int num) {
BlogListFragment f = new BlogListFragment();
Bundle args = new Bundle();
args.putInt("num", num);
f.setArguments(args);
return f;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_blogs_list, container,
false);
TextView numView = (TextView) v.findViewById(R.id.num);
String num = String.valueOf(getArguments().getInt("num"));
numView.setText(num);
return v;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public String getUniqueTag() {
return TAG;
}
}

View File

@@ -0,0 +1,67 @@
package org.briarproject.android.blogs;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogPostHeader;
import java.util.Collection;
class BlogListItem {
private final Blog blog;
private final int postCount;
private final long timestamp;
private final int unread;
private final boolean ours;
BlogListItem(Blog blog, Collection<BlogPostHeader> headers, boolean ours) {
this.blog = blog;
if (headers.isEmpty()) {
postCount = 0;
timestamp = 0;
unread = 0;
} else {
BlogPostHeader newest = null;
long timestamp = -1;
int unread = 0;
for (BlogPostHeader h : headers) {
if (h.getTimestamp() > timestamp) {
timestamp = h.getTimestamp();
newest = h;
}
if (!h.isRead()) unread++;
}
this.postCount = headers.size();
this.timestamp = newest.getTimestamp();
this.unread = unread;
}
this.ours = ours;
}
Blog getBlog() {
return blog;
}
String getName() {
return blog.getName();
}
boolean isEmpty() {
return postCount == 0;
}
int getPostCount() {
return postCount;
}
long getTimestamp() {
return timestamp;
}
int getUnreadCount() {
return unread;
}
boolean isOurs() {
return ours;
}
}

View File

@@ -0,0 +1,49 @@
package org.briarproject.android.blogs;
import org.briarproject.api.sync.GroupId;
import java.util.Collection;
import java.util.TreeSet;
import javax.inject.Inject;
/**
* This class is a singleton that defines the data that should persist, i.e.
* still be present in memory after activity restarts. This class is not thread
* safe.
*/
public class BlogPersistentData {
private volatile GroupId groupId;
private volatile TreeSet<BlogPostItem> posts = new TreeSet<>();
public BlogPersistentData() {
}
public void setGroupId(GroupId groupId) {
this.groupId = groupId;
}
public GroupId getGroupId() {
return groupId;
}
public void setPosts(Collection<BlogPostItem> posts) {
this.posts.clear();
this.posts.addAll(posts);
}
void addPost(BlogPostItem post) {
posts.add(post);
}
TreeSet<BlogPostItem> getBlogPosts() {
return posts;
}
void clearAll() {
groupId = null;
posts.clear();
}
}

View File

@@ -0,0 +1,166 @@
package org.briarproject.android.blogs;
import android.content.Context;
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.ImageView;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.util.TrustIndicatorView;
import org.briarproject.api.identity.Author;
import org.briarproject.util.StringUtils;
import java.util.Collection;
import de.hdodenhof.circleimageview.CircleImageView;
import im.delight.android.identicons.IdenticonDrawable;
class BlogPostAdapter extends
RecyclerView.Adapter<BlogPostAdapter.BlogPostHolder> {
private SortedList<BlogPostItem> posts = new SortedList<>(
BlogPostItem.class, new SortedList.Callback<BlogPostItem>() {
@Override
public int compare(BlogPostItem a, BlogPostItem b) {
return a.compareTo(b);
}
@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(BlogPostItem a, BlogPostItem b) {
return a.isRead() == b.isRead();
}
@Override
public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) {
return a.getId().equals(b.getId());
}
});
private final Context ctx;
private final OnBlogPostClickListener listener;
BlogPostAdapter(Context ctx, OnBlogPostClickListener listener) {
this.ctx = ctx;
this.listener = listener;
}
@Override
public BlogPostHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(ctx).inflate(
R.layout.list_item_blog_post, parent, false);
return new BlogPostHolder(v);
}
@Override
public void onBindViewHolder(final BlogPostHolder ui, int position) {
final BlogPostItem post = getItem(position);
Author author = post.getAuthor();
IdenticonDrawable d = new IdenticonDrawable(author.getId().getBytes());
ui.avatar.setImageDrawable(d);
ui.author.setText(author.getName());
ui.trust.setTrustLevel(post.getAuthorStatus());
// date
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, post.getTimestamp()));
// post body
ui.body.setText(StringUtils.fromUtf8(post.getBody()));
ui.layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onBlogPostClick(ui.getAdapterPosition());
}
});
}
@Override
public int getItemCount() {
return posts.size();
}
public BlogPostItem getItem(int position) {
return posts.get(position);
}
public void add(BlogPostItem item) {
posts.add(item);
}
public void addAll(Collection<BlogPostItem> items) {
posts.addAll(items);
}
public void remove(BlogPostItem item) {
posts.remove(item);
}
public void clear() {
posts.clear();
}
public boolean isEmpty() {
return posts.size() == 0;
}
static class BlogPostHolder extends RecyclerView.ViewHolder {
private final ViewGroup layout;
private final CircleImageView avatar;
private final TextView author;
private final TrustIndicatorView trust;
private final TextView date;
private final TextView unread;
private final ImageView chat;
private final ImageView comment;
private final TextView title;
private final TextView body;
BlogPostHolder(View v) {
super(v);
layout = (ViewGroup) v;
avatar = (CircleImageView) v.findViewById(R.id.avatar);
author = (TextView) v.findViewById(R.id.authorName);
trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator);
date = (TextView) v.findViewById(R.id.dateView);
unread = (TextView) v.findViewById(R.id.newView);
chat = (ImageView) v.findViewById(R.id.chatView);
comment = (ImageView) v.findViewById(R.id.commentView);
title = (TextView) v.findViewById(R.id.titleView);
body = (TextView) v.findViewById(R.id.bodyView);
}
}
interface OnBlogPostClickListener {
void onBlogPostClick(int position);
}
}

View File

@@ -0,0 +1,157 @@
package org.briarproject.android.blogs;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.util.TrustIndicatorView;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils;
import javax.inject.Inject;
import im.delight.android.identicons.IdenticonDrawable;
import static android.view.View.GONE;
import static android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.android.BriarActivity.GROUP_ID;
public class BlogPostFragment extends BaseFragment {
public final static String TAG = BlogPostFragment.class.getName();
private final static String BLOG_POST_ID = "briar.BLOG_NAME";
private GroupId groupId;
private MessageId postId;
private BlogPostViewHolder ui;
@Inject
BlogController blogController;
static BlogPostFragment newInstance(GroupId groupId, MessageId postId) {
BlogPostFragment f = new BlogPostFragment();
Bundle bundle = new Bundle();
bundle.putByteArray(GROUP_ID, groupId.getBytes());
bundle.putByteArray(BLOG_POST_ID, postId.getBytes());
f.setArguments(bundle);
return f;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
setHasOptionsMenu(true);
byte[] b = getArguments().getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No Group found.");
groupId = new GroupId(b);
byte[] p = getArguments().getByteArray(BLOG_POST_ID);
if (p == null) throw new IllegalStateException("No MessageId found.");
postId = new MessageId(p);
View v = inflater.inflate(R.layout.fragment_blog_post, container,
false);
ui = new BlogPostViewHolder(v);
return v;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public void onStart() {
super.onStart();
blogController.loadBlog(groupId, false,
new UiResultHandler<Boolean>((Activity) listener) {
@Override
public void onResultUi(Boolean result) {
listener.hideLoadingScreen();
if (result) {
BlogPostItem post =
blogController.getBlogPost(postId);
if (post != null) {
bind(post);
}
} else {
Toast.makeText(getActivity(),
R.string.blogs_blog_post_failed_to_load,
LENGTH_SHORT).show();
}
}
});
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
getActivity().onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public String getUniqueTag() {
return TAG;
}
private void bind(BlogPostItem post) {
Author author = post.getAuthor();
IdenticonDrawable d = new IdenticonDrawable(author.getId().getBytes());
ui.avatar.setImageDrawable(d);
ui.authorName.setText(author.getName());
ui.trust.setTrustLevel(post.getAuthorStatus());
ui.date.setText(
DateUtils.getRelativeTimeSpanString(post.getTimestamp()));
if (post.getTitle() != null) {
ui.title.setText(post.getTitle());
} else {
ui.title.setVisibility(GONE);
}
ui.body.setText(StringUtils.fromUtf8(post.getBody()));
}
private static class BlogPostViewHolder {
private ImageView avatar;
private TextView authorName;
private TrustIndicatorView trust;
private TextView date;
private TextView title;
private TextView body;
BlogPostViewHolder(View v) {
avatar = (ImageView) v.findViewById(R.id.avatar);
authorName = (TextView) v.findViewById(R.id.authorName);
trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator);
date = (TextView) v.findViewById(R.id.date);
title = (TextView) v.findViewById(R.id.title);
body = (TextView) v.findViewById(R.id.body);
}
}
}

View File

@@ -0,0 +1,73 @@
package org.briarproject.android.blogs;
import android.support.annotation.NonNull;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.Author.Status;
import org.briarproject.api.sync.MessageId;
// This class is not thread-safe
class BlogPostItem implements Comparable<BlogPostItem> {
private final BlogPostHeader header;
private final byte[] body;
private boolean read;
BlogPostItem(BlogPostHeader header, byte[] body) {
this.header = header;
this.body = body;
read = header.isRead();
}
public MessageId getId() {
return header.getId();
}
public String getTitle() {
return header.getTitle();
}
public byte[] getBody() {
return body;
}
public long getTimestamp() {
return header.getTimestamp();
}
public long getTimeReceived() {
return header.getTimeReceived();
}
public Author getAuthor() {
return header.getAuthor();
}
Status getAuthorStatus() {
return header.getAuthorStatus();
}
public void setRead(boolean read) {
this.read = read;
}
public boolean isRead() {
return read;
}
@Override
public int compareTo(@NonNull BlogPostItem other) {
if (this == other) return 0;
// The blog with the newest message comes first
long aTime = getTimeReceived(), bTime = other.getTimeReceived();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
// Break ties by post title
if (getTitle() != null && other.getTitle() != null) {
return String.CASE_INSENSITIVE_ORDER
.compare(getTitle(), other.getTitle());
}
return 0;
}
}

View File

@@ -15,6 +15,8 @@ import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.fragment.BaseFragment;
import static android.view.View.GONE;
public class BlogsFragment extends BaseFragment {
public final static String TAG = BlogsFragment.class.getName();
@@ -54,6 +56,8 @@ public class BlogsFragment extends BaseFragment {
viewPager.setAdapter(tabAdapter);
tabLayout.setupWithViewPager(viewPager);
tabLayout.setVisibility(GONE);
if (savedInstanceState != null) {
int position = savedInstanceState.getInt(SELECTED_TAB, 0);
viewPager.setCurrentItem(position);
@@ -88,16 +92,21 @@ public class BlogsFragment extends BaseFragment {
@Override
public int getCount() {
return titles.length;
return 1;
// return titles.length;
}
@Override
public Fragment getItem(int position) {
switch (position) {
// TODO add your fragments here
default:
return MyBlogsFragment.newInstance(position);
}
return FeedFragment.newInstance();
// switch (position) {
// case 0:
// return FeedFragment.newInstance();
// case 1:
// return new MyBlogsFragment();
// default:
// return BlogListFragment.newInstance(position);
// }
}
@Override

View File

@@ -0,0 +1,195 @@
package org.briarproject.android.blogs;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.BriarActivity;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.db.DbException;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.util.StringUtils;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.blogs.BlogActivity.BLOG_NAME;
import static org.briarproject.android.blogs.BlogActivity.IS_MY_BLOG;
import static org.briarproject.android.blogs.BlogActivity.IS_NEW_BLOG;
import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_DESC_LENGTH;
import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_TITLE_LENGTH;
public class CreateBlogActivity extends BriarActivity
implements OnEditorActionListener, OnClickListener {
private static final Logger LOG =
Logger.getLogger(CreateBlogActivity.class.getName());
private TextInputEditText titleInput, descInput;
private Button button;
private ProgressBar progress;
// Fields that are accessed from background threads must be volatile
@Inject
protected volatile IdentityManager identityManager;
@Inject
volatile BlogManager blogManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_create_blog);
TextInputLayout titleLayout =
(TextInputLayout) findViewById(R.id.titleLayout);
if (titleLayout != null) {
titleLayout.setCounterMaxLength(MAX_BLOG_TITLE_LENGTH);
}
titleInput = (TextInputEditText) findViewById(R.id.titleInput);
TextWatcher nameEntryWatcher = new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence text, int start,
int lengthBefore, int lengthAfter) {
enableOrDisableCreateButton();
}
};
titleInput.setOnEditorActionListener(this);
titleInput.addTextChangedListener(nameEntryWatcher);
TextInputLayout descLayout =
(TextInputLayout) findViewById(R.id.descLayout);
if (descLayout != null) {
descLayout.setCounterMaxLength(MAX_BLOG_DESC_LENGTH);
}
descInput = (TextInputEditText) findViewById(R.id.descInput);
if (descInput != null) {
descInput.addTextChangedListener(nameEntryWatcher);
}
button = (Button) findViewById(R.id.createBlogButton);
if (button != null) {
button.setOnClickListener(this);
}
progress = (ProgressBar) findViewById(R.id.createBlogProgressBar);
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
private void enableOrDisableCreateButton() {
if (progress == null) return; // Not created yet
button.setEnabled(validateTitle() && validateDescription());
}
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
descInput.requestFocus();
return true;
}
private boolean validateTitle() {
String name = titleInput.getText().toString();
int length = StringUtils.toUtf8(name).length;
return length <= MAX_BLOG_TITLE_LENGTH && length > 0;
}
private boolean validateDescription() {
String name = descInput.getText().toString();
int length = StringUtils.toUtf8(name).length;
return length <= MAX_BLOG_DESC_LENGTH && length > 0;
}
@Override
public void onClick(View view) {
if (view == button) {
hideSoftKeyboard(view);
if (!validateTitle()) return;
button.setVisibility(GONE);
progress.setVisibility(VISIBLE);
addBlog(titleInput.getText().toString(),
descInput.getText().toString());
}
}
private void addBlog(final String title, final String description) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
Collection<LocalAuthor> authors =
identityManager.getLocalAuthors();
// take first identity, don't support more for now
LocalAuthor author = authors.iterator().next();
Blog f = blogManager.addBlog(author, title, description);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Storing blog took " + duration + " ms");
displayBlog(f);
} catch (DbException e) {
// TODO show error, e.g. blog with same title exists
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
finishOnUiThread();
}
}
});
}
private void displayBlog(final Blog b) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Intent i =
new Intent(CreateBlogActivity.this, BlogActivity.class);
i.putExtra(GROUP_ID, b.getId().getBytes());
i.putExtra(BLOG_NAME, b.getName());
i.putExtra(IS_MY_BLOG, true);
i.putExtra(IS_NEW_BLOG, true);
ActivityOptionsCompat options =
makeCustomAnimation(CreateBlogActivity.this,
android.R.anim.fade_in,
android.R.anim.fade_out);
ActivityCompat.startActivity(CreateBlogActivity.this, i,
options.toBundle());
supportFinishAfterTransition();
}
});
}
}

View File

@@ -0,0 +1,25 @@
package org.briarproject.android.blogs;
import org.briarproject.android.controller.ActivityLifecycleController;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.api.blogs.Blog;
import java.util.Collection;
public interface FeedController {
void onResume();
void onPause();
void loadPosts(
final UiResultHandler<Collection<BlogPostItem>> resultHandler);
void loadPersonalBlog(final UiResultHandler<Blog> resultHandler);
void setOnBlogPostAddedListener(OnBlogPostAddedListener listener);
interface OnBlogPostAddedListener {
void onBlogPostAdded(final BlogPostItem post);
}
}

View File

@@ -0,0 +1,133 @@
package org.briarproject.android.blogs;
import org.briarproject.android.controller.DbControllerImpl;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.BlogPostAddedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.IdentityManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
public class FeedControllerImpl extends DbControllerImpl
implements FeedController, EventListener {
private static final Logger LOG =
Logger.getLogger(FeedControllerImpl.class.getName());
@Inject
protected volatile BlogManager blogManager;
@Inject
protected volatile IdentityManager identityManager;
@Inject
protected volatile EventBus eventBus;
private volatile OnBlogPostAddedListener listener;
@Inject
FeedControllerImpl() {
}
public void onResume() {
eventBus.addListener(this);
}
public void onPause() {
eventBus.removeListener(this);
}
@Override
public void eventOccurred(Event e) {
if (!(e instanceof BlogPostAddedEvent)) return;
LOG.info("New blog post added");
if (listener != null) {
final BlogPostAddedEvent m = (BlogPostAddedEvent) e;
final BlogPostHeader header = m.getHeader();
try {
final byte[] body = blogManager.getPostBody(header.getId());
final BlogPostItem post = new BlogPostItem(header, body);
listener.onBlogPostAdded(post);
} catch (DbException ex) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, ex.toString(), ex);
}
}
}
@Override
public void loadPosts(
final UiResultHandler<Collection<BlogPostItem>> resultHandler) {
LOG.info("Loading blog posts...");
runOnDbThread(new Runnable() {
@Override
public void run() {
Collection<BlogPostItem> posts = new ArrayList<>();
try {
// load blog posts
long now = System.currentTimeMillis();
for (Blog b : blogManager.getBlogs()) {
Collection<BlogPostHeader> header =
blogManager.getPostHeaders(b.getId());
for (BlogPostHeader h : header) {
byte[] body = blogManager.getPostBody(h.getId());
posts.add(new BlogPostItem(h, body));
}
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading posts took " + duration + " ms");
resultHandler.onResult(posts);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(null);
}
}
});
}
@Override
public void loadPersonalBlog(final UiResultHandler<Blog> resultHandler) {
LOG.info("Loading personal blog...");
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
// load blog posts
long now = System.currentTimeMillis();
Author a =
identityManager.getLocalAuthors().iterator().next();
Blog b = blogManager.getPersonalBlog(a);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading pers. blog took " + duration + " ms");
resultHandler.onResult(b);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(null);
}
}
});
}
@Override
public void setOnBlogPostAddedListener(OnBlogPostAddedListener listener) {
this.listener = listener;
}
}

View File

@@ -0,0 +1,203 @@
package org.briarproject.android.blogs;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityOptionsCompat;
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.View.OnClickListener;
import android.view.ViewGroup;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.blogs.Blog;
import java.util.Collection;
import javax.inject.Inject;
import static android.app.Activity.RESULT_OK;
import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
import static org.briarproject.android.BriarActivity.GROUP_ID;
import static org.briarproject.android.blogs.BlogActivity.BLOG_NAME;
import static org.briarproject.android.blogs.BlogActivity.REQUEST_WRITE_POST;
public class FeedFragment extends BaseFragment implements
OnBlogPostClickListener, FeedController.OnBlogPostAddedListener {
public final static String TAG = FeedFragment.class.getName();
@Inject
FeedController feedController;
private BlogPostAdapter adapter;
private LinearLayoutManager layoutManager;
private BriarRecyclerView list;
private Blog personalBlog = null;
static FeedFragment newInstance() {
FeedFragment f = new FeedFragment();
Bundle args = new Bundle();
f.setArguments(args);
return f;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
setHasOptionsMenu(true);
View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter = new BlogPostAdapter(getActivity(), this);
layoutManager = new LinearLayoutManager(getActivity());
list = (BriarRecyclerView) v.findViewById(R.id.postList);
list.setLayoutManager(layoutManager);
list.setAdapter(adapter);
list.setEmptyText(R.string.blogs_feed_empty_state);
return v;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
feedController.setOnBlogPostAddedListener(this);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// The BlogPostAddedEvent arrives when the controller is not listening
if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) {
showSnackBar(R.string.blogs_blog_post_created);
}
}
@Override
public void onStart() {
super.onStart();
feedController
.loadPersonalBlog(new UiResultHandler<Blog>(getActivity()) {
@Override
public void onResultUi(Blog b) {
personalBlog = b;
}
});
}
@Override
public void onResume() {
super.onResume();
feedController.onResume();
feedController.loadPosts(
new UiResultHandler<Collection<BlogPostItem>>(getActivity()) {
@Override
public void onResultUi(Collection<BlogPostItem> posts) {
if (posts == null) {
// TODO show error?
} else if (posts.isEmpty()) {
list.showData();
} else {
adapter.addAll(posts);
}
}
});
}
@Override
public void onPause() {
super.onPause();
feedController.onPause();
// TODO save list position in database/preferences?
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.blogs_feed_actions, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_write_blog_post:
if (personalBlog == null) return false;
Intent i =
new Intent(getActivity(), WriteBlogPostActivity.class);
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
i.putExtra(BLOG_NAME, personalBlog.getName());
ActivityOptionsCompat options =
makeCustomAnimation(getActivity(),
android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
startActivityForResult(i, REQUEST_WRITE_POST,
options.toBundle());
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onBlogPostAdded(final BlogPostItem post) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
adapter.add(post);
showSnackBar(R.string.blogs_blog_post_received);
}
});
}
@Override
public void onBlogPostClick(int position) {
// noop
}
@Override
public String getUniqueTag() {
return TAG;
}
private void showSnackBar(int stringRes) {
int firstVisible =
layoutManager.findFirstCompletelyVisibleItemPosition();
int lastVisible = layoutManager.findLastCompletelyVisibleItemPosition();
int count = adapter.getItemCount();
boolean scroll = count > (lastVisible - firstVisible + 1);
Snackbar s = Snackbar.make(list, stringRes, LENGTH_LONG);
s.getView().setBackgroundResource(R.color.briar_primary);
if (scroll) {
OnClickListener onClick = new OnClickListener() {
@Override
public void onClick(View v) {
list.smoothScrollToPosition(0);
}
};
s.setActionTextColor(ContextCompat
.getColor(getContext(),
R.color.briar_button_positive));
s.setAction(R.string.blogs_blog_post_scroll_to, onClick);
}
s.show();
}
}

View File

@@ -1,49 +1,113 @@
package org.briarproject.android.blogs;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
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.TextView;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchGroupException;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
public class MyBlogsFragment extends BaseFragment {
public final static String TAG = MyBlogsFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
private BriarRecyclerView list;
private BlogListAdapter adapter;
// Fields that are accessed from background threads must be volatile
@Inject
protected volatile IdentityManager identityManager;
@Inject
volatile BlogManager blogManager;
@Inject
public MyBlogsFragment() {
}
static MyBlogsFragment newInstance(int num) {
MyBlogsFragment f = new MyBlogsFragment();
Bundle args = new Bundle();
args.putInt("num", num);
f.setArguments(args);
return f;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_blogs_my, container,
false);
setHasOptionsMenu(true);
TextView numView = (TextView) v.findViewById(R.id.num);
String num = String.valueOf(getArguments().getInt("num"));
numView.setText(num);
adapter = new BlogListAdapter(getActivity());
return v;
list = (BriarRecyclerView) inflater
.inflate(R.layout.fragment_blogs_my, container, false);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
list.setEmptyText(getString(R.string.blogs_my_blogs_empty_state));
return list;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
listener.getActivityComponent().inject(this);
// Starting from here, we can use injected objects
}
@Override
public void onResume() {
super.onResume();
adapter.clear();
loadBlogs();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.blogs_my_actions, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
// Handle presses on the action bar items
switch (item.getItemId()) {
case R.id.action_create_blog:
Intent intent =
new Intent(getContext(), CreateBlogActivity.class);
ActivityOptionsCompat options =
makeCustomAnimation(getActivity(),
android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
ActivityCompat.startActivity(getActivity(), intent,
options.toBundle());
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
@@ -56,4 +120,49 @@ public class MyBlogsFragment extends BaseFragment {
component.inject(this);
}
private void loadBlogs() {
listener.runOnDbThread(new Runnable() {
@Override
public void run() {
try {
// load blogs
long now = System.currentTimeMillis();
Collection<BlogListItem> blogs = new ArrayList<>();
Collection<LocalAuthor> authors =
identityManager.getLocalAuthors();
LocalAuthor a = authors.iterator().next();
for (Blog b : blogManager.getBlogs(a)) {
try {
Collection<BlogPostHeader> headers =
blogManager.getPostHeaders(b.getId());
blogs.add(new BlogListItem(b, headers, true));
} catch (NoSuchGroupException e) {
// Continue
}
}
displayBlogs(blogs);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Full blog load took " + duration + " ms");
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayBlogs(final Collection<BlogListItem> items) {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
if (items.size() == 0) {
list.showData();
} else {
adapter.addAll(items);
}
}
});
}
}

View File

@@ -0,0 +1,200 @@
package org.briarproject.android.blogs;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.BriarActivity;
import org.briarproject.api.FormatException;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogPost;
import org.briarproject.api.blogs.BlogPostFactory;
import org.briarproject.api.db.DbException;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId;
import org.briarproject.util.StringUtils;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static java.util.logging.Level.WARNING;
import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_BODY_LENGTH;
import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_TITLE_LENGTH;
public class WriteBlogPostActivity extends BriarActivity
implements OnEditorActionListener {
private static final Logger LOG =
Logger.getLogger(WriteBlogPostActivity.class.getName());
private static final String contentType = "text/plain";
private TextInputEditText titleInput;
private EditText bodyInput;
private Button publishButton;
private ProgressBar progressBar;
// Fields that are accessed from background threads must be volatile
private volatile GroupId groupId;
@Inject
protected volatile IdentityManager identityManager;
@Inject
volatile BlogPostFactory blogPostFactory;
@Inject
volatile BlogManager blogManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No Group in intent.");
groupId = new GroupId(b);
// String blogName = i.getStringExtra(BLOG_NAME);
// if (blogName != null) setTitle(blogName);
setContentView(R.layout.activity_write_blog_post);
// String title =
// getTitle() + ": " + getString(R.string.blogs_write_blog_post);
// setTitle(title);
TextInputLayout titleLayout =
(TextInputLayout) findViewById(R.id.titleLayout);
if (titleLayout != null) {
titleLayout.setCounterMaxLength(MAX_BLOG_POST_TITLE_LENGTH);
}
titleInput = (TextInputEditText) findViewById(R.id.titleInput);
if (titleInput != null) {
titleInput.setOnEditorActionListener(this);
}
bodyInput = (EditText) findViewById(R.id.bodyInput);
bodyInput.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
}
@Override
public void afterTextChanged(Editable s) {
enableOrDisablePublishButton();
}
});
publishButton = (Button) findViewById(R.id.publishButton);
publishButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
publish();
}
});
progressBar = (ProgressBar) findViewById(R.id.progressBar);
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
bodyInput.requestFocus();
return true;
}
private void enableOrDisablePublishButton() {
int bodyLength =
StringUtils.toUtf8(bodyInput.getText().toString()).length;
if (bodyLength > 0 && bodyLength <= MAX_BLOG_POST_BODY_LENGTH &&
titleInput.getText().length() <= MAX_BLOG_POST_TITLE_LENGTH)
publishButton.setEnabled(true);
else
publishButton.setEnabled(false);
}
private void publish() {
// title
String title = titleInput.getText().toString();
if (title.length() > MAX_BLOG_POST_TITLE_LENGTH) return;
if (title.length() == 0) title = null;
// body
byte[] body = StringUtils.toUtf8(bodyInput.getText().toString());
// hide publish button, show progress bar
publishButton.setVisibility(GONE);
progressBar.setVisibility(VISIBLE);
storePost(title, body);
}
private void storePost(final String title, final byte[] body) {
runOnDbThread(new Runnable() {
@Override
public void run() {
long now = System.currentTimeMillis();
try {
Collection<LocalAuthor> authors =
identityManager.getLocalAuthors();
LocalAuthor author = authors.iterator().next();
BlogPost p = blogPostFactory
.createBlogPost(groupId, title, now, null, author,
contentType, body);
blogManager.addLocalPost(p);
postPublished();
} catch (DbException | GeneralSecurityException | FormatException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
postFailedToPublish();
}
}
});
}
private void postPublished() {
runOnUiThread(new Runnable() {
@Override
public void run() {
setResult(RESULT_OK);
supportFinishAfterTransition();
}
});
}
private void postFailedToPublish() {
runOnUiThread(new Runnable() {
@Override
public void run() {
// hide progress bar, show publish button
progressBar.setVisibility(GONE);
publishButton.setVisibility(VISIBLE);
// TODO show error
}
});
}
}

View File

@@ -104,16 +104,17 @@ class ForumListAdapter extends
// Post Count
int postCount = item.getPostCount();
if (postCount > 0) {
ui.unread.setText(ctx.getResources()
.getQuantityString(R.plurals.forum_posts, postCount,
ui.avatar.setProblem(false);
ui.postCount.setText(ctx.getResources()
.getQuantityString(R.plurals.posts, postCount,
postCount));
ui.unread.setTextColor(
ui.postCount.setTextColor(
ContextCompat
.getColor(ctx, R.color.briar_text_secondary));
} else {
ui.avatar.setProblem(true);
ui.unread.setText(ctx.getString(R.string.forum_no_posts));
ui.unread.setTextColor(
ui.postCount.setText(ctx.getString(R.string.no_posts));
ui.postCount.setTextColor(
ContextCompat
.getColor(ctx, R.color.briar_text_tertiary));
}
@@ -187,7 +188,7 @@ class ForumListAdapter extends
private final ViewGroup layout;
private final TextAvatarView avatar;
private final TextView name;
private final TextView unread;
private final TextView postCount;
private final TextView date;
ForumViewHolder(View v) {
@@ -196,7 +197,7 @@ class ForumListAdapter extends
layout = (ViewGroup) v;
avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
name = (TextView) v.findViewById(R.id.forumNameView);
unread = (TextView) v.findViewById(R.id.unreadView);
postCount = (TextView) v.findViewById(R.id.postCountView);
date = (TextView) v.findViewById(R.id.dateView);
}
}

View File

@@ -57,6 +57,11 @@ public class AndroidUtils {
til.setError(null);
}
public static void setError(TextInputLayout til, int res,
boolean condition) {
setError(til, til.getContext().getString(res), condition);
}
public static String getBluetoothAddress(Context ctx,
BluetoothAdapter adapter) {
// Return the adapter's address if it's valid and not fake

View File

@@ -130,6 +130,11 @@ public class BriarRecyclerView extends FrameLayout {
emptyView.setText(text);
}
public void setEmptyText(int res) {
if (recyclerView == null) initViews();
emptyView.setText(res);
}
public void showProgressBar() {
if (recyclerView == null) initViews();
recyclerView.setVisibility(INVISIBLE);
@@ -158,6 +163,11 @@ public class BriarRecyclerView extends FrameLayout {
recyclerView.scrollToPosition(position);
}
public void smoothScrollToPosition(int position) {
if (recyclerView == null) initViews();
recyclerView.smoothScrollToPosition(position);
}
public RecyclerView getRecyclerView() {
return this.recyclerView;
}

View File

@@ -38,7 +38,7 @@ public class TextAvatarView extends FrameLayout {
}
public void setText(String text) {
character.setText(text);
character.setText(text.toUpperCase());
}
public void setUnreadCount(int count) {

View File

@@ -8,6 +8,8 @@ import android.widget.ImageView;
import org.briarproject.R;
import org.briarproject.api.identity.Author.Status;
import static org.briarproject.api.identity.Author.Status.OURSELVES;
public class TrustIndicatorView extends ImageView {
public TrustIndicatorView(Context context) {
@@ -24,6 +26,11 @@ public class TrustIndicatorView extends ImageView {
}
public void setTrustLevel(Status status) {
if (status == OURSELVES) {
setVisibility(GONE);
return;
}
int res;
switch (status) {
case ANONYMOUS:
@@ -39,6 +46,7 @@ public class TrustIndicatorView extends ImageView {
res = R.drawable.trust_indicator_unknown;
}
setImageDrawable(ContextCompat.getDrawable(getContext(), res));
setVisibility(VISIBLE);
}
}

View File

@@ -5,7 +5,7 @@ import java.io.UnsupportedEncodingException;
/** A pseudonym for a user. */
public class Author {
public enum Status { ANONYMOUS, UNKNOWN, UNVERIFIED, VERIFIED }
public enum Status { ANONYMOUS, UNKNOWN, UNVERIFIED, VERIFIED, OURSELVES }
private final AuthorId id;
private final String name;

View File

@@ -17,6 +17,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import javax.inject.Inject;
import static org.briarproject.api.identity.Author.Status.OURSELVES;
import static org.briarproject.api.identity.Author.Status.UNKNOWN;
import static org.briarproject.api.identity.Author.Status.VERIFIED;
@@ -110,7 +111,7 @@ class IdentityManagerImpl implements IdentityManager {
throws DbException {
// Compare to the IDs of the user's identities
for (LocalAuthor a : db.getLocalAuthors(txn))
if (a.getId().equals(authorId)) return VERIFIED;
if (a.getId().equals(authorId)) return OURSELVES;
// Compare to the IDs of contacts' identities
for (Contact c : db.getContacts(txn))
if (c.getAuthor().getId().equals(authorId)) return VERIFIED;