diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml index 0677e45dbda840b74b6441c24eba9f9e52693215..2feaa4e95fbea9bbb14af009cc80761f3beb8141 100644 --- a/.idea/codeStyleSettings.xml +++ b/.idea/codeStyleSettings.xml @@ -37,6 +37,34 @@ <JavaCodeStyleSettings> <option name="ANNOTATION_PARAMETER_WRAP" value="1" /> </JavaCodeStyleSettings> + <Objective-C-extensions> + <option name="GENERATE_INSTANCE_VARIABLES_FOR_PROPERTIES" value="ASK" /> + <option name="RELEASE_STYLE" value="IVAR" /> + <option name="TYPE_QUALIFIERS_PLACEMENT" value="BEFORE" /> + <file> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" /> + </file> + <class> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" /> + <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" /> + </class> + <extensions> + <pair source="cpp" header="h" /> + <pair source="c" header="h" /> + </extensions> + </Objective-C-extensions> <XML> <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" /> </XML> diff --git a/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java index e2893714dacc1bce8ab50b76684cfd9f2fd1f49d..3bfaae9a2a9ef99ad10490611f0a87dd0e7d81f4 100644 --- a/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java @@ -32,7 +32,6 @@ import org.briarproject.lifecycle.LifecycleModule; import org.briarproject.properties.PropertiesModule; import org.briarproject.sync.SyncModule; import org.briarproject.transport.TransportModule; -import org.briarproject.util.StringUtils; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -60,7 +59,6 @@ import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; import static org.briarproject.api.sync.ValidationManager.State.INVALID; import static org.briarproject.api.sync.ValidationManager.State.PENDING; import static org.briarproject.api.sync.ValidationManager.State.VALID; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -193,8 +191,7 @@ public class BlogManagerTest { assertEquals(1, headers0.size()); // check that body is there - assertArrayEquals(StringUtils.toUtf8(body), - blogManager0.getPostBody(p.getMessage().getId())); + assertEquals(body, blogManager0.getPostBody(p.getMessage().getId())); // make sure that blog0 at author1 doesn't have the post yet Collection<BlogPostHeader> headers1 = @@ -211,8 +208,7 @@ public class BlogManagerTest { assertEquals(POST, headers1.iterator().next().getType()); // check that body is there - assertArrayEquals(StringUtils.toUtf8(body), - blogManager1.getPostBody(p.getMessage().getId())); + assertEquals(body, blogManager1.getPostBody(p.getMessage().getId())); stopLifecycles(); } @@ -334,8 +330,7 @@ public class BlogManagerTest { assertEquals(author0, h.getParent().getAuthor()); // ensure that body can be retrieved from wrapped post - assertArrayEquals(StringUtils.toUtf8(body), - blogManager0.getPostBody(h.getParentId())); + assertEquals(body, blogManager0.getPostBody(h.getParentId())); // 1 has only their own comment in their blog headers1 = blogManager1.getPostHeaders(blog1.getId()); @@ -375,6 +370,13 @@ public class BlogManagerTest { Collection<BlogPostHeader> headers1 = blogManager1.getPostHeaders(blog0.getId()); assertEquals(2, headers1.size()); + for (BlogPostHeader h : headers1) { + if (h.getType() == POST) { + assertEquals(body, blogManager1.getPostBody(h.getId())); + } else { + assertEquals(comment, ((BlogCommentHeader)h).getComment()); + } + } stopLifecycles(); } diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index 0ce2865ecdb281bc5989c247b7befa515c45e40b..5da35cdf462510dd5206de26c2692a3ed516497d 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -211,6 +211,17 @@ /> </activity> + <activity + android:name=".android.blogs.ReblogActivity" + android:label="@string/blogs_reblog_button" + android:parentActivityName=".android.blogs.BlogActivity" + android:windowSoftInputMode="stateHidden"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".android.blogs.BlogActivity" + /> + </activity> + <activity android:name=".android.blogs.RssFeedImportActivity" android:label="@string/blogs_rss_feeds_import" diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 8e75abfe40a67660b43a696458d6bf3081be7bbd..adea13909d84b5fb0cb8f69d979fdd6e4315149c 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -27,6 +27,7 @@ dependencies { exclude module: 'support-v4' exclude module: 'recyclerview-v7' } + compile "com.android.support:cardview-v7:$supportVersion" compile('ch.acra:acra:4.8.5') { exclude module: 'support-v4' exclude module: 'support-annotations' @@ -61,6 +62,7 @@ dependencyVerification { 'com.android.support:animated-vector-drawable:06d1963b85aa917099d7757e6a7b3e4dc06889413dc747f625ae8683606db3a1', 'com.android.support:support-vector-drawable:799bafe4c3de812386f0b291f744d5d6876452722dd40189b9ab87dbbf594ea1', 'com.android.support:recyclerview-v7:44040a888e23e0c93162a3377cfe06751080e3c22d369ab0d4301ef60d63b0fe', + 'com.android.support:cardview-v7:4595f1c4a28cfa083b6c0920ad4d49e1c2ca4b8302a955e548f68eb63b74931b', ] } diff --git a/briar-android/res/drawable/bubble_white.xml b/briar-android/res/drawable/bubble_white.xml new file mode 100644 index 0000000000000000000000000000000000000000..21d0e8e15eb5e066cf275cc8ee087e98a5f34e74 --- /dev/null +++ b/briar-android/res/drawable/bubble_white.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + + <corners + android:radius="@dimen/unread_bubble_size"/> + + <solid + android:color="@color/briar_text_primary_inverse"/> + + <stroke + android:color="@color/briar_text_primary" + android:width="1dp"/> + +</shape> + diff --git a/briar-android/res/drawable/ic_our_identity_black.xml b/briar-android/res/drawable/ic_our_identity_black.xml new file mode 100644 index 0000000000000000000000000000000000000000..af997ec0c067ed1f944219241cf447136b0ce5e9 --- /dev/null +++ b/briar-android/res/drawable/ic_our_identity_black.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:alpha="0.54" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M12,5.9c1.16,0 2.1,0.94 2.1,2.1s-0.94,2.1 -2.1,2.1S9.9,9.16 9.9,8s0.94,-2.1 2.1,-2.1m0,9c2.97,0 6.1,1.46 6.1,2.1v1.1L5.9,18.1L5.9,17c0,-0.64 3.13,-2.1 6.1,-2.1M12,4C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,13c-2.67,0 -8,1.34 -8,4v3h16v-3c0,-2.66 -5.33,-4 -8,-4z"/> +</vector> diff --git a/briar-android/res/layout/activity_introduction.xml b/briar-android/res/layout/activity_fragment_container.xml similarity index 82% rename from briar-android/res/layout/activity_introduction.xml rename to briar-android/res/layout/activity_fragment_container.xml index f351897d0f7b281b42f63847603b676d7f5bf7d2..e6c20760fb5078de5def86eec38402fc2db9042a 100644 --- a/briar-android/res/layout/activity_introduction.xml +++ b/briar-android/res/layout/activity_fragment_container.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <FrameLayout - android:id="@+id/introductionContainer" + android:id="@+id/fragmentContainer" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"/> \ No newline at end of file diff --git a/briar-android/res/layout/author_view.xml b/briar-android/res/layout/author_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..405376fac9b46eeec2780eddbf3e44d9a356dc9c --- /dev/null +++ b/briar-android/res/layout/author_view.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + tools:showIn="@layout/list_item_blog_post"> + + <de.hdodenhof.circleimageview.CircleImageView + android:id="@+id/avatar" + style="@style/BriarAvatar" + android:layout_width="@dimen/blogs_avatar_normal_size" + android:layout_height="@dimen/blogs_avatar_normal_size" + android:layout_centerVertical="true" + android:layout_marginRight="@dimen/margin_medium" + tools:src="@drawable/ic_launcher"/> + + <ImageView + android:id="@+id/avatarIcon" + android:layout_width="@dimen/blogs_avatar_icon_size" + android:layout_height="@dimen/blogs_avatar_icon_size" + android:layout_alignBottom="@+id/avatar" + android:layout_alignRight="@+id/avatar" + android:background="@drawable/bubble_white" + android:padding="2dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_repeat" + android:visibility="invisible" + tools:ignore="ContentDescription"/> + + <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_small" + tools:text="Author Name"/> + + <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"/> + + <TextView + android:id="@+id/dateView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/authorName" + android:layout_toEndOf="@+id/avatar" + android:layout_toRightOf="@+id/avatar" + android:gravity="bottom" + android:textColor="@color/briar_text_secondary" + android:textSize="@dimen/text_size_tiny" + tools:text="yesterday"/> + +</merge> diff --git a/briar-android/res/layout/fragment_reblog.xml b/briar-android/res/layout/fragment_reblog.xml new file mode 100644 index 0000000000000000000000000000000000000000..b17143b35193564ad3b2c503e15f83f0d6bb0a9e --- /dev/null +++ b/briar-android/res/layout/fragment_reblog.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView + android:id="@+id/scrollView" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/window_background"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/margin_small"> + + <include + android:id="@+id/postLayout" + layout="@layout/list_item_blog_post" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + <ProgressBar + android:id="@+id/progressBar" + style="?android:attr/progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true"/> + + <EditText + android:id="@+id/inputText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/postLayout" + android:layout_margin="@dimen/listitem_vertical_margin" + android:gravity="bottom" + android:hint="@string/blogs_reblog_comment_hint" + android:inputType="textShortMessage|textMultiLine|textCapSentences|textAutoCorrect"/> + + <Button + android:id="@+id/publishButton" + style="@style/BriarButton" + android:layout_below="@+id/inputText" + android:enabled="false" + android:text="@string/blogs_reblog_button"/> + + </RelativeLayout> + +</ScrollView> diff --git a/briar-android/res/layout/list_item_blog_comment.xml b/briar-android/res/layout/list_item_blog_comment.xml new file mode 100644 index 0000000000000000000000000000000000000000..bdbe42842d003db9c92fc6e1a8515366da1957ff --- /dev/null +++ b/briar-android/res/layout/list_item_blog_comment.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:showIn="@layout/list_item_blog_post"> + + <View + android:id="@+id/inputDivider" + style="@style/Divider.Horizontal"/> + + <org.briarproject.android.util.AuthorView + android:id="@+id/authorView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:padding="@dimen/listitem_vertical_margin" + app:persona="commenter"/> + + <TextView + android:id="@+id/bodyView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/authorView" + android:paddingBottom="@dimen/listitem_vertical_margin" + android:paddingLeft="@dimen/listitem_vertical_margin" + android:paddingRight="@dimen/listitem_vertical_margin" + android:textColor="@color/briar_text_secondary" + android:textIsSelectable="true" + android:textSize="@dimen/text_size_small" + tools:text="This is a comment that appears below a blog post. Usually, it is expected to be rather short. Not much longer than this one."/> + +</RelativeLayout> diff --git a/briar-android/res/layout/list_item_blog_post.xml b/briar-android/res/layout/list_item_blog_post.xml index 0f7e1d53294356d48b0c7b3fd96164fee6e64b82..4697c461bf8e22f8fdb1832662d565ab7294309f 100644 --- a/briar-android/res/layout/list_item_blog_post.xml +++ b/briar-android/res/layout/list_item_blog_post.xml @@ -1,118 +1,78 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<android.support.v7.widget.CardView + style="@style/BriarCard" xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginLeft="@dimen/listitem_horizontal_margin" - android:layout_marginStart="@dimen/listitem_horizontal_margin" - android:layout_marginTop="@dimen/listitem_vertical_margin" - android:background="?attr/selectableItemBackground"> + android:layout_height="wrap_content"> - <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" + <LinearLayout + android:layout_width="match_parent" 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"/> + android:orientation="vertical"> - <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"/> + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/listitem_vertical_margin"> - <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.AuthorView + android:id="@+id/rebloggerView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:layout_marginBottom="@dimen/listitem_horizontal_margin" + android:layout_toLeftOf="@+id/commentView" + app:persona="reblogger"/> - <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"/> + <org.briarproject.android.util.AuthorView + android:id="@+id/authorView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_below="@+id/rebloggerView" + android:layout_marginBottom="@dimen/listitem_vertical_margin" + android:layout_toLeftOf="@+id/commentView"/> - <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_alignParentTop="true" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:contentDescription="@string/blogs_reblog_comment_hint" + android:padding="@dimen/margin_small" + android:src="@drawable/ic_repeat"/> - <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/bodyView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/authorView" + android:textColor="@color/briar_text_secondary" + android:textIsSelectable="true" + android:textSize="@dimen/text_size_medium" + tools:text="This is a body text that shows the content of a blog post.\n\nThis one is not short, but it is also not too long."/> - <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"/> + </RelativeLayout> - <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."/> + <LinearLayout + android:id="@+id/commentContainer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <include + layout="@layout/list_item_blog_comment" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> - <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"/> + </LinearLayout> -</RelativeLayout> + </LinearLayout> +</android.support.v7.widget.CardView> diff --git a/briar-android/res/menu/blogs_blog_actions.xml b/briar-android/res/menu/blogs_blog_actions.xml index 3661a49dffb91a655b61845b9deec7daf8afe860..64b42ac867a44016de994bb5508ab065f4b33de2 100644 --- a/briar-android/res/menu/blogs_blog_actions.xml +++ b/briar-android/res/menu/blogs_blog_actions.xml @@ -1,7 +1,16 @@ <?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"> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <item + android:id="@+id/action_write_blog_post" + android:icon="@drawable/forum_item_create_white" + android:title="@string/blogs_write_blog_post" + android:visible="false" + app:showAsAction="ifRoom" + tools:visible="true"/> <item android:id="@+id/action_blog_share" @@ -18,6 +27,8 @@ android:id="@+id/action_blog_delete" android:icon="@drawable/action_delete_white" android:title="@string/blogs_remove_blog" - app:showAsAction="never"/> + android:visible="false" + app:showAsAction="never" + tools:visible="true"/> </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 deleted file mode 100644 index 5c9052edb01da64626f8a04b6e59f8728a37bf1f..0000000000000000000000000000000000000000 --- a/briar-android/res/menu/blogs_my_blog_actions.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?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"/> - -</menu> \ No newline at end of file diff --git a/briar-android/res/values/attrs.xml b/briar-android/res/values/attrs.xml index 0de786b4b557f43344bfbd48cb5a30f3180aaded..a09b80b32c17ba83878d2ae6e79a7ad6e33cb695 100644 --- a/briar-android/res/values/attrs.xml +++ b/briar-android/res/values/attrs.xml @@ -1,8 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <resources> + <declare-styleable name="BriarRecyclerView"> <attr name="scrollToEnd" format="boolean" /> <attr name="emptyText" format="string" /> </declare-styleable> + <declare-styleable name="AuthorView"> + <attr name="persona" format="enum"> + <enum name="normal" value="0"/> + <enum name="reblogger" value="1"/> + <enum name="commenter" value="2"/> + </attr> + </declare-styleable> + </resources> \ No newline at end of file diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml index 7c676e61aee591c01dc4a36dee1c189dd59247b1..9f62ff0944d8491f42245eeecb7b4a858524a423 100644 --- a/briar-android/res/values/dimens.xml +++ b/briar-android/res/values/dimens.xml @@ -43,4 +43,8 @@ <dimen name="forum_nested_indicator">24dp</dimen> <dimen name="forum_avatar_size">20dp</dimen> + <dimen name="blogs_avatar_normal_size">30dp</dimen> + <dimen name="blogs_avatar_icon_size">15dp</dimen> + <dimen name="blogs_avatar_comment_size">20dp</dimen> + </resources> diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index dc399cd7e12823ff4435c1278e3e4ac4b10bede0..ba764c356d3880c5a16081cb1d98e02dd48466dd 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -35,7 +35,7 @@ <string name="nav_drawer_close_description">Close the navigation drawer</string> <string name="contact_list_button">Contacts</string> <string name="forums_button">Forums</string> - <string name="blogs_button">Micro Blogs</string> + <string name="blogs_button">Blogs</string> <string name="settings_button">Settings</string> <string name="sign_out_button">Sign Out</string> @@ -207,7 +207,6 @@ <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> @@ -227,6 +226,8 @@ <string name="blogs_remove_blog_dialog_message">Are you sure that you want to remove this blog and all posts?\nNote that this will not remove the blog from other people\'s devices.</string> <string name="blogs_remove_blog_ok">Remove Blog</string> <string name="blogs_blog_removed">Blog Removed</string> + <string name="blogs_reblog_comment_hint">Add an optional comment</string> + <string name="blogs_reblog_button">Reblog</string> <string name="blogs_blog_list">Blog List</string> <string name="blogs_available_blogs">Available Blogs</string> diff --git a/briar-android/res/values/styles.xml b/briar-android/res/values/styles.xml index 98751d04c188c20f0686342a83c3a594bbdd8bc0..76c9c0a1f45ee25969ab480473235f6056cbef80 100644 --- a/briar-android/res/values/styles.xml +++ b/briar-android/res/values/styles.xml @@ -124,6 +124,11 @@ <item name="tabTextColor">@color/briar_text_primary_inverse</item> </style> + <style name="BriarCard" parent="CardView"> + <item name="cardUseCompatPadding">true</item> + <item name="android:layout_margin">@dimen/margin_small</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> diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java index 3e491d25b1682cab98db282b947efc34925183c6..2a0d82c4b26811aec01e7b8656e1aa0e169e6dd3 100644 --- a/briar-android/src/org/briarproject/android/ActivityComponent.java +++ b/briar-android/src/org/briarproject/android/ActivityComponent.java @@ -6,10 +6,10 @@ 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.ReblogActivity; +import org.briarproject.android.blogs.ReblogFragment; import org.briarproject.android.blogs.RssFeedImportActivity; import org.briarproject.android.blogs.RssFeedManageActivity; import org.briarproject.android.blogs.WriteBlogPostActivity; @@ -93,6 +93,10 @@ public interface ActivityComponent { void inject(BlogPostFragment fragment); + void inject(ReblogFragment fragment); + + void inject(ReblogActivity activity); + void inject(SettingsActivity activity); void inject(ChangePasswordActivity activity); @@ -106,10 +110,8 @@ public interface ActivityComponent { // Fragments void inject(ContactListFragment fragment); void inject(ForumListFragment fragment); - void inject(BlogsFragment fragment); void inject(BlogListFragment fragment); void inject(FeedFragment fragment); - void inject(MyBlogsFragment fragment); void inject(IntroFragment fragment); void inject(ShowQrCodeFragment fragment); void inject(ContactChooserFragment fragment); diff --git a/briar-android/src/org/briarproject/android/BriarFragmentActivity.java b/briar-android/src/org/briarproject/android/BriarFragmentActivity.java index 0a9f22a2e4b93da6f1816da5e332b40690fbf15e..513ace7b560c5b038f7839e1240fcf2ed690a3ca 100644 --- a/briar-android/src/org/briarproject/android/BriarFragmentActivity.java +++ b/briar-android/src/org/briarproject/android/BriarFragmentActivity.java @@ -7,7 +7,7 @@ import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import org.briarproject.R; -import org.briarproject.android.blogs.BlogsFragment; +import org.briarproject.android.blogs.FeedFragment; import org.briarproject.android.contact.ContactListFragment; import org.briarproject.android.forum.ForumListFragment; import org.briarproject.android.fragment.BaseFragment; @@ -27,7 +27,7 @@ public abstract class BriarFragmentActivity extends BriarActivity { actionBar.setTitle(R.string.contact_list_button); } else if (fragmentTag.equals(ForumListFragment.TAG)) { actionBar.setTitle(R.string.forums_button); - } else if (fragmentTag.equals(BlogsFragment.TAG)) { + } else if (fragmentTag.equals(FeedFragment.TAG)) { actionBar.setTitle(R.string.blogs_button); } } diff --git a/briar-android/src/org/briarproject/android/NavDrawerActivity.java b/briar-android/src/org/briarproject/android/NavDrawerActivity.java index 8de76247adbcc85f138f1f2a37dbb544a1efb75c..6381bc9cd1491eefb9b94cde41b0bfca989e12b4 100644 --- a/briar-android/src/org/briarproject/android/NavDrawerActivity.java +++ b/briar-android/src/org/briarproject/android/NavDrawerActivity.java @@ -21,7 +21,7 @@ import android.widget.ImageView; import android.widget.TextView; import org.briarproject.R; -import org.briarproject.android.blogs.BlogsFragment; +import org.briarproject.android.blogs.FeedFragment; import org.briarproject.android.contact.ContactListFragment; import org.briarproject.android.controller.NavDrawerController; import org.briarproject.android.controller.TransportStateListener; @@ -82,7 +82,7 @@ public class NavDrawerActivity extends BriarFragmentActivity implements startFragment(ContactListFragment.newInstance()); } else if (intent.getBooleanExtra(INTENT_BLOGS, false)) { - startFragment(BlogsFragment.newInstance()); + startFragment(FeedFragment.newInstance()); } setIntent(null); } @@ -186,7 +186,7 @@ public class NavDrawerActivity extends BriarFragmentActivity implements startFragment(ForumListFragment.newInstance()); break; case R.id.nav_btn_blogs: - startFragment(BlogsFragment.newInstance()); + startFragment(FeedFragment.newInstance()); break; case R.id.nav_btn_settings: startActivity(new Intent(this, SettingsActivity.class)); diff --git a/briar-android/src/org/briarproject/android/blogs/BaseController.java b/briar-android/src/org/briarproject/android/blogs/BaseController.java new file mode 100644 index 0000000000000000000000000000000000000000..a7cf1e4243eebaa076e59f7ecedc373b5cf2251c --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BaseController.java @@ -0,0 +1,41 @@ +package org.briarproject.android.blogs; + +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; + +import org.briarproject.android.controller.handler.ResultExceptionHandler; +import org.briarproject.api.blogs.BlogPostHeader; +import org.briarproject.api.db.DbException; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; + +import java.util.Collection; + +public interface BaseController { + + @UiThread + void onStart(); + + @UiThread + void onStop(); + + void loadBlogPosts(GroupId g, + ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler); + + void loadBlogPost(BlogPostHeader header, + ResultExceptionHandler<BlogPostItem, DbException> handler); + + void loadBlogPost(GroupId g, MessageId m, + ResultExceptionHandler<BlogPostItem, DbException> handler); + + void repeatPost(BlogPostItem item, @Nullable String comment, + ResultExceptionHandler<Void, DbException> resultHandler); + + void setOnBlogPostAddedListener(OnBlogPostAddedListener listener); + + interface OnBlogPostAddedListener { + @UiThread + void onBlogPostAdded(BlogPostHeader header, boolean local); + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BaseControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/BaseControllerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..ac5ce7b72ddecac43b34c57e988a966ee5a1ed3c --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BaseControllerImpl.java @@ -0,0 +1,250 @@ +package org.briarproject.android.blogs; + +import android.app.Activity; +import android.support.annotation.CallSuper; +import android.support.annotation.Nullable; + +import org.briarproject.android.api.AndroidNotificationManager; +import org.briarproject.android.controller.DbControllerImpl; +import org.briarproject.android.controller.handler.ResultExceptionHandler; +import org.briarproject.api.blogs.Blog; +import org.briarproject.api.blogs.BlogCommentHeader; +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.IdentityManager; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; + +abstract class BaseControllerImpl extends DbControllerImpl + implements BaseController, EventListener { + + private static final Logger LOG = + Logger.getLogger(BaseControllerImpl.class.getName()); + + @Inject + protected Activity activity; + @Inject + protected EventBus eventBus; + @Inject + protected AndroidNotificationManager notificationManager; + @Inject + protected IdentityManager identityManager; + + @Inject + protected volatile BlogManager blogManager; + + private final Map<MessageId, String> bodyCache = new ConcurrentHashMap<>(); + private final Map<MessageId, BlogPostHeader> headerCache = + new ConcurrentHashMap<>(); + + protected volatile OnBlogPostAddedListener listener; + + @Override + @CallSuper + public void onStart() { + eventBus.addListener(this); + } + + @Override + @CallSuper + public void onStop() { + eventBus.removeListener(this); + } + + @Override + @CallSuper + public void eventOccurred(Event e) { + if (e instanceof BlogPostAddedEvent) { + final BlogPostAddedEvent m = (BlogPostAddedEvent) e; + LOG.info("New blog post added"); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + listener.onBlogPostAdded(m.getHeader(), m.isLocal()); + } + }); + } + } + + @Override + public void setOnBlogPostAddedListener(OnBlogPostAddedListener listener) { + if (this.listener != null) + throw new IllegalStateException("Listener was already set"); + this.listener = listener; + } + + @Override + public void loadBlogPosts(final GroupId groupId, + final ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) { + if (groupId == null) throw new IllegalStateException(); + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + Collection<BlogPostItem> items = loadItems(groupId); + handler.onResult(items); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + } + } + }); + } + + protected Collection<BlogPostItem> loadItems(GroupId groupId) + throws DbException { + long now = System.currentTimeMillis(); + Collection<BlogPostHeader> headers = + blogManager.getPostHeaders(groupId); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading headers took " + duration + " ms"); + Collection<BlogPostItem> items = new ArrayList<>(headers.size()); + now = System.currentTimeMillis(); + for (BlogPostHeader h : headers) { + headerCache.put(h.getId(), h); + BlogPostItem item = getItem(h); + items.add(item); + } + duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading bodies took " + duration + " ms"); + return items; + } + + @Override + public void loadBlogPost(final BlogPostHeader header, + final ResultExceptionHandler<BlogPostItem, DbException> handler) { + + String body = bodyCache.get(header.getId()); + if (body != null) { + LOG.info("Loaded body from cache"); + handler.onResult(new BlogPostItem(header, body)); + return; + } + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + long now = System.currentTimeMillis(); + BlogPostItem item = getItem(header); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading body took " + duration + " ms"); + handler.onResult(item); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + } + } + }); + } + + @Override + public void loadBlogPost(final GroupId g, final MessageId m, + final ResultExceptionHandler<BlogPostItem, DbException> handler) { + + BlogPostHeader header = headerCache.get(m); + if (header != null) { + LOG.info("Loaded header from cache"); + loadBlogPost(header, handler); + return; + } + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + long now = System.currentTimeMillis(); + BlogPostHeader header = getPostHeader(g, m); + BlogPostItem item = getItem(header); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading post took " + duration + " ms"); + handler.onResult(item); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + } + } + }); + } + + @Override + public void repeatPost(final BlogPostItem item, + final @Nullable String comment, + final ResultExceptionHandler<Void, DbException> handler) { + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + LocalAuthor a = identityManager.getLocalAuthor(); + Blog b = blogManager.getPersonalBlog(a); + BlogPostHeader h = item.getHeader(); + blogManager.addLocalComment(a, b.getId(), comment, h); + handler.onResult(null); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + } + } + }); + } + + private BlogPostHeader getPostHeader(GroupId g, MessageId m) + throws DbException { + + if (g == null) throw new IllegalStateException(); + BlogPostHeader header = headerCache.get(m); + if (header == null) { + header = blogManager.getPostHeader(g, m); + headerCache.put(m, header); + } + return header; + } + + private BlogPostItem getItem(BlogPostHeader h) throws DbException { + String body; + if (h instanceof BlogCommentHeader) { + BlogCommentHeader c = (BlogCommentHeader) h; + BlogCommentItem item = new BlogCommentItem(c); + body = getPostBody(item.getPostHeader().getId()); + item.setBody(body); + return item; + } else { + body = getPostBody(h.getId()); + return new BlogPostItem(h, body); + } + } + + private String getPostBody(MessageId m) throws DbException { + String body = bodyCache.get(m); + if (body == null) { + body = blogManager.getPostBody(m); + if (body != null) bodyCache.put(m, body); + } + //noinspection ConstantConditions + return body; + } + +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java index 7c3309010e1c85ccfabdcedab718e80637d61816..249d0ff7cf0fc741b7fad3bff73c02c6d908c7d0 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java @@ -14,7 +14,7 @@ import android.widget.ProgressBar; 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.BaseController.OnBlogPostAddedListener; import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; import org.briarproject.android.controller.handler.UiResultExceptionHandler; import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener; @@ -33,16 +33,16 @@ import javax.inject.Inject; import static android.view.View.GONE; import static android.view.View.VISIBLE; -public class BlogActivity extends BriarActivity implements BlogPostListener, +public class BlogActivity extends BriarActivity implements + OnBlogPostAddedListener, OnBlogPostClickListener, BaseFragmentListener { static final int REQUEST_WRITE_POST = 1; static final int REQUEST_SHARE = 2; - static final String BLOG_NAME = "briar.BLOG_NAME"; - static final String IS_MY_BLOG = "briar.IS_MY_BLOG"; + public static final String BLOG_NAME = "briar.BLOG_NAME"; static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG"; - private static final String POST_ID = "briar.POST_ID"; + public static final String POST_ID = "briar.POST_ID"; private GroupId groupId; private ProgressBar progressBar; @@ -50,7 +50,7 @@ public class BlogActivity extends BriarActivity implements BlogPostListener, private BlogPagerAdapter blogPagerAdapter; private BlogPostPagerAdapter postPagerAdapter; private String blogName; - private boolean myBlog, isNew; + private boolean isNew; private MessageId savedPostId; @Inject @@ -67,12 +67,11 @@ public class BlogActivity extends BriarActivity implements BlogPostListener, groupId = new GroupId(b); blogController.setGroupId(groupId); - // Name of the Blog from Intent + // Name of the blog 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); + // Was this blog just created? isNew = i.getBooleanExtra(IS_NEW_BLOG, false); setContentView(R.layout.activity_blog); @@ -254,7 +253,7 @@ public class BlogActivity extends BriarActivity implements BlogPostListener, @Override public Fragment getItem(int position) { - return BlogFragment.newInstance(groupId, blogName, myBlog, isNew); + return BlogFragment.newInstance(groupId, blogName, isNew); } @Override diff --git a/briar-android/src/org/briarproject/android/blogs/BlogCommentItem.java b/briar-android/src/org/briarproject/android/blogs/BlogCommentItem.java new file mode 100644 index 0000000000000000000000000000000000000000..b3dea0b8c96a5faf7753d96ceec98efb47c69b25 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogCommentItem.java @@ -0,0 +1,65 @@ +package org.briarproject.android.blogs; + +import android.support.annotation.UiThread; + +import org.briarproject.api.blogs.BlogCommentHeader; +import org.briarproject.api.blogs.BlogPostHeader; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +@UiThread +class BlogCommentItem extends BlogPostItem { + + private static final BlogCommentComparator COMPARATOR = + new BlogCommentComparator(); + + private final BlogPostHeader postHeader; + private final List<BlogCommentHeader> comments = new ArrayList<>(); + + BlogCommentItem(BlogCommentHeader header) { + super(header, null); + postHeader = collectComments(header); + Collections.sort(comments, COMPARATOR); + } + + private BlogPostHeader collectComments(BlogPostHeader header) { + if (header instanceof BlogCommentHeader) { + BlogCommentHeader comment = (BlogCommentHeader) header; + if (comment.getComment() != null) + comments.add(comment); + return collectComments(comment.getParent()); + } else { + return header; + } + } + + public void setBody(String body) { + this.body = body; + } + + @Override + public BlogCommentHeader getHeader() { + return (BlogCommentHeader) super.getHeader(); + } + + @Override + BlogPostHeader getPostHeader() { + return postHeader; + } + + List<BlogCommentHeader> getComments() { + return comments; + } + + private static class BlogCommentComparator + implements Comparator<BlogCommentHeader> { + @Override + public int compare(BlogCommentHeader h1, BlogCommentHeader h2) { + // re-use same comparator used for blog posts, but reverse it + return BlogPostItem.compare(h2, h1); + } + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogController.java b/briar-android/src/org/briarproject/android/blogs/BlogController.java index 90fbb61cf1ae4ab2428674fadca842608a92da97..768d29c14cb059bebe4618c93fa7c1c6156ae03e 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogController.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogController.java @@ -11,26 +11,20 @@ import org.briarproject.api.sync.MessageId; import java.util.Collection; -public interface BlogController extends ActivityLifecycleController { +public interface BlogController extends BaseController { void setGroupId(GroupId g); void loadBlogPosts( ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler); - void loadBlogPost(BlogPostHeader header, - ResultExceptionHandler<BlogPostItem, DbException> handler); - void loadBlogPost(MessageId m, ResultExceptionHandler<BlogPostItem, DbException> handler); + void isMyBlog(ResultExceptionHandler<Boolean, DbException> handler); + void canDeleteBlog(ResultExceptionHandler<Boolean, DbException> handler); void deleteBlog(ResultExceptionHandler<Void, DbException> handler); - interface BlogPostListener { - @UiThread - void onBlogPostAdded(BlogPostHeader header, boolean local); - } - } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java index 9906e511e80952f4a9c1352b405d38e7613e3b76..d70779e34ea17b04f149c318601f736fe6a3597e 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java @@ -1,107 +1,76 @@ package org.briarproject.android.blogs; -import android.app.Activity; - -import org.briarproject.android.api.AndroidNotificationManager; -import org.briarproject.android.controller.DbControllerImpl; +import org.briarproject.android.controller.ActivityLifecycleController; import org.briarproject.android.controller.handler.ResultExceptionHandler; 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.identity.LocalAuthor; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; 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 { +public class BlogControllerImpl extends BaseControllerImpl + implements ActivityLifecycleController, BlogController, EventListener { private static final Logger LOG = Logger.getLogger(BlogControllerImpl.class.getName()); - @Inject - protected Activity activity; - @Inject - protected EventBus eventBus; - @Inject - protected AndroidNotificationManager notificationManager; - - @Inject - protected volatile BlogManager blogManager; - - private final Map<MessageId, byte[]> bodyCache = new ConcurrentHashMap<>(); - private final Map<MessageId, BlogPostHeader> headerCache = - new ConcurrentHashMap<>(); - - private volatile BlogPostListener listener; private volatile GroupId groupId = null; @Inject BlogControllerImpl() { } - @Override - public void setGroupId(GroupId g) { - groupId = g; - } - @Override public void onActivityCreate() { - if (activity instanceof BlogPostListener) { - listener = (BlogPostListener) activity; + if (activity instanceof OnBlogPostAddedListener) { + listener = (OnBlogPostAddedListener) activity; } else { throw new IllegalStateException( "An activity that injects the BlogController must " + - "implement the BlogPostListener"); + "implement the OnBlogPostAddedListener"); } } @Override public void onActivityResume() { + super.onStart(); // TODO: Should be called when activity starts. #609 notificationManager.blockNotification(groupId); notificationManager.clearBlogPostNotification(groupId); - eventBus.addListener(this); } @Override public void onActivityPause() { + super.onStop(); // TODO: Should be called when activity stops. #609 notificationManager.unblockNotification(groupId); - eventBus.removeListener(this); } @Override public void onActivityDestroy() { } + @Override + public void setGroupId(GroupId g) { + groupId = g; + } + @Override public void eventOccurred(Event e) { if (groupId == null) throw new IllegalStateException(); if (e instanceof BlogPostAddedEvent) { - final BlogPostAddedEvent m = (BlogPostAddedEvent) e; - if (m.getGroupId().equals(groupId)) { - LOG.info("New blog post added"); - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - listener.onBlogPostAdded(m.getHeader(), m.isLocal()); - } - }); + BlogPostAddedEvent s = (BlogPostAddedEvent) e; + if (s.getGroupId().equals(groupId)) { + super.eventOccurred(e); } } else if (e instanceof GroupRemovedEvent) { GroupRemovedEvent s = (GroupRemovedEvent) e; @@ -122,86 +91,27 @@ public class BlogControllerImpl extends DbControllerImpl public void loadBlogPosts( final ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) { if (groupId == null) throw new IllegalStateException(); - runOnDbThread(new Runnable() { - @Override - public void run() { - try { - long now = System.currentTimeMillis(); - Collection<BlogPostHeader> headers = - blogManager.getPostHeaders(groupId); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading headers took " + duration + " ms"); - List<BlogPostItem> items = new ArrayList<>(headers.size()); - now = System.currentTimeMillis(); - for (BlogPostHeader h : headers) { - headerCache.put(h.getId(), h); - byte[] body = getPostBody(h.getId()); - items.add(new BlogPostItem(groupId, h, body)); - } - duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading bodies took " + duration + " ms"); - handler.onResult(items); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - handler.onException(e); - } - } - }); + loadBlogPosts(groupId, handler); } @Override - public void loadBlogPost(final BlogPostHeader header, + public void loadBlogPost(final MessageId m, final ResultExceptionHandler<BlogPostItem, DbException> handler) { if (groupId == null) throw new IllegalStateException(); - byte[] body = bodyCache.get(header.getId()); - if (body != null) { - LOG.info("Loaded body from cache"); - handler.onResult(new BlogPostItem(groupId, header, body)); - return; - } - runOnDbThread(new Runnable() { - @Override - public void run() { - try { - long now = System.currentTimeMillis(); - byte[] body = getPostBody(header.getId()); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading body took " + duration + " ms"); - handler.onResult(new BlogPostItem(groupId, header, body)); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - handler.onException(e); - } - } - }); + loadBlogPost(groupId, m, handler); } @Override - public void loadBlogPost(final MessageId m, - final ResultExceptionHandler<BlogPostItem, DbException> handler) { + public void isMyBlog( + final ResultExceptionHandler<Boolean, DbException> handler) { if (groupId == null) throw new IllegalStateException(); - BlogPostHeader header = headerCache.get(m); - if (header != null) { - LOG.info("Loaded header from cache"); - loadBlogPost(header, handler); - return; - } runOnDbThread(new Runnable() { @Override public void run() { try { - long now = System.currentTimeMillis(); - BlogPostHeader header = getPostHeader(m); - byte[] body = getPostBody(m); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading post took " + duration + " ms"); - handler.onResult(new BlogPostItem(groupId, header, body)); + LocalAuthor a = identityManager.getLocalAuthor(); + Blog b = blogManager.getBlog(groupId); + handler.onResult(b.getAuthor().getId().equals(a.getId())); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -209,25 +119,7 @@ public class BlogControllerImpl extends DbControllerImpl } } }); - } - private BlogPostHeader getPostHeader(MessageId m) throws DbException { - if (groupId == null) throw new IllegalStateException(); - BlogPostHeader header = headerCache.get(m); - if (header == null) { - header = blogManager.getPostHeader(groupId, m); - headerCache.put(m, header); - } - return header; - } - - private byte[] getPostBody(MessageId m) throws DbException { - byte[] body = bodyCache.get(m); - if (body == null) { - body = blogManager.getPostBody(m); - if (body != null) bodyCache.put(m, body); - } - return body; } @Override diff --git a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java index 0e89542af0459ebd9e35411353adf62a39edde18..b3b87a113c3481a7e7eae63457ad6d3bf0ed5e17 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java @@ -19,7 +19,7 @@ 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.BaseController.OnBlogPostAddedListener; import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; import org.briarproject.android.controller.handler.UiResultExceptionHandler; import org.briarproject.android.fragment.BaseFragment; @@ -42,12 +42,12 @@ 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_SHARE; import static org.briarproject.android.blogs.BlogActivity.REQUEST_WRITE_POST; -public class BlogFragment extends BaseFragment implements BlogPostListener { +public class BlogFragment extends BaseFragment implements + OnBlogPostAddedListener { public final static String TAG = BlogFragment.class.getName(); @@ -56,20 +56,18 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { private GroupId groupId; private String blogName; - private boolean myBlog; private BlogPostAdapter adapter; private BriarRecyclerView list; - private MenuItem deleteButton; + private MenuItem writeButton, deleteButton; static BlogFragment newInstance(GroupId groupId, String name, - boolean myBlog, boolean isNew) { + 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); @@ -88,7 +86,6 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { if (b == null) throw new IllegalStateException("No group ID in args"); 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); @@ -99,12 +96,7 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setAdapter(adapter); list.showProgressBar(); - if (myBlog) { - list.setEmptyText( - getString(R.string.blogs_my_blogs_blog_empty_state)); - } else { - list.setEmptyText(getString(R.string.blogs_other_blog_empty_state)); - } + list.setEmptyText(getString(R.string.blogs_other_blog_empty_state)); // show snackbar if this blog was just created if (isNew) { @@ -128,7 +120,8 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { @Override public void onStart() { super.onStart(); - if (!myBlog) checkIfBlogCanBeDeleted(); + checkIfThisIsMyBlog(); + checkIfBlogCanBeDeleted(); } @Override @@ -146,13 +139,10 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (myBlog) { - inflater.inflate(R.menu.blogs_my_blog_actions, menu); - } else { - inflater.inflate(R.menu.blogs_blog_actions, menu); - deleteButton = menu.findItem(R.id.action_blog_delete); - deleteButton.setVisible(false); - } + inflater.inflate(R.menu.blogs_blog_actions, menu); + writeButton = menu.findItem(R.id.action_write_blog_post); + deleteButton = menu.findItem(R.id.action_blog_delete); + super.onCreateOptionsMenu(menu, inflater); } @@ -199,7 +189,9 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { public void onActivityResult(int request, int result, Intent data) { super.onActivityResult(request, result, data); - if (request == REQUEST_SHARE && result == RESULT_OK) { + if (request == REQUEST_WRITE_POST && result == RESULT_OK) { + displaySnackbar(R.string.blogs_blog_post_created); + } else if (request == REQUEST_SHARE && result == RESULT_OK) { displaySnackbar(R.string.blogs_sharing_snackbar); } } @@ -217,7 +209,12 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { @Override public void onResultUi(BlogPostItem post) { adapter.add(post); - if (local) list.scrollToPosition(0); + if (local) { + list.scrollToPosition(0); + displaySnackbar(R.string.blogs_blog_post_created); + } else { + displaySnackbar(R.string.blogs_blog_post_received); + } } @Override @@ -251,6 +248,25 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { }); } + private void checkIfThisIsMyBlog() { + blogController.canDeleteBlog( + new UiResultExceptionHandler<Boolean, DbException>( + getActivity()) { + @Override + public void onResultUi(Boolean isMyBlog) { + if (isMyBlog) { + showWriteButton(); + } + } + + @Override + public void onExceptionUi(DbException exception) { + // TODO: Decide how to handle errors in the UI + getActivity().finish(); + } + }); + } + private void checkIfBlogCanBeDeleted() { blogController.canDeleteBlog( new UiResultExceptionHandler<Boolean, DbException>( @@ -270,6 +286,11 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { }); } + private void showWriteButton() { + if (writeButton != null) + writeButton.setVisible(true); + } + private void showDeleteButton() { if (deleteButton != null) deleteButton.setVisible(true); diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java index 92f9430c42a1c7365f0add736b811114891c43ca..37f7dd30167d58160b40b35aa53922ea4543b219 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java @@ -1,7 +1,6 @@ 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; @@ -9,7 +8,6 @@ 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; @@ -23,12 +21,10 @@ 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> { @@ -136,7 +132,6 @@ class BlogListAdapter extends 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); diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java index 44d971c83f0d23d5dcdaacb22835fba44cc89b02..badf034503d5d99fc57e0a89860d8abe99b9a5f5 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java @@ -6,25 +6,15 @@ import android.support.v7.widget.RecyclerView; 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.AndroidUtils; -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> { +class BlogPostAdapter extends RecyclerView.Adapter<BlogPostViewHolder> { private SortedList<BlogPostItem> posts = new SortedList<>( BlogPostItem.class, new SortedList.Callback<BlogPostItem>() { - @Override public int compare(BlogPostItem a, BlogPostItem b) { return a.compareTo(b); @@ -60,7 +50,6 @@ class BlogPostAdapter extends return a.getId().equals(b.getId()); } }); - private final Context ctx; private final OnBlogPostClickListener listener; @@ -70,34 +59,18 @@ class BlogPostAdapter extends } @Override - public BlogPostHolder onCreateViewHolder(ViewGroup parent, int viewType) { + public BlogPostViewHolder onCreateViewHolder(ViewGroup parent, + int viewType) { View v = LayoutInflater.from(ctx).inflate( R.layout.list_item_blog_post, parent, false); - return new BlogPostHolder(v); + BlogPostViewHolder ui = new BlogPostViewHolder(v); + ui.setOnBlogPostClickListener(listener); + return ui; } @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(AndroidUtils.formatDate(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(post); - } - }); + public void onBindViewHolder(BlogPostViewHolder ui, int position) { + ui.bindItem(getItem(position)); } @Override @@ -129,27 +102,6 @@ class BlogPostAdapter extends 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 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); - body = (TextView) v.findViewById(R.id.bodyView); - } - } - interface OnBlogPostClickListener { void onBlogPostClick(BlogPostItem post); } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java index f96deda0b2e9ecaeed0d79d3b9cea3adc0d2f3a8..7e0cb1c95a29fd18aba6f72c76dc9b5a0cf82f3b 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java @@ -19,7 +19,6 @@ import org.briarproject.android.util.TrustIndicatorView; import org.briarproject.api.db.DbException; import org.briarproject.api.identity.Author; import org.briarproject.api.sync.MessageId; -import org.briarproject.util.StringUtils; import java.util.logging.Logger; @@ -27,7 +26,6 @@ import javax.inject.Inject; import im.delight.android.identicons.IdenticonDrawable; -import static android.view.View.GONE; import static org.briarproject.android.util.AndroidUtils.MIN_RESOLUTION; public class BlogPostFragment extends BaseFragment { @@ -135,11 +133,7 @@ public class BlogPostFragment extends BaseFragment { if (ctx != null) { ui.date.setText(AndroidUtils.formatDate(ctx, post.getTimestamp())); } - - // TODO remove #598 - ui.title.setVisibility(GONE); - - ui.body.setText(StringUtils.fromUtf8(post.getBody())); + ui.body.setText(post.getBody()); } private static class BlogPostViewHolder { @@ -148,7 +142,6 @@ public class BlogPostFragment extends BaseFragment { private final TextView authorName; private final TrustIndicatorView trust; private final TextView date; - private final TextView title; private final TextView body; private BlogPostViewHolder(View v) { @@ -156,7 +149,6 @@ public class BlogPostFragment extends BaseFragment { 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 index 55a439e2edfb148f28f8d44556f4f27c5188fea3..747b661593e1d0cb12f6efd2dc73a247c08dcff1 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostItem.java @@ -2,6 +2,7 @@ package org.briarproject.android.blogs; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.UiThread; import org.briarproject.api.blogs.BlogPostHeader; import org.briarproject.api.identity.Author; @@ -9,19 +10,17 @@ import org.briarproject.api.identity.Author.Status; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; -// This class is not thread-safe +@UiThread class BlogPostItem implements Comparable<BlogPostItem> { - private final GroupId groupId; private final BlogPostHeader header; - private final byte[] body; + protected String body; private boolean read; - BlogPostItem(GroupId groupId, BlogPostHeader header, byte[] body) { - this.groupId = groupId; + BlogPostItem(BlogPostHeader header, @Nullable String body) { this.header = header; this.body = body; - read = header.isRead(); + this.read = header.isRead(); } public MessageId getId() { @@ -29,17 +28,13 @@ class BlogPostItem implements Comparable<BlogPostItem> { } public GroupId getGroupId() { - return groupId; + return header.getGroupId(); } public long getTimestamp() { return header.getTimestamp(); } - public long getTimeReceived() { - return header.getTimeReceived(); - } - public Author getAuthor() { return header.getAuthor(); } @@ -48,7 +43,7 @@ class BlogPostItem implements Comparable<BlogPostItem> { return header.getAuthorStatus(); } - public byte[] getBody() { + public String getBody() { return body; } @@ -56,15 +51,23 @@ class BlogPostItem implements Comparable<BlogPostItem> { return read; } - public void setRead(boolean read) { - this.read = read; + public BlogPostHeader getHeader() { + return header; + } + + BlogPostHeader getPostHeader() { + return getHeader(); } @Override public int compareTo(@NonNull BlogPostItem other) { if (this == other) return 0; + return compare(getHeader(), other.getHeader()); + } + + protected static int compare(BlogPostHeader h1, BlogPostHeader h2) { // The newest post comes first - long aTime = getTimeReceived(), bTime = other.getTimeReceived(); + long aTime = h1.getTimeReceived(), bTime = h2.getTimeReceived(); if (aTime > bTime) return -1; if (aTime < bTime) return 1; return 0; diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java b/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..378b3993b7afe3545e6e5312e7b9527aef6b24b6 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java @@ -0,0 +1,157 @@ +package org.briarproject.android.blogs; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.UiThread; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.RecyclerView; +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.blogs.BlogPostAdapter.OnBlogPostClickListener; +import org.briarproject.android.util.AuthorView; +import org.briarproject.api.blogs.BlogCommentHeader; +import org.briarproject.api.blogs.BlogPostHeader; +import org.briarproject.api.identity.Author; +import org.briarproject.api.sync.MessageId; + +import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation; +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.POST_ID; +import static org.briarproject.api.blogs.MessageType.POST; + +@UiThread +class BlogPostViewHolder extends RecyclerView.ViewHolder { + + private final Context ctx; + private final ViewGroup layout; + private final AuthorView reblogger; + private final AuthorView author; + private final ImageView reblogButton; + private final TextView body; + private final ViewGroup commentContainer; + + private OnBlogPostClickListener listener; + + BlogPostViewHolder(View v) { + super(v); + + ctx = v.getContext(); + layout = (ViewGroup) v; + reblogger = (AuthorView) v.findViewById(R.id.rebloggerView); + author = (AuthorView) v.findViewById(R.id.authorView); + reblogButton = (ImageView) v.findViewById(R.id.commentView); + body = (TextView) v.findViewById(R.id.bodyView); + commentContainer = + (ViewGroup) v.findViewById(R.id.commentContainer); + } + + void setOnBlogPostClickListener(OnBlogPostClickListener listener) { + this.listener = listener; + } + + void setVisibility(int visibility) { + layout.setVisibility(visibility); + } + + void hideReblogButton() { + reblogButton.setVisibility(GONE); + } + + void setTransitionName(MessageId id) { + ViewCompat.setTransitionName(layout, getTransitionName(id)); + } + + private String getTransitionName(MessageId id) { + return "blogPost" + id.hashCode(); + } + + void bindItem(final BlogPostItem item) { + setTransitionName(item.getId()); + layout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onBlogPostClick(item); + } + } + }); + + // author and date + BlogPostHeader post = item.getPostHeader(); + Author a = post.getAuthor(); + author.setAuthor(a); + author.setAuthorStatus(post.getAuthorStatus()); + author.setDate(post.getTimestamp()); + // TODO make author clickable more often #624 + if (item.getHeader().getType() == POST) { + author.setBlogLink(post.getGroupId()); + } else { + author.unsetBlogLink(); + } + + // post body + body.setText(item.getBody()); + + // reblog button + reblogButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent i = new Intent(ctx, ReblogActivity.class); + i.putExtra(GROUP_ID, item.getGroupId().getBytes()); + i.putExtra(POST_ID, item.getId().getBytes()); + + ActivityOptionsCompat options = + makeSceneTransitionAnimation((Activity) ctx, layout, + getTransitionName(item.getId())); + ActivityCompat + .startActivity((Activity) ctx, i, options.toBundle()); + } + }); + + // comments + commentContainer.removeAllViews(); + if (item instanceof BlogCommentItem) { + onBindComment((BlogCommentItem) item); + } else { + reblogger.setVisibility(GONE); + } + } + + private void onBindComment(final BlogCommentItem item) { + // reblogger + reblogger.setAuthor(item.getAuthor()); + reblogger.setAuthorStatus(item.getAuthorStatus()); + reblogger.setDate(item.getTimestamp()); + reblogger.setBlogLink(item.getGroupId()); + reblogger.setVisibility(VISIBLE); + + // comments + for (BlogCommentHeader c : item.getComments()) { + View v = LayoutInflater.from(ctx) + .inflate(R.layout.list_item_blog_comment, + commentContainer, false); + + AuthorView author = (AuthorView) v.findViewById(R.id.authorView); + TextView body = (TextView) v.findViewById(R.id.bodyView); + + author.setAuthor(c.getAuthor()); + author.setAuthorStatus(c.getAuthorStatus()); + author.setDate(c.getTimestamp()); + // TODO make author clickable #624 + + body.setText(c.getComment()); + + commentContainer.addView(v); + } + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java deleted file mode 100644 index 80f234010360227c985d37cfe9df029d80ef5e68..0000000000000000000000000000000000000000 --- a/briar-android/src/org/briarproject/android/blogs/BlogsFragment.java +++ /dev/null @@ -1,118 +0,0 @@ -package org.briarproject.android.blogs; - -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TabLayout; -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.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -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(); - - private static final String SELECTED_TAB = "selectedTab"; - private TabLayout tabLayout; - - public static BlogsFragment newInstance() { - - Bundle args = new Bundle(); - - BlogsFragment fragment = new BlogsFragment(); - fragment.setArguments(args); - return fragment; - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - - View v = inflater.inflate(R.layout.fragment_blogs, container, - false); - - tabLayout = (TabLayout) v.findViewById(R.id.tabLayout); - ViewPager viewPager = (ViewPager) v.findViewById(R.id.pager); - - String[] titles = { - getString(R.string.blogs_feed), - getString(R.string.blogs_my_blogs), - getString(R.string.blogs_blog_list), - getString(R.string.blogs_available_blogs), - getString(R.string.blogs_drafts) - }; - TabAdapter tabAdapter = - new TabAdapter(getChildFragmentManager(), titles); - viewPager.setAdapter(tabAdapter); - tabLayout.setupWithViewPager(viewPager); - - tabLayout.setVisibility(GONE); - - if (savedInstanceState != null) { - int position = savedInstanceState.getInt(SELECTED_TAB, 0); - viewPager.setCurrentItem(position); - } - return v; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt(SELECTED_TAB, tabLayout.getSelectedTabPosition()); - } - - @Override - public String getUniqueTag() { - return TAG; - } - - @Override - public void injectFragment(ActivityComponent component) { - component.inject(this); - } - - - private static class TabAdapter extends FragmentStatePagerAdapter { - private String[] titles; - - TabAdapter(FragmentManager fm, String[] titles) { - super(fm); - this.titles = titles; - } - - @Override - public int getCount() { - return 1; -// return titles.length; - } - - @Override - public Fragment getItem(int position) { - return FeedFragment.newInstance(); -// switch (position) { -// case 0: -// return FeedFragment.newInstance(); -// case 1: -// return new MyBlogsFragment(); -// default: -// return BlogListFragment.newInstance(position); -// } - } - - @Override - public CharSequence getPageTitle(int position) { - return titles[position]; - } - } - -} diff --git a/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java b/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java index 980d67d1da8c96f84882d3b94b6e1dda6a82d34b..5bac574b5cf4dbd64ba64ffbb90f8baa91bb57e7 100644 --- a/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java +++ b/briar-android/src/org/briarproject/android/blogs/CreateBlogActivity.java @@ -15,7 +15,6 @@ 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; @@ -35,11 +34,9 @@ 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; @@ -180,7 +177,6 @@ public class CreateBlogActivity extends BriarActivity 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, diff --git a/briar-android/src/org/briarproject/android/blogs/FeedController.java b/briar-android/src/org/briarproject/android/blogs/FeedController.java index d449eb60be5f8f53c53549650fbfa11e3fa51f8b..e2934f49fd67757128d0dc521f020d86953fcb4a 100644 --- a/briar-android/src/org/briarproject/android/blogs/FeedController.java +++ b/briar-android/src/org/briarproject/android/blogs/FeedController.java @@ -1,24 +1,17 @@ package org.briarproject.android.blogs; +import org.briarproject.android.controller.handler.ResultExceptionHandler; import org.briarproject.android.controller.handler.ResultHandler; import org.briarproject.api.blogs.Blog; +import org.briarproject.api.db.DbException; import java.util.Collection; -public interface FeedController { +public interface FeedController extends BaseController { - void onResume(); - - void onPause(); - - void loadPosts(ResultHandler<Collection<BlogPostItem>> resultHandler); + void loadBlogPosts( + ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler); void loadPersonalBlog(ResultHandler<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 index 7c093598b156da61c6d04d6bca7758f7c1b88161..50074c0a7adf24bd36053d1eb7bbeeef3002b828 100644 --- a/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java +++ b/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java @@ -1,19 +1,12 @@ package org.briarproject.android.blogs; -import org.briarproject.android.api.AndroidNotificationManager; -import org.briarproject.android.controller.DbControllerImpl; +import org.briarproject.android.controller.handler.ResultExceptionHandler; import org.briarproject.android.controller.handler.ResultHandler; 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.db.NoSuchGroupException; +import org.briarproject.api.db.NoSuchMessageException; import org.briarproject.api.identity.Author; -import org.briarproject.api.identity.IdentityManager; -import org.briarproject.api.sync.GroupId; import java.util.ArrayList; import java.util.Collection; @@ -24,82 +17,56 @@ 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 { +public class FeedControllerImpl extends BaseControllerImpl + implements FeedController { private static final Logger LOG = Logger.getLogger(FeedControllerImpl.class.getName()); - @SuppressWarnings("WeakerAccess") - @Inject - AndroidNotificationManager notificationManager; - @Inject - protected EventBus eventBus; - - @Inject - protected volatile BlogManager blogManager; - @Inject - protected volatile IdentityManager identityManager; - - private volatile OnBlogPostAddedListener listener; - @Inject FeedControllerImpl() { } @Override - public void onResume() { + public void onStart() { + super.onStart(); notificationManager.blockAllBlogPostNotifications(); notificationManager.clearAllBlogPostNotifications(); - eventBus.addListener(this); } @Override - public void onPause() { + public void onStop() { + super.onStop(); notificationManager.unblockAllBlogPostNotifications(); - eventBus.removeListener(this); } @Override - public void eventOccurred(Event e) { - if (!(e instanceof BlogPostAddedEvent)) return; - - LOG.info("New blog post added"); - if (listener != null) { - BlogPostAddedEvent m = (BlogPostAddedEvent) e; - BlogPostHeader header = m.getHeader(); - addPost(m.getGroupId(), header); - } - } - - @Override - public void loadPosts( - final ResultHandler<Collection<BlogPostItem>> resultHandler) { - - LOG.info("Loading blog posts..."); + public void loadBlogPosts( + final ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) { + LOG.info("Loading all blog posts..."); runOnDbThread(new Runnable() { @Override public void run() { - Collection<BlogPostItem> posts = new ArrayList<>(); try { // load blog posts long now = System.currentTimeMillis(); + Collection<BlogPostItem> posts = new ArrayList<>(); 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(b.getId(), h, body)); + try { + posts.addAll(loadItems(b.getId())); + } catch (NoSuchGroupException | NoSuchMessageException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); } } long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) - LOG.info("Loading posts took " + duration + " ms"); - resultHandler.onResult(posts); + LOG.info("Loading all posts took " + duration + " ms"); + handler.onResult(posts); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - resultHandler.onResult(null); + handler.onException(e); } } }); @@ -114,8 +81,7 @@ public class FeedControllerImpl extends DbControllerImpl try { // load blog posts long now = System.currentTimeMillis(); - Author a = - identityManager.getLocalAuthors().iterator().next(); + Author a = identityManager.getLocalAuthor(); Blog b = blogManager.getPersonalBlog(a); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) @@ -130,25 +96,4 @@ public class FeedControllerImpl extends DbControllerImpl }); } - @Override - public void setOnBlogPostAddedListener(OnBlogPostAddedListener listener) { - this.listener = listener; - } - - private void addPost(final GroupId groupId, final BlogPostHeader header) { - runOnDbThread(new Runnable() { - @Override - public void run() { - try { - byte[] body = blogManager.getPostBody(header.getId()); - BlogPostItem post = new BlogPostItem(groupId, header, body); - listener.onBlogPostAdded(post); - } catch (DbException ex) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, ex.toString(), ex); - } - } - }); - } - } diff --git a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java index a83f9beb056b87d5d41ba441c63f08e3c5d0e2c7..9e6f5774877c5a43f99aef2f21780f02dba2d446 100644 --- a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java @@ -17,11 +17,15 @@ import android.view.ViewGroup; import org.briarproject.R; import org.briarproject.android.ActivityComponent; +import org.briarproject.android.blogs.BaseController.OnBlogPostAddedListener; import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; +import org.briarproject.android.controller.handler.UiResultExceptionHandler; 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 org.briarproject.api.blogs.BlogPostHeader; +import org.briarproject.api.db.DbException; import java.util.Collection; @@ -32,11 +36,10 @@ 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.IS_MY_BLOG; import static org.briarproject.android.blogs.BlogActivity.REQUEST_WRITE_POST; public class FeedFragment extends BaseFragment implements - OnBlogPostClickListener, FeedController.OnBlogPostAddedListener { + OnBlogPostClickListener, OnBlogPostAddedListener { public final static String TAG = FeedFragment.class.getName(); @@ -48,7 +51,7 @@ public class FeedFragment extends BaseFragment implements private BriarRecyclerView list; private Blog personalBlog = null; - static FeedFragment newInstance() { + public static FeedFragment newInstance() { FeedFragment f = new FeedFragment(); Bundle args = new Bundle(); @@ -95,6 +98,7 @@ public class FeedFragment extends BaseFragment implements @Override public void onStart() { super.onStart(); + feedController.onStart(); feedController.loadPersonalBlog( new UiResultHandler<Blog>(getActivity()) { @Override @@ -102,33 +106,30 @@ public class FeedFragment extends BaseFragment implements personalBlog = b; } }); - } - - @Override - public void onResume() { - super.onResume(); - list.startPeriodicUpdate(); - feedController.onResume(); - feedController.loadPosts( - new UiResultHandler<Collection<BlogPostItem>>(getActivity()) { + feedController.loadBlogPosts( + new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>( + getActivity()) { @Override public void onResultUi(Collection<BlogPostItem> posts) { - if (posts == null) { - // TODO show error? - } else if (posts.isEmpty()) { + if (posts.isEmpty()) { list.showData(); } else { adapter.addAll(posts); } } + @Override + public void onExceptionUi(DbException exception) { + // TODO + } }); + list.startPeriodicUpdate(); } @Override - public void onPause() { - super.onPause(); + public void onStop() { + super.onStop(); + feedController.onStop(); list.stopPeriodicUpdate(); - feedController.onPause(); // TODO save list position in database/preferences? } @@ -171,33 +172,30 @@ public class FeedFragment extends BaseFragment implements } @Override - public void onBlogPostAdded(final BlogPostItem post) { - listener.runOnUiThread(new Runnable() { - @Override - public void run() { - adapter.add(post); - showSnackBar(R.string.blogs_blog_post_received); - } - }); + public void onBlogPostAdded(BlogPostHeader header, final boolean local) { + feedController.loadBlogPost(header, + new UiResultExceptionHandler<BlogPostItem, DbException>( + getActivity()) { + @Override + public void onResultUi(BlogPostItem post) { + adapter.add(post); + if (local) { + showSnackBar(R.string.blogs_blog_post_created); + } else { + showSnackBar(R.string.blogs_blog_post_received); + } + } + @Override + public void onExceptionUi(DbException exception) { + // TODO: Decide how to handle errors in the UI + } + } + ); } @Override public void onBlogPostClick(BlogPostItem post) { - byte[] groupId = post.getGroupId().getBytes(); - String name = getString(R.string.blogs_personal_blog, - post.getAuthor().getName()); - boolean myBlog = personalBlog != null && - personalBlog.getId().equals(post.getGroupId()); - - Intent i = new Intent(getActivity(), BlogActivity.class); - i.putExtra(GROUP_ID, groupId); - i.putExtra(BLOG_NAME, name); - i.putExtra(IS_MY_BLOG, myBlog); - ActivityOptionsCompat options = - makeCustomAnimation(getActivity(), - android.R.anim.slide_in_left, - android.R.anim.slide_out_right); - startActivity(i, options.toBundle()); + // TODO Open detail view of post } @Override @@ -228,5 +226,4 @@ public class FeedFragment extends BaseFragment implements } s.show(); } - } diff --git a/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java b/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java deleted file mode 100644 index 97f33ebf4bf78112f21dd0b00c9ed98a34a7aa03..0000000000000000000000000000000000000000 --- a/briar-android/src/org/briarproject/android/blogs/MyBlogsFragment.java +++ /dev/null @@ -1,169 +0,0 @@ -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 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() { - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - - setHasOptionsMenu(true); - - adapter = new BlogListAdapter(getActivity()); - - list = (BriarRecyclerView) inflater - .inflate(R.layout.fragment_blogs_my, container, false); - list.setLayoutManager(new LinearLayoutManager(getActivity())); - list.setAdapter(adapter); - list.setEmptyText(getString(R.string.blogs_my_blogs_empty_state)); - - return list; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - listener.getActivityComponent().inject(this); - // Starting from here, we can use injected objects - } - - @Override - public void onResume() { - super.onResume(); - adapter.clear(); - list.showProgressBar(); - loadBlogs(); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.blogs_my_actions, menu); - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - // Handle presses on the action bar items - switch (item.getItemId()) { - case R.id.action_create_blog: - Intent intent = - new Intent(getContext(), CreateBlogActivity.class); - ActivityOptionsCompat options = - makeCustomAnimation(getActivity(), - android.R.anim.slide_in_left, - android.R.anim.slide_out_right); - ActivityCompat.startActivity(getActivity(), intent, - options.toBundle()); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public String getUniqueTag() { - return TAG; - } - - @Override - public void injectFragment(ActivityComponent component) { - 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/ReblogActivity.java b/briar-android/src/org/briarproject/android/blogs/ReblogActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..923d520a65053f6d7adfc3c2e7ab03c977acf2f8 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/ReblogActivity.java @@ -0,0 +1,91 @@ +package org.briarproject.android.blogs; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.transition.Fade; +import android.transition.Transition; +import android.view.MenuItem; + +import org.briarproject.R; +import org.briarproject.android.ActivityComponent; +import org.briarproject.android.BriarActivity; +import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; + +import static org.briarproject.android.blogs.BlogActivity.POST_ID; + +public class ReblogActivity extends BriarActivity implements + BaseFragmentListener { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (Build.VERSION.SDK_INT >= 21) { + setTransition(); + } + + Intent intent = getIntent(); + byte[] groupId = intent.getByteArrayExtra(GROUP_ID); + if (groupId == null) + throw new IllegalArgumentException("No group ID in intent"); + byte[] postId = intent.getByteArrayExtra(POST_ID); + if (postId == null) + throw new IllegalArgumentException("No post message ID in intent"); + + setContentView(R.layout.activity_fragment_container); + + if (savedInstanceState == null) { + ReblogFragment f = ReblogFragment + .newInstance(new GroupId(groupId), new MessageId(postId)); + getSupportFragmentManager() + .beginTransaction() + .add(R.id.fragmentContainer, f) + .commit(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + @Override + public void showLoadingScreen(boolean isBlocking, int stringId) { + // this is handled by the fragment + } + + @Override + public void hideLoadingScreen() { + // this is handled by the fragment + } + + @Override + public void onFragmentCreated(String tag) { + + } + + @TargetApi(21) + private void setTransition() { + Transition fade = new Fade(); + fade.excludeTarget(android.R.id.statusBarBackground, true); + fade.excludeTarget(R.id.action_bar_container, true); + fade.excludeTarget(android.R.id.navigationBarBackground, true); + getWindow().setExitTransition(fade); + getWindow().setEnterTransition(fade); + } +} diff --git a/briar-android/src/org/briarproject/android/blogs/ReblogFragment.java b/briar-android/src/org/briarproject/android/blogs/ReblogFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..1fee6c6473124c6d2893cbcb5bd01d3e33fe6274 --- /dev/null +++ b/briar-android/src/org/briarproject/android/blogs/ReblogFragment.java @@ -0,0 +1,198 @@ +package org.briarproject.android.blogs; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.ScrollView; + +import org.briarproject.R; +import org.briarproject.android.ActivityComponent; +import org.briarproject.android.controller.handler.UiResultExceptionHandler; +import org.briarproject.android.fragment.BaseFragment; +import org.briarproject.api.db.DbException; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; + +import javax.inject.Inject; + +import static android.view.View.FOCUS_DOWN; +import static android.view.View.GONE; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static org.briarproject.android.BriarActivity.GROUP_ID; +import static org.briarproject.android.blogs.BlogActivity.POST_ID; + +public class ReblogFragment extends BaseFragment { + + public static final String TAG = ReblogFragment.class.getName(); + + private BaseFragmentListener listener; + private ViewHolder ui; + private GroupId blogId; + private MessageId postId; + private BlogPostItem item; + + @Inject + FeedController feedController; + + static ReblogFragment newInstance(GroupId groupId, MessageId messageId) { + ReblogFragment f = new ReblogFragment(); + + Bundle args = new Bundle(); + args.putByteArray(GROUP_ID, groupId.getBytes()); + args.putByteArray(POST_ID, messageId.getBytes()); + f.setArguments(args); + + return f; + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + listener = (BaseFragmentListener) context; + } catch (ClassCastException e) { + throw new ClassCastException( + "Using class must implement BaseFragmentListener"); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + setHasOptionsMenu(true); + + Bundle args = getArguments(); + blogId = new GroupId(args.getByteArray(GROUP_ID)); + postId = new MessageId(args.getByteArray(POST_ID)); + + View v = inflater.inflate(R.layout.fragment_reblog, container, + false); + ui = new ViewHolder(v); + ui.post.setTransitionName(postId); + showProgressBar(); + + return v; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + listener.getActivityComponent().inject(this); + } + + @Override + public void onStart() { + super.onStart(); + + // TODO: Load blog post when fragment is created. #631 + feedController.loadBlogPost(blogId, postId, + new UiResultExceptionHandler<BlogPostItem, DbException>( + getActivity()) { + @Override + public void onResultUi(BlogPostItem result) { + item = result; + bindViewHolder(); + } + + @Override + public void onExceptionUi(DbException exception) { + // TODO + finish(); + } + }); + } + + private void bindViewHolder() { + if (item == null) return; + + hideProgressBar(); + + ui.post.bindItem(item); + ui.post.hideReblogButton(); + + ui.publish.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + send(); + finish(); + } + }); + ui.publish.setEnabled(true); + ui.scrollView.post(new Runnable() { + @Override + public void run() { + ui.scrollView.fullScroll(FOCUS_DOWN); + } + }); + } + + private void send() { + String comment = getComment(); + feedController.repeatPost(item, comment, + new UiResultExceptionHandler<Void, DbException>(getActivity()) { + @Override + public void onResultUi(Void result) { + // do nothing, this fragment is gone already + } + + @Override + public void onExceptionUi(DbException exception) { + // do nothing, this fragment is gone already + } + }); + } + + @Nullable + private String getComment() { + if (ui.input.getText().length() == 0) return null; + return ui.input.getText().toString(); + } + + private void showProgressBar() { + ui.progressBar.setVisibility(VISIBLE); + ui.input.setVisibility(GONE); + ui.publish.setVisibility(GONE); + } + + private void hideProgressBar() { + ui.progressBar.setVisibility(INVISIBLE); + ui.input.setVisibility(VISIBLE); + ui.publish.setVisibility(VISIBLE); + } + + private static class ViewHolder { + + private final ScrollView scrollView; + private final ProgressBar progressBar; + private final BlogPostViewHolder post; + private final EditText input; + private final Button publish; + + private ViewHolder(View v) { + scrollView = (ScrollView) v.findViewById(R.id.scrollView); + progressBar = (ProgressBar) v.findViewById(R.id.progressBar); + post = new BlogPostViewHolder(v.findViewById(R.id.postLayout)); + input = (EditText) v.findViewById(R.id.inputText); + publish = (Button) v.findViewById(R.id.publishButton); + } + } +} diff --git a/briar-android/src/org/briarproject/android/fragment/BaseFragment.java b/briar-android/src/org/briarproject/android/fragment/BaseFragment.java index 75d480bfbfe78a18600f4338f3c57e4892dfb9e7..c19bcf1eed31cafa16714666da3283ab0a1901cb 100644 --- a/briar-android/src/org/briarproject/android/fragment/BaseFragment.java +++ b/briar-android/src/org/briarproject/android/fragment/BaseFragment.java @@ -3,6 +3,7 @@ package org.briarproject.android.fragment; import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.annotation.UiThread; import android.support.v4.app.Fragment; import org.briarproject.android.ActivityComponent; @@ -39,18 +40,27 @@ public abstract class BaseFragment extends Fragment { listener.onFragmentCreated(getUniqueTag()); } + @UiThread + protected void finish() { + getActivity().supportFinishAfterTransition(); + } + public interface BaseFragmentListener { + @UiThread void showLoadingScreen(boolean isBlocking, int stringId); + @UiThread void hideLoadingScreen(); void runOnUiThread(Runnable runnable); void runOnDbThread(Runnable runnable); + @UiThread ActivityComponent getActivityComponent(); + @UiThread void onFragmentCreated(String tag); } } diff --git a/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java b/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java index f2227d826257c7a2feb11d744642846c75db6435..3c14628ebf55a535190f26736029abf327573676 100644 --- a/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java +++ b/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java @@ -32,12 +32,12 @@ public class IntroductionActivity extends BriarActivity implements if (contactId == -1) throw new IllegalArgumentException("Wrong ContactId"); - setContentView(R.layout.activity_introduction); + setContentView(R.layout.activity_fragment_container); if (savedInstanceState == null) { getSupportFragmentManager() .beginTransaction() - .add(R.id.introductionContainer, + .add(R.id.fragmentContainer, ContactChooserFragment.newInstance()) .commit(); } @@ -109,7 +109,7 @@ public class IntroductionActivity extends BriarActivity implements android.R.anim.slide_in_left, android.R.anim.slide_out_right) .addSharedElement(view, "avatar") - .replace(R.id.introductionContainer, messageFragment, + .replace(R.id.fragmentContainer, messageFragment, ContactChooserFragment.TAG) .addToBackStack(null) .commit(); diff --git a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java index 82fca3e36d63c587eac49ee6e7edabe278b00437..268b16b85ecc8822b0ae808fdc742bfa58540cd7 100644 --- a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java +++ b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java @@ -343,12 +343,12 @@ public class ShowQrCodeFragment extends BaseEventFragment }); } - private void finish() { + @Override + protected void finish() { getActivity().getSupportFragmentManager().popBackStack(); } private class BluetoothStateReceiver extends BroadcastReceiver { - @Override public void onReceive(Context ctx, Intent intent) { int state = intent.getIntExtra(EXTRA_STATE, 0); diff --git a/briar-android/src/org/briarproject/android/util/AuthorView.java b/briar-android/src/org/briarproject/android/util/AuthorView.java new file mode 100644 index 0000000000000000000000000000000000000000..c98c889a305b28ec67ca4486c35375fa3e8609a7 --- /dev/null +++ b/briar-android/src/org/briarproject/android/util/AuthorView.java @@ -0,0 +1,141 @@ +package org.briarproject.android.util; + +import android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.support.annotation.Nullable; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import org.briarproject.R; +import org.briarproject.android.blogs.BlogActivity; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.Author.Status; +import org.briarproject.api.sync.GroupId; + +import de.hdodenhof.circleimageview.CircleImageView; +import im.delight.android.identicons.IdenticonDrawable; + +import static android.content.Context.LAYOUT_INFLATER_SERVICE; +import static android.graphics.Typeface.BOLD; +import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation; +import static android.util.TypedValue.COMPLEX_UNIT_PX; +import static org.briarproject.android.BriarActivity.GROUP_ID; +import static org.briarproject.api.identity.Author.Status.OURSELVES; + +public class AuthorView extends RelativeLayout { + + private final CircleImageView avatar; + private final ImageView avatarIcon; + private final TextView authorName; + private final TextView date; + private final TrustIndicatorView trustIndicator; + + public AuthorView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.author_view, this, true); + + avatar = (CircleImageView) findViewById(R.id.avatar); + avatarIcon = (ImageView) findViewById(R.id.avatarIcon); + authorName = (TextView) findViewById(R.id.authorName); + date = (TextView) findViewById(R.id.dateView); + trustIndicator = (TrustIndicatorView) findViewById(R.id.trustIndicator); + + TypedArray attributes = + context.obtainStyledAttributes(attrs, R.styleable.AuthorView); + int persona = attributes.getInteger(R.styleable.AuthorView_persona, 0); + setPersona(persona); + attributes.recycle(); + } + + public AuthorView(Context context) { + this(context, null); + } + + public void setAuthor(Author author) { + authorName.setText(author.getName()); + IdenticonDrawable d = new IdenticonDrawable(author.getId().getBytes()); + avatar.setImageDrawable(d); + + invalidate(); + requestLayout(); + } + + public void setAuthorStatus(Status status) { + trustIndicator.setTrustLevel(status); + if (status == OURSELVES) { + authorName.setTypeface(authorName.getTypeface(), BOLD); + } + + invalidate(); + requestLayout(); + } + + public void setDate(long date) { + this.date.setText(AndroidUtils.formatDate(getContext(), date)); + + invalidate(); + requestLayout(); + } + + public void setBlogLink(final GroupId groupId) { + setClickable(true); + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute( + android.R.attr.selectableItemBackground, outValue, true); + setBackgroundResource(outValue.resourceId); + setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent i = new Intent(getContext(), BlogActivity.class); + i.putExtra(GROUP_ID, groupId.getBytes()); + ActivityOptionsCompat options = + makeCustomAnimation(getContext(), + android.R.anim.slide_in_left, + android.R.anim.slide_out_right); + Intent[] intents = {i}; + ContextCompat.startActivities(getContext(), intents, + options.toBundle()); + } + }); + } + + public void unsetBlogLink() { + setClickable(false); + setBackgroundResource(android.R.color.transparent); + setOnClickListener(null); + } + + private void setPersona(int persona) { + switch (persona) { + // reblogger + case 1: + avatarIcon.setVisibility(VISIBLE); + break; + // commenter + case 2: + ViewGroup.LayoutParams params = avatar.getLayoutParams(); + int size = getResources().getDimensionPixelSize( + R.dimen.blogs_avatar_comment_size); + params.height = size; + params.width = size; + avatar.setLayoutParams(params); + float textSize = getResources() + .getDimensionPixelSize(R.dimen.text_size_tiny); + authorName.setTextSize(COMPLEX_UNIT_PX, textSize); + break; + } + } + +} diff --git a/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java b/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java index a9ea3c8fb24653f07cbfc09252813550abad1253..97d8e7763609a175d86a9e18199098239720bfbf 100644 --- a/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java +++ b/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java @@ -26,11 +26,6 @@ public class TrustIndicatorView extends ImageView { } public void setTrustLevel(Status status) { - if (status == OURSELVES) { - setVisibility(GONE); - return; - } - int res; switch (status) { case ANONYMOUS: @@ -42,11 +37,17 @@ public class TrustIndicatorView extends ImageView { case VERIFIED: res = R.drawable.trust_indicator_verified; break; + case OURSELVES: + res = R.drawable.ic_our_identity_black; + break; default: res = R.drawable.trust_indicator_unknown; } setImageDrawable(ContextCompat.getDrawable(getContext(), res)); setVisibility(VISIBLE); + + invalidate(); + requestLayout(); } } diff --git a/briar-api/src/org/briarproject/api/blogs/BlogManager.java b/briar-api/src/org/briarproject/api/blogs/BlogManager.java index ac8ea516b47f4269a51f8fca2c0d68a4d0fc1437..2637779d46e248687824ca3518d9906cd0368b6a 100644 --- a/briar-api/src/org/briarproject/api/blogs/BlogManager.java +++ b/briar-api/src/org/briarproject/api/blogs/BlogManager.java @@ -56,7 +56,7 @@ public interface BlogManager { BlogPostHeader getPostHeader(GroupId g, MessageId m) throws DbException; /** Returns the body of the blog post with the given ID. */ - byte[] getPostBody(MessageId m) throws DbException; + String getPostBody(MessageId m) throws DbException; /** Returns the headers of all posts in the given blog. */ Collection<BlogPostHeader> getPostHeaders(GroupId g) throws DbException; diff --git a/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java b/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java index 2720910e0c1f1ca77a7f3e93fc2950f582215fc3..92f7e9c8a0d6ae289267ef5d105438fb841e16eb 100644 --- a/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java +++ b/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java @@ -516,7 +516,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, } @Override - public byte[] getPostBody(MessageId m) throws DbException { + public String getPostBody(MessageId m) throws DbException { try { BdfList message = clientHelper.getMessageAsList(m); if (message == null) throw new DbException(); @@ -526,15 +526,14 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager, } } - // TODO directly return String (#598) - private byte[] getPostBody(BdfList message) throws FormatException { + private String getPostBody(BdfList message) throws FormatException { MessageType type = MessageType.valueOf(message.getLong(0).intValue()); if (type == POST) { // type, body, signature - return StringUtils.toUtf8(message.getString(1)); + return message.getString(1); } else if (type == WRAPPED_POST) { // type, p_group descriptor, p_timestamp, p_content, p_signature - return StringUtils.toUtf8(message.getString(3)); + return message.getString(3); } else { throw new FormatException(); }