diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index 03664f0efa100ae0cba4cdeaeadc960454b77007..702e7e6976c6f524859ff2561788bfb2b2ef643d 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -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" diff --git a/briar-android/res/drawable/ic_chat.xml b/briar-android/res/drawable/ic_chat.xml new file mode 100644 index 0000000000000000000000000000000000000000..04f9fdcdfd7427decad183a5e0a8ae3434f2b4f8 --- /dev/null +++ b/briar-android/res/drawable/ic_chat.xml @@ -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> diff --git a/briar-android/res/drawable/ic_repeat.xml b/briar-android/res/drawable/ic_repeat.xml new file mode 100644 index 0000000000000000000000000000000000000000..115dbda813aa64bde9f6c4a91ba0e92ef4ddda1b --- /dev/null +++ b/briar-android/res/drawable/ic_repeat.xml @@ -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> diff --git a/briar-android/res/drawable/trust_indicator_anonymous.xml b/briar-android/res/drawable/trust_indicator_anonymous.xml index 99ccadd1bea9fae5483b327fcfc48f89d574638f..82214b8d4fccc90ac9c85f520467d160cebdf546 100644 --- a/briar-android/res/drawable/trust_indicator_anonymous.xml +++ b/briar-android/res/drawable/trust_indicator_anonymous.xml @@ -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 diff --git a/briar-android/res/drawable/trust_indicator_unknown.xml b/briar-android/res/drawable/trust_indicator_unknown.xml index ec65b5a5528a0f636c76c6f990c0e35b91afbfbb..63e6ab7afaedca0935498be5f0847429846b665f 100644 --- a/briar-android/res/drawable/trust_indicator_unknown.xml +++ b/briar-android/res/drawable/trust_indicator_unknown.xml @@ -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 diff --git a/briar-android/res/drawable/trust_indicator_unverified.xml b/briar-android/res/drawable/trust_indicator_unverified.xml index 3697685b0666be83b532c311431426749aa993e7..97af93df956bd198b07ba367c960741a7cf7becc 100644 --- a/briar-android/res/drawable/trust_indicator_unverified.xml +++ b/briar-android/res/drawable/trust_indicator_unverified.xml @@ -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 diff --git a/briar-android/res/drawable/trust_indicator_verified.xml b/briar-android/res/drawable/trust_indicator_verified.xml index 40a8ff035f262a9294255d6279bae1a0de241240..5b37c223ad2a2cba22690643cf43d0ea90eb81cc 100644 --- a/briar-android/res/drawable/trust_indicator_verified.xml +++ b/briar-android/res/drawable/trust_indicator_verified.xml @@ -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 diff --git a/briar-android/res/layout/activity_blog.xml b/briar-android/res/layout/activity_blog.xml new file mode 100644 index 0000000000000000000000000000000000000000..3ff8d409e0e60ee2b749b2e51726b8af8e5cab7c --- /dev/null +++ b/briar-android/res/layout/activity_blog.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/activity_create_blog.xml b/briar-android/res/layout/activity_create_blog.xml new file mode 100644 index 0000000000000000000000000000000000000000..31a1c3df85e8983dc973c65a3529486432c3e86b --- /dev/null +++ b/briar-android/res/layout/activity_create_blog.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/activity_create_forum.xml b/briar-android/res/layout/activity_create_forum.xml index b48bce90fac407609c4b4ad898636fe057e43563..96afb47a99fdc2174122ccf0635077a185ef73e5 100644 --- a/briar-android/res/layout/activity_create_forum.xml +++ b/briar-android/res/layout/activity_create_forum.xml @@ -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> diff --git a/briar-android/res/layout/activity_create_identity.xml b/briar-android/res/layout/activity_create_identity.xml index 23309bfa83e7f539fcffad2c5340b0bc806707fe..5919662cfba59435b0dfb13028e77fa63c8a26cb 100644 --- a/briar-android/res/layout/activity_create_identity.xml +++ b/briar-android/res/layout/activity_create_identity.xml @@ -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"/> diff --git a/briar-android/res/layout/activity_write_blog_post.xml b/briar-android/res/layout/activity_write_blog_post.xml new file mode 100644 index 0000000000000000000000000000000000000000..5c3bcf3804982874598a6f63f4f28f47e9ae3607 --- /dev/null +++ b/briar-android/res/layout/activity_write_blog_post.xml @@ -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> diff --git a/briar-android/res/layout/fragment_blog.xml b/briar-android/res/layout/fragment_blog.xml new file mode 100644 index 0000000000000000000000000000000000000000..c0ac19fbe0eac0b76817a2c7bc9bb8566f6aa840 --- /dev/null +++ b/briar-android/res/layout/fragment_blog.xml @@ -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"/> diff --git a/briar-android/res/layout/fragment_blog_post.xml b/briar-android/res/layout/fragment_blog_post.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e5e73ed1c049c5aa805b6027b212557ef0b4995 --- /dev/null +++ b/briar-android/res/layout/fragment_blog_post.xml @@ -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> diff --git a/briar-android/res/layout/fragment_blogs_list.xml b/briar-android/res/layout/fragment_blogs_list.xml new file mode 100644 index 0000000000000000000000000000000000000000..a552dc0fce49e2a0f2c2ca14340ab4c38ae51fff --- /dev/null +++ b/briar-android/res/layout/fragment_blogs_list.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/fragment_blogs_my.xml b/briar-android/res/layout/fragment_blogs_my.xml index a552dc0fce49e2a0f2c2ca14340ab4c38ae51fff..288adfaa3937ea3d0b242b6c7177007b3e994d67 100644 --- a/briar-android/res/layout/fragment_blogs_my.xml +++ b/briar-android/res/layout/fragment_blogs_my.xml @@ -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> \ No newline at end of file + tools:listitem="@layout/list_item_blog"/> diff --git a/briar-android/res/layout/introduction_message.xml b/briar-android/res/layout/introduction_message.xml index a557059f14e3e5bac83394f247c3b8218ab17712..cf7e5a27e4fb5ebfa7999dd93e76bea4cf036702 100644 --- a/briar-android/res/layout/introduction_message.xml +++ b/briar-android/res/layout/introduction_message.xml @@ -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> diff --git a/briar-android/res/layout/list_item_blog.xml b/briar-android/res/layout/list_item_blog.xml new file mode 100644 index 0000000000000000000000000000000000000000..1fe1f22db024383b68baea541fbfea8e11eb88e3 --- /dev/null +++ b/briar-android/res/layout/list_item_blog.xml @@ -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> + diff --git a/briar-android/res/layout/list_item_blog_post.xml b/briar-android/res/layout/list_item_blog_post.xml new file mode 100644 index 0000000000000000000000000000000000000000..0f7e1d53294356d48b0c7b3fd96164fee6e64b82 --- /dev/null +++ b/briar-android/res/layout/list_item_blog_post.xml @@ -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> + diff --git a/briar-android/res/layout/list_item_forum.xml b/briar-android/res/layout/list_item_forum.xml index 171a40859c67555455afbe89f7e902882c39ac6b..0652bec6b01927253e4bbe35986de7a1ace3cb24 100644 --- a/briar-android/res/layout/list_item_forum.xml +++ b/briar-android/res/layout/list_item_forum.xml @@ -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> diff --git a/briar-android/res/layout/share_forum_message.xml b/briar-android/res/layout/share_forum_message.xml index 522a0472d75bda5ea46a11b6e56ce7253d102e68..33863fc13d0a3c9d1f5f08aea1c9a8ac058a50b3 100644 --- a/briar-android/res/layout/share_forum_message.xml +++ b/briar-android/res/layout/share_forum_message.xml @@ -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> diff --git a/briar-android/res/menu/blogs_feed_actions.xml b/briar-android/res/menu/blogs_feed_actions.xml new file mode 100644 index 0000000000000000000000000000000000000000..38721c812dd0b37c75a0dd5770bf12e89a187d35 --- /dev/null +++ b/briar-android/res/menu/blogs_feed_actions.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/menu/blogs_my_actions.xml b/briar-android/res/menu/blogs_my_actions.xml new file mode 100644 index 0000000000000000000000000000000000000000..9a3ce5b49a27da3c2484e66fe542ca375b669b2b --- /dev/null +++ b/briar-android/res/menu/blogs_my_actions.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/menu/blogs_my_blog_actions.xml b/briar-android/res/menu/blogs_my_blog_actions.xml new file mode 100644 index 0000000000000000000000000000000000000000..95cb880e767e1eb826d1df231d74cb0ec25800c0 --- /dev/null +++ b/briar-android/res/menu/blogs_my_blog_actions.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/menu/blogs_write_blog_post_actions.xml b/briar-android/res/menu/blogs_write_blog_post_actions.xml new file mode 100644 index 0000000000000000000000000000000000000000..11befe03696ed3c17e2200a9c2928b62e8ba31ca --- /dev/null +++ b/briar-android/res/menu/blogs_write_blog_post_actions.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml index 0a0fc5ce53d27e573c35226632070410fca9b305..8aa43b6693613f2104a9bbdf90f539478d47897b 100644 --- a/briar-android/res/values/dimens.xml +++ b/briar-android/res/values/dimens.xml @@ -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> diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index d44bc389a6872b6a3c538fa3e9efc5ff7d251c4b..4bef054ae141d9afbebc79a88938f564ce6084d3 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -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> diff --git a/briar-android/res/values/styles.xml b/briar-android/res/values/styles.xml index 9251475dad32cb3016d210ed13969b3174e934b1..729554b99cb0f4f152b95dfecb85be8ac2f69a36 100644 --- a/briar-android/res/values/styles.xml +++ b/briar-android/res/values/styles.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/values/themes.xml b/briar-android/res/values/themes.xml index e87077f031f9ec1e2db96c41eba58cd7f88ff95c..49c05904f621ef1462ff5b771b7b73cf5dddc11d 100644 --- a/briar-android/res/values/themes.xml +++ b/briar-android/res/values/themes.xml @@ -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 --> diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java index 3353f62b44e7fd29024f72b03a96502d773cc077..3f3368b575b7bdfcbbf35964685864515a2fe6d9 100644 --- a/briar-android/src/org/briarproject/android/ActivityComponent.java +++ b/briar-android/src/org/briarproject/android/ActivityComponent.java @@ -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); diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java index ea984bca0bab42760c652869ea561f87c85e69ee..5627651aecb15d7f0ea76befa8c04d3c5b63302d 100644 --- a/briar-android/src/org/briarproject/android/ActivityModule.java +++ b/briar-android/src/org/briarproject/android/ActivityModule.java @@ -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( diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java index 5bc21c1cc27c7d79099c6e82b545b692e3d11f1a..3e59233be3dc1542bfa2b4867cb90197cc5c6e39 100644 --- a/briar-android/src/org/briarproject/android/AndroidComponent.java +++ b/briar-android/src/org/briarproject/android/AndroidComponent.java @@ -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(); diff --git a/briar-android/src/org/briarproject/android/AppModule.java b/briar-android/src/org/briarproject/android/AppModule.java index 36933a1184aec4c6df22703b391d15046b1c0750..825e639c6c69ab442b1d85d388b2769e68920733 100644 --- a/briar-android/src/org/briarproject/android/AppModule.java +++ b/briar-android/src/org/briarproject/android/AppModule.java @@ -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(); + } } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..35321f390a96ee09c356de2b294552cc812d9696 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java @@ -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; + } + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogController.java b/briar-android/src/org/briarproject/android/blogs/BlogController.java new file mode 100644 index 0000000000000000000000000000000000000000..a805c2d344e52c6295d60b90e7a79b563604583e --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogController.java @@ -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); + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..d2898602c68dd664a4709cd761667b7d4e01a1dd --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java @@ -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); + } + } + }); + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..7ee529ca3c8ae9a35e36b8145b9a6552f0b75e3f --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java @@ -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(); + } + }); + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..92526387f18948365615c9aa3aacc1c1e273f589 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java @@ -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); + } + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogListFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..61f3dff54a547415013d1dc473f3406b763f2021 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogListFragment.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListItem.java b/briar-android/src/org/briarproject/android/blogs/BlogListItem.java new file mode 100644 index 0000000000000000000000000000000000000000..35c9f532be2a239fecd798a629955afb7d548a95 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogListItem.java @@ -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; + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java b/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java new file mode 100644 index 0000000000000000000000000000000000000000..a2834c809cd5afc0643be76ccdb7d6d5b3de1025 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java @@ -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(); + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..dbcce29651b0b7531cd841a10300f92c09bf7b62 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java @@ -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); + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..54ac2cdda54898ec9f60815fb3cbb63b2e9ef85c --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java @@ -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); + } + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb9c15ef7950a1437944d60ceada841efbe0432 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java @@ -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; + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java index a736537ff9257943e90197255ef9b08e42079765..80f234010360227c985d37cfe9df029d80ef5e68 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java @@ -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 diff --git a/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java b/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..980d67d1da8c96f84882d3b94b6e1dda6a82d34b --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java @@ -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(); + } + }); + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/FeedController.java b/briar-android/src/org/briarproject/android/blogs/FeedController.java new file mode 100644 index 0000000000000000000000000000000000000000..ad98adeefed6788e8a9854921156ff4a1ee1de20 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/FeedController.java @@ -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); + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e22757f675d4843de4fdcd4c9196f0804d82b81 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java @@ -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; + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..bc7a1e457664384eeb5c5c76562c3981f9432f19 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java @@ -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(); + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java b/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java index 572bc516eb02192deee98f8b4b0d9a7965e120c5..1cc4ee8aceec054f5ca25ff4bbe934ceb9d021c1 100644 --- a/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java @@ -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(); + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + setHasOptionsMenu(true); + + adapter = new BlogListAdapter(getActivity()); - Bundle args = new Bundle(); - args.putInt("num", num); - f.setArguments(args); + 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 f; + return list; } - @Nullable @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + listener.getActivityComponent().inject(this); + // Starting from here, we can use injected objects + } - View v = inflater.inflate(R.layout.fragment_blogs_my, container, - false); + @Override + public void onResume() { + super.onResume(); + adapter.clear(); + loadBlogs(); + } - TextView numView = (TextView) v.findViewById(R.id.num); - String num = String.valueOf(getArguments().getInt("num")); - numView.setText(num); + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.blogs_my_actions, menu); + super.onCreateOptionsMenu(menu, inflater); + } - return v; + @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); + } + } + }); + } + } diff --git a/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java b/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..4d9cc61518b154b4e148e1f93c099b689920f9ff --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java @@ -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 + } + }); + } + +} diff --git a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java index 17fd01346b17fbedbcf58567a430987c9c77d209..88759804aefa31203ede1b5047da0ab2becd6e48 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java +++ b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java @@ -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); } } diff --git a/briar-android/src/org/briarproject/android/util/AndroidUtils.java b/briar-android/src/org/briarproject/android/util/AndroidUtils.java index 252d955749a925f725aacaf44432ef6d3ac78fe4..077dc8aa58f95d366a678bb18fffbd3f403047d7 100644 --- a/briar-android/src/org/briarproject/android/util/AndroidUtils.java +++ b/briar-android/src/org/briarproject/android/util/AndroidUtils.java @@ -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 diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java index 7563354b65a1e335961c3348f76e52d424705259..6cd36dd6b0e71c06c815d7f4f5d0cf44ebb7d2fd 100644 --- a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java +++ b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java @@ -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; } diff --git a/briar-android/src/org/briarproject/android/util/TextAvatarView.java b/briar-android/src/org/briarproject/android/util/TextAvatarView.java index cc4ab91485f01f58350ce663ddcbd81e39a0c502..6033021eb2699f4410799252dff8bea808e6ee5f 100644 --- a/briar-android/src/org/briarproject/android/util/TextAvatarView.java +++ b/briar-android/src/org/briarproject/android/util/TextAvatarView.java @@ -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) { diff --git a/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java b/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java index ce01bf301e34e1176d307e81e23ef80dd226fb62..a9ea3c8fb24653f07cbfc09252813550abad1253 100644 --- a/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java +++ b/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java @@ -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); } } diff --git a/briar-api/src/org/briarproject/api/identity/Author.java b/briar-api/src/org/briarproject/api/identity/Author.java index 9aa543b3acd09654cac3f81da23814350496336a..de15869d3ebcbdf646580e177128cffea130eac7 100644 --- a/briar-api/src/org/briarproject/api/identity/Author.java +++ b/briar-api/src/org/briarproject/api/identity/Author.java @@ -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; diff --git a/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java b/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java index 5b1434ad1e3644b6964bea20916d5508d4ee53dc..bd14c334076b222f0218b5783118e3a5cd915c11 100644 --- a/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java +++ b/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java @@ -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;