mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-11 18:29:05 +01:00
Make the blog post pager usable for the feed and individual blogs
This commit is contained in:
@@ -3,4 +3,14 @@
|
||||
android:id="@+id/fragmentContainer"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="invisible"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -6,7 +6,12 @@
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:descendantFocusability="beforeDescendants"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true">
|
||||
<!-- Above Focusability attributes prevent automatic scroll-down,
|
||||
because body text is selectable -->
|
||||
|
||||
<include
|
||||
android:id="@+id/postLayout"
|
||||
|
||||
6
briar-android/res/layout/fragment_blog_post_pager.xml
Normal file
6
briar-android/res/layout/fragment_blog_post_pager.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v4.view.ViewPager
|
||||
android:id="@+id/pager"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
@@ -6,8 +6,11 @@ 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.BlogPostPagerFragment;
|
||||
import org.briarproject.android.blogs.CreateBlogActivity;
|
||||
import org.briarproject.android.blogs.FeedPostFragment;
|
||||
import org.briarproject.android.blogs.FeedFragment;
|
||||
import org.briarproject.android.blogs.FeedPostPagerFragment;
|
||||
import org.briarproject.android.blogs.ReblogActivity;
|
||||
import org.briarproject.android.blogs.ReblogFragment;
|
||||
import org.briarproject.android.blogs.RssFeedImportActivity;
|
||||
@@ -92,6 +95,10 @@ public interface ActivityComponent {
|
||||
void inject(BlogFragment fragment);
|
||||
|
||||
void inject(BlogPostFragment fragment);
|
||||
void inject(FeedPostFragment fragment);
|
||||
|
||||
void inject(BlogPostPagerFragment fragment);
|
||||
void inject(FeedPostPagerFragment fragment);
|
||||
|
||||
void inject(ReblogFragment fragment);
|
||||
|
||||
|
||||
@@ -74,7 +74,9 @@ public class NavDrawerActivity extends BriarFragmentActivity implements
|
||||
super.onNewIntent(intent);
|
||||
exitIfStartupFailed(intent);
|
||||
checkAuthorHandle(intent);
|
||||
clearBackStack();
|
||||
// FIXME why was the stack cleared here?
|
||||
// This prevents state from being restored properly
|
||||
// clearBackStack();
|
||||
if (intent.getBooleanExtra(INTENT_FORUMS, false)) {
|
||||
startFragment(ForumListFragment.newInstance());
|
||||
}
|
||||
@@ -248,7 +250,6 @@ public class NavDrawerActivity extends BriarFragmentActivity implements
|
||||
@Override
|
||||
public void hideLoadingScreen() {
|
||||
drawerLayout.setDrawerLockMode(LOCK_MODE_UNLOCKED);
|
||||
CustomAnimations.animateHeight(toolbar, true, 250);
|
||||
progressViewGroup.setVisibility(INVISIBLE);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.briarproject.android.blogs;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.CallSuper;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.fragment.BaseFragment;
|
||||
import org.briarproject.api.db.DbException;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.briarproject.android.util.AndroidUtils.MIN_RESOLUTION;
|
||||
|
||||
public abstract class BasePostFragment extends BaseFragment {
|
||||
|
||||
private final Logger LOG =
|
||||
Logger.getLogger(BasePostFragment.class.getName());
|
||||
|
||||
private View view;
|
||||
private BlogPostViewHolder ui;
|
||||
private BlogPostItem post;
|
||||
private Runnable refresher;
|
||||
|
||||
@CallSuper
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
view = inflater.inflate(R.layout.fragment_blog_post, container,
|
||||
false);
|
||||
ui = new BlogPostViewHolder(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
startPeriodicUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
stopPeriodicUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
getActivity().onBackPressed();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onBlogPostLoaded(BlogPostItem post) {
|
||||
listener.hideLoadingScreen();
|
||||
this.post = post;
|
||||
ui.bindItem(post);
|
||||
}
|
||||
|
||||
protected void onBlogPostLoadException(DbException exception) {
|
||||
// TODO: Decide how to handle errors in the UI
|
||||
finish();
|
||||
}
|
||||
|
||||
private void startPeriodicUpdate() {
|
||||
refresher = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (ui == null) return;
|
||||
LOG.info("Updating Content...");
|
||||
|
||||
ui.updateDate(post.getTimestamp());
|
||||
view.postDelayed(refresher, MIN_RESOLUTION);
|
||||
}
|
||||
};
|
||||
LOG.info("Adding Handler Callback");
|
||||
view.postDelayed(refresher, MIN_RESOLUTION);
|
||||
}
|
||||
|
||||
private void stopPeriodicUpdate() {
|
||||
if (refresher != null && ui != null) {
|
||||
LOG.info("Removing Handler Callback");
|
||||
view.removeCallbacks(refresher);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package org.briarproject.android.blogs;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
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.blogs.BaseController.OnBlogPostAddedListener;
|
||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.android.fragment.BaseFragment;
|
||||
import org.briarproject.api.blogs.BlogPostHeader;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.briarproject.android.blogs.BasePostPagerFragment.BlogPostPagerAdapter.INVALID_POSITION;
|
||||
import static org.briarproject.android.blogs.BlogActivity.POST_ID;
|
||||
|
||||
abstract class BasePostPagerFragment extends BaseFragment
|
||||
implements OnBlogPostAddedListener {
|
||||
|
||||
private ViewPager pager;
|
||||
private BlogPostPagerAdapter postPagerAdapter;
|
||||
private MessageId postId;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle state) {
|
||||
|
||||
Bundle args;
|
||||
if (state == null) args = getArguments();
|
||||
else args = state;
|
||||
byte[] p = args.getByteArray(POST_ID);
|
||||
if (p == null)
|
||||
throw new IllegalStateException("No post ID in args");
|
||||
postId = new MessageId(p);
|
||||
|
||||
View v = inflater.inflate(R.layout.fragment_blog_post_pager, container,
|
||||
false);
|
||||
pager = (ViewPager) v.findViewById(R.id.pager);
|
||||
postPagerAdapter = new BlogPostPagerAdapter(getChildFragmentManager());
|
||||
listener.showLoadingScreen(false, R.string.progress_title_please_wait);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
if (postId == null) {
|
||||
MessageId selected = getSelectedPost();
|
||||
if (selected != null) loadBlogPosts(selected);
|
||||
} else {
|
||||
loadBlogPosts(postId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
MessageId selected = getSelectedPost();
|
||||
if (selected != null)
|
||||
outState.putByteArray(POST_ID, selected.getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
|
||||
loadBlogPost(header);
|
||||
}
|
||||
|
||||
abstract void loadBlogPosts(final MessageId select);
|
||||
|
||||
abstract BaseController getController();
|
||||
|
||||
protected void onBlogPostsLoaded(MessageId select,
|
||||
Collection<BlogPostItem> posts) {
|
||||
|
||||
postId = null;
|
||||
postPagerAdapter.setPosts(posts);
|
||||
selectPost(select);
|
||||
}
|
||||
|
||||
protected void onBlogPostsLoadedException(DbException exception) {
|
||||
// TODO: Decide how to handle errors in the UI
|
||||
finish();
|
||||
}
|
||||
|
||||
private void loadBlogPost(BlogPostHeader header) {
|
||||
getController().loadBlogPost(header,
|
||||
new UiResultExceptionHandler<BlogPostItem, DbException>(
|
||||
getActivity()) {
|
||||
@Override
|
||||
public void onResultUi(BlogPostItem post) {
|
||||
addPost(post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO: Decide how to handle errors in the UI
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MessageId getSelectedPost() {
|
||||
if (postPagerAdapter.getCount() == 0) return null;
|
||||
int position = pager.getCurrentItem();
|
||||
return postPagerAdapter.getPost(position).getId();
|
||||
}
|
||||
|
||||
private void selectPost(MessageId m) {
|
||||
int pos = postPagerAdapter.getPostPosition(m);
|
||||
if (pos != INVALID_POSITION) {
|
||||
pager.setAdapter(postPagerAdapter);
|
||||
pager.setCurrentItem(pos);
|
||||
}
|
||||
}
|
||||
|
||||
protected void addPost(BlogPostItem post) {
|
||||
MessageId selected = getSelectedPost();
|
||||
postPagerAdapter.addPost(post);
|
||||
if (selected != null) selectPost(selected);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
static class BlogPostPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
static final int INVALID_POSITION = -1;
|
||||
private final List<BlogPostItem> posts = new ArrayList<>();
|
||||
|
||||
private BlogPostPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return posts.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
BlogPostItem post = posts.get(position);
|
||||
return FeedPostFragment.newInstance(post.getGroupId(), post.getId());
|
||||
}
|
||||
|
||||
private BlogPostItem getPost(int position) {
|
||||
return posts.get(position);
|
||||
}
|
||||
|
||||
private void setPosts(Collection<BlogPostItem> posts) {
|
||||
this.posts.clear();
|
||||
this.posts.addAll(posts);
|
||||
Collections.sort(this.posts);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void addPost(BlogPostItem post) {
|
||||
posts.add(post);
|
||||
Collections.sort(posts);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private int getPostPosition(MessageId m) {
|
||||
int count = getCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (getPost(i).getId().equals(m)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return INVALID_POSITION;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,13 +2,6 @@ package org.briarproject.android.blogs;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import org.briarproject.R;
|
||||
@@ -16,42 +9,25 @@ import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
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;
|
||||
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.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
public class BlogActivity extends BriarActivity implements
|
||||
OnBlogPostAddedListener,
|
||||
OnBlogPostClickListener, BaseFragmentListener {
|
||||
OnBlogPostAddedListener, OnBlogPostClickListener, BaseFragmentListener {
|
||||
|
||||
static final int REQUEST_WRITE_POST = 1;
|
||||
static final int REQUEST_SHARE = 2;
|
||||
public static final String BLOG_NAME = "briar.BLOG_NAME";
|
||||
static final String BLOG_NAME = "briar.BLOG_NAME";
|
||||
static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG";
|
||||
static final String POST_ID = "briar.POST_ID";
|
||||
|
||||
public static final String POST_ID = "briar.POST_ID";
|
||||
|
||||
private GroupId groupId;
|
||||
private ProgressBar progressBar;
|
||||
private ViewPager pager;
|
||||
private BlogPagerAdapter blogPagerAdapter;
|
||||
private BlogPostPagerAdapter postPagerAdapter;
|
||||
private String blogName;
|
||||
private boolean isNew;
|
||||
private MessageId savedPostId;
|
||||
|
||||
@Inject
|
||||
BlogController blogController;
|
||||
@@ -64,63 +40,30 @@ public class BlogActivity extends BriarActivity implements
|
||||
Intent i = getIntent();
|
||||
byte[] b = i.getByteArrayExtra(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException("No group ID in intent");
|
||||
groupId = new GroupId(b);
|
||||
GroupId groupId = new GroupId(b);
|
||||
blogController.setGroupId(groupId);
|
||||
|
||||
// Name of the blog
|
||||
blogName = i.getStringExtra(BLOG_NAME);
|
||||
String blogName = i.getStringExtra(BLOG_NAME);
|
||||
if (blogName != null) setTitle(blogName);
|
||||
|
||||
// Was this blog just created?
|
||||
isNew = i.getBooleanExtra(IS_NEW_BLOG, false);
|
||||
boolean isNew = i.getBooleanExtra(IS_NEW_BLOG, false);
|
||||
|
||||
setContentView(R.layout.activity_blog);
|
||||
|
||||
pager = (ViewPager) findViewById(R.id.pager);
|
||||
setContentView(R.layout.activity_fragment_container);
|
||||
progressBar = (ProgressBar) findViewById(R.id.progressBar);
|
||||
|
||||
blogPagerAdapter = new BlogPagerAdapter(getSupportFragmentManager());
|
||||
postPagerAdapter = new BlogPostPagerAdapter(
|
||||
getSupportFragmentManager());
|
||||
|
||||
if (state == null || state.getByteArray(POST_ID) == null) {
|
||||
// The blog fragment has its own progress bar
|
||||
hideLoadingScreen();
|
||||
pager.setAdapter(blogPagerAdapter);
|
||||
savedPostId = null;
|
||||
} else {
|
||||
// Adapter will be set in selectPostInPostPager()
|
||||
savedPostId = new MessageId(state.getByteArray(POST_ID));
|
||||
if (state == null) {
|
||||
BlogFragment f = BlogFragment.newInstance(groupId, blogName, isNew);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (savedPostId == null) {
|
||||
MessageId selected = getSelectedPostInPostPager();
|
||||
if (selected != null) loadBlogPosts(selected);
|
||||
} else {
|
||||
loadBlogPosts(savedPostId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
MessageId selected = getSelectedPostInPostPager();
|
||||
if (selected != null)
|
||||
outState.putByteArray(POST_ID, selected.getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (pager.getAdapter() == postPagerAdapter) {
|
||||
pager.setAdapter(blogPagerAdapter);
|
||||
savedPostId = null;
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -128,6 +71,21 @@ public class BlogActivity extends BriarActivity implements
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
|
||||
// all our fragments are implementing and registering that hook,
|
||||
// so we don't need to do that ourselves
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlogPostClick(BlogPostItem post) {
|
||||
BlogPostPagerFragment f = BlogPostPagerFragment.newInstance(post.getId());
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
|
||||
.addToBackStack(f.getUniqueTag())
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showLoadingScreen(boolean isBlocking, int stringId) {
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
@@ -135,176 +93,10 @@ public class BlogActivity extends BriarActivity implements
|
||||
|
||||
@Override
|
||||
public void hideLoadingScreen() {
|
||||
progressBar.setVisibility(GONE);
|
||||
progressBar.setVisibility(INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentCreated(String tag) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlogPostClick(BlogPostItem post) {
|
||||
loadBlogPosts(post.getId());
|
||||
}
|
||||
|
||||
private void loadBlogPosts(final MessageId select) {
|
||||
blogController.loadBlogPosts(
|
||||
new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>(
|
||||
this) {
|
||||
@Override
|
||||
public void onResultUi(Collection<BlogPostItem> posts) {
|
||||
hideLoadingScreen();
|
||||
savedPostId = null;
|
||||
postPagerAdapter.setPosts(posts);
|
||||
selectPostInPostPager(select);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO: Decide how to handle errors in the UI
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
|
||||
if (pager.getAdapter() == postPagerAdapter) {
|
||||
loadBlogPost(header);
|
||||
} else {
|
||||
BlogFragment f = blogPagerAdapter.getFragment();
|
||||
if (f != null && f.isVisible()) f.onBlogPostAdded(header, local);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadBlogPost(BlogPostHeader header) {
|
||||
blogController.loadBlogPost(header,
|
||||
new UiResultExceptionHandler<BlogPostItem, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(BlogPostItem post) {
|
||||
addPostToPostPager(post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO: Decide how to handle errors in the UI
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MessageId getSelectedPostInPostPager() {
|
||||
if (pager.getAdapter() != postPagerAdapter) return null;
|
||||
if (postPagerAdapter.getCount() == 0) return null;
|
||||
int position = pager.getCurrentItem();
|
||||
return postPagerAdapter.getPost(position).getId();
|
||||
}
|
||||
|
||||
private void selectPostInPostPager(MessageId m) {
|
||||
int count = postPagerAdapter.getCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (postPagerAdapter.getPost(i).getId().equals(m)) {
|
||||
pager.setAdapter(postPagerAdapter);
|
||||
pager.setCurrentItem(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addPostToPostPager(BlogPostItem post) {
|
||||
MessageId selected = getSelectedPostInPostPager();
|
||||
postPagerAdapter.addPost(post);
|
||||
if (selected != null) selectPostInPostPager(selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode,
|
||||
Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
// The BlogPostAddedEvent arrives when the controller is not listening,
|
||||
// so we need to manually reload the blog posts :(
|
||||
if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) {
|
||||
if (pager.getAdapter() == postPagerAdapter) {
|
||||
MessageId selected = getSelectedPostInPostPager();
|
||||
if (selected != null) loadBlogPosts(selected);
|
||||
} else {
|
||||
BlogFragment f = blogPagerAdapter.getFragment();
|
||||
if (f != null && f.isVisible()) f.loadBlogPosts(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private class BlogPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private BlogFragment fragment = null;
|
||||
|
||||
private BlogPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
return BlogFragment.newInstance(groupId, blogName, isNew);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
// save a reference to the single fragment here for later
|
||||
fragment =
|
||||
(BlogFragment) super.instantiateItem(container, position);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private BlogFragment getFragment() {
|
||||
return fragment;
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private static class BlogPostPagerAdapter
|
||||
extends FragmentStatePagerAdapter {
|
||||
|
||||
private final List<BlogPostItem> posts = new ArrayList<>();
|
||||
|
||||
private BlogPostPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return posts.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
return BlogPostFragment.newInstance(posts.get(position).getId());
|
||||
}
|
||||
|
||||
private BlogPostItem getPost(int position) {
|
||||
return posts.get(position);
|
||||
}
|
||||
|
||||
private void setPosts(Collection<BlogPostItem> posts) {
|
||||
this.posts.clear();
|
||||
this.posts.addAll(posts);
|
||||
Collections.sort(this.posts);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void addPost(BlogPostItem post) {
|
||||
posts.add(post);
|
||||
Collections.sort(posts);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -59,6 +59,7 @@ public class BlogFragment extends BaseFragment implements
|
||||
private BlogPostAdapter adapter;
|
||||
private BriarRecyclerView list;
|
||||
private MenuItem writeButton, deleteButton;
|
||||
private boolean isMyBlog = false, canDeleteBlog = false;
|
||||
|
||||
static BlogFragment newInstance(GroupId groupId, String name,
|
||||
boolean isNew) {
|
||||
@@ -114,7 +115,7 @@ public class BlogFragment extends BaseFragment implements
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
blogController.setGroupId(groupId);
|
||||
blogController.setOnBlogPostAddedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -141,7 +142,9 @@ public class BlogFragment extends BaseFragment implements
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.blogs_blog_actions, menu);
|
||||
writeButton = menu.findItem(R.id.action_write_blog_post);
|
||||
if (isMyBlog) writeButton.setVisible(true);
|
||||
deleteButton = menu.findItem(R.id.action_blog_delete);
|
||||
if (canDeleteBlog) deleteButton.setVisible(true);
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
@@ -161,8 +164,8 @@ public class BlogFragment extends BaseFragment implements
|
||||
new Intent(getActivity(), WriteBlogPostActivity.class);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
i.putExtra(BLOG_NAME, blogName);
|
||||
ActivityCompat.startActivityForResult(getActivity(), i,
|
||||
REQUEST_WRITE_POST, options.toBundle());
|
||||
startActivityForResult(i, REQUEST_WRITE_POST,
|
||||
options.toBundle());
|
||||
return true;
|
||||
case R.id.action_blog_share:
|
||||
Intent i2 = new Intent(getActivity(), ShareBlogActivity.class);
|
||||
@@ -190,9 +193,10 @@ public class BlogFragment extends BaseFragment implements
|
||||
super.onActivityResult(request, result, data);
|
||||
|
||||
if (request == REQUEST_WRITE_POST && result == RESULT_OK) {
|
||||
displaySnackbar(R.string.blogs_blog_post_created);
|
||||
displaySnackbar(R.string.blogs_blog_post_created, true);
|
||||
loadBlogPosts(true);
|
||||
} else if (request == REQUEST_SHARE && result == RESULT_OK) {
|
||||
displaySnackbar(R.string.blogs_sharing_snackbar);
|
||||
displaySnackbar(R.string.blogs_sharing_snackbar, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,9 +215,9 @@ public class BlogFragment extends BaseFragment implements
|
||||
adapter.add(post);
|
||||
if (local) {
|
||||
list.scrollToPosition(0);
|
||||
displaySnackbar(R.string.blogs_blog_post_created);
|
||||
displaySnackbar(R.string.blogs_blog_post_created, false);
|
||||
} else {
|
||||
displaySnackbar(R.string.blogs_blog_post_received);
|
||||
displaySnackbar(R.string.blogs_blog_post_received, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,13 +247,13 @@ public class BlogFragment extends BaseFragment implements
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO: Decide how to handle errors in the UI
|
||||
getActivity().finish();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void checkIfThisIsMyBlog() {
|
||||
blogController.canDeleteBlog(
|
||||
blogController.isMyBlog(
|
||||
new UiResultExceptionHandler<Boolean, DbException>(
|
||||
getActivity()) {
|
||||
@Override
|
||||
@@ -262,7 +266,7 @@ public class BlogFragment extends BaseFragment implements
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO: Decide how to handle errors in the UI
|
||||
getActivity().finish();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -281,25 +285,39 @@ public class BlogFragment extends BaseFragment implements
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO: Decide how to handle errors in the UI
|
||||
getActivity().finish();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showWriteButton() {
|
||||
isMyBlog = true;
|
||||
if (writeButton != null)
|
||||
writeButton.setVisible(true);
|
||||
}
|
||||
|
||||
private void showDeleteButton() {
|
||||
canDeleteBlog = true;
|
||||
if (deleteButton != null)
|
||||
deleteButton.setVisible(true);
|
||||
}
|
||||
|
||||
private void displaySnackbar(int stringId) {
|
||||
private void displaySnackbar(int stringId, boolean scroll) {
|
||||
Snackbar snackbar =
|
||||
Snackbar.make(list, stringId, Snackbar.LENGTH_SHORT);
|
||||
Snackbar.make(list, stringId, Snackbar.LENGTH_LONG);
|
||||
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
||||
if (scroll) {
|
||||
View.OnClickListener onClick = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
list.smoothScrollToPosition(0);
|
||||
}
|
||||
};
|
||||
snackbar.setActionTextColor(ContextCompat
|
||||
.getColor(getContext(),
|
||||
R.color.briar_button_positive));
|
||||
snackbar.setAction(R.string.blogs_blog_post_scroll_to, onClick);
|
||||
}
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
@@ -335,7 +353,7 @@ public class BlogFragment extends BaseFragment implements
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO: Decide how to handle errors in the UI
|
||||
getActivity().finish();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,35 +3,23 @@ package org.briarproject.android.blogs;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
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.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.android.fragment.BaseFragment;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.android.util.AndroidUtils.MIN_RESOLUTION;
|
||||
import static org.briarproject.android.blogs.BlogActivity.POST_ID;
|
||||
|
||||
public class BlogPostFragment extends BaseFragment {
|
||||
public class BlogPostFragment extends BasePostFragment {
|
||||
|
||||
public final static String TAG = BlogPostFragment.class.getName();
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(TAG);
|
||||
private static final String BLOG_POST_ID = "briar.BLOG_POST_ID";
|
||||
|
||||
private View view;
|
||||
private MessageId postId;
|
||||
private BlogPostViewHolder ui;
|
||||
private BlogPostItem post;
|
||||
private Runnable refresher;
|
||||
|
||||
@Inject
|
||||
BlogController blogController;
|
||||
@@ -40,7 +28,7 @@ public class BlogPostFragment extends BaseFragment {
|
||||
BlogPostFragment f = new BlogPostFragment();
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putByteArray(BLOG_POST_ID, postId.getBytes());
|
||||
bundle.putByteArray(POST_ID, postId.getBytes());
|
||||
|
||||
f.setArguments(bundle);
|
||||
return f;
|
||||
@@ -50,16 +38,18 @@ public class BlogPostFragment extends BaseFragment {
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
byte[] b = getArguments().getByteArray(BLOG_POST_ID);
|
||||
if (b == null) throw new IllegalStateException("No post ID in args");
|
||||
postId = new MessageId(b);
|
||||
Bundle args = getArguments();
|
||||
byte[] p = args.getByteArray(POST_ID);
|
||||
if (p == null) throw new IllegalStateException("No post ID in args");
|
||||
postId = new MessageId(p);
|
||||
|
||||
view = inflater.inflate(R.layout.fragment_blog_post, container,
|
||||
false);
|
||||
ui = new BlogPostViewHolder(view);
|
||||
return view;
|
||||
return super.onCreateView(inflater, container, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,62 +65,12 @@ public class BlogPostFragment extends BaseFragment {
|
||||
getActivity()) {
|
||||
@Override
|
||||
public void onResultUi(BlogPostItem post) {
|
||||
listener.hideLoadingScreen();
|
||||
BlogPostFragment.this.post = post;
|
||||
ui.bindItem(post);
|
||||
startPeriodicUpdate();
|
||||
onBlogPostLoaded(post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO: Decide how to handle errors in the UI
|
||||
finish();
|
||||
onBlogPostLoadException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
stopPeriodicUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
getActivity().onBackPressed();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
private void startPeriodicUpdate() {
|
||||
refresher = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (ui == null) return;
|
||||
LOG.info("Updating Content...");
|
||||
|
||||
ui.updateDate(post.getTimestamp());
|
||||
view.postDelayed(refresher, MIN_RESOLUTION);
|
||||
}
|
||||
};
|
||||
LOG.info("Adding Handler Callback");
|
||||
view.postDelayed(refresher, MIN_RESOLUTION);
|
||||
}
|
||||
|
||||
private void stopPeriodicUpdate() {
|
||||
if (refresher != null && ui != null) {
|
||||
LOG.info("Removing Handler Callback");
|
||||
view.removeCallbacks(refresher);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.briarproject.android.blogs;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.android.blogs.BlogActivity.POST_ID;
|
||||
|
||||
|
||||
public class BlogPostPagerFragment extends BasePostPagerFragment {
|
||||
|
||||
public final static String TAG = BlogPostPagerFragment.class.getName();
|
||||
|
||||
@Inject
|
||||
BlogController blogController;
|
||||
|
||||
static BlogPostPagerFragment newInstance(MessageId postId) {
|
||||
BlogPostPagerFragment f = new BlogPostPagerFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putByteArray(POST_ID, postId.getBytes());
|
||||
f.setArguments(args);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
blogController.setOnBlogPostAddedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
BaseController getController() {
|
||||
return blogController;
|
||||
}
|
||||
|
||||
void loadBlogPosts(final MessageId select) {
|
||||
blogController.loadBlogPosts(
|
||||
new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>(
|
||||
getActivity()) {
|
||||
@Override
|
||||
public void onResultUi(Collection<BlogPostItem> posts) {
|
||||
onBlogPostsLoaded(select, posts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
onBlogPostsLoadedException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -195,7 +195,12 @@ public class FeedFragment extends BaseFragment implements
|
||||
|
||||
@Override
|
||||
public void onBlogPostClick(BlogPostItem post) {
|
||||
// TODO Open detail view of post
|
||||
FeedPostPagerFragment f = FeedPostPagerFragment
|
||||
.newInstance(post.getId());
|
||||
getActivity().getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.content_fragment, f, f.getUniqueTag())
|
||||
.addToBackStack(f.getUniqueTag())
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.briarproject.android.blogs;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.android.BriarActivity.GROUP_ID;
|
||||
import static org.briarproject.android.blogs.BlogActivity.POST_ID;
|
||||
|
||||
public class FeedPostFragment extends BasePostFragment {
|
||||
|
||||
public final static String TAG = FeedPostFragment.class.getName();
|
||||
|
||||
private MessageId postId;
|
||||
private GroupId blogId;
|
||||
|
||||
@Inject
|
||||
FeedController feedController;
|
||||
|
||||
static FeedPostFragment newInstance(GroupId blogId, MessageId postId) {
|
||||
FeedPostFragment f = new FeedPostFragment();
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putByteArray(GROUP_ID, blogId.getBytes());
|
||||
bundle.putByteArray(POST_ID, postId.getBytes());
|
||||
|
||||
f.setArguments(bundle);
|
||||
return f;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
Bundle args = getArguments();
|
||||
byte[] b = args.getByteArray(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException("No group ID in args");
|
||||
blogId = new GroupId(b);
|
||||
|
||||
byte[] p = args.getByteArray(POST_ID);
|
||||
if (p == null) throw new IllegalStateException("No post ID in args");
|
||||
postId = new MessageId(p);
|
||||
|
||||
return super.onCreateView(inflater, container, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
feedController.loadBlogPost(blogId, postId,
|
||||
new UiResultExceptionHandler<BlogPostItem, DbException>(
|
||||
getActivity()) {
|
||||
@Override
|
||||
public void onResultUi(BlogPostItem post) {
|
||||
onBlogPostLoaded(post);
|
||||
}
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
onBlogPostLoadException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.briarproject.android.blogs;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.android.blogs.BlogActivity.POST_ID;
|
||||
|
||||
public class FeedPostPagerFragment extends BasePostPagerFragment {
|
||||
|
||||
public final static String TAG = FeedPostPagerFragment.class.getName();
|
||||
|
||||
@Inject
|
||||
FeedController feedController;
|
||||
|
||||
static FeedPostPagerFragment newInstance(MessageId postId) {
|
||||
FeedPostPagerFragment f = new FeedPostPagerFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putByteArray(POST_ID, postId.getBytes());
|
||||
f.setArguments(args);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
feedController.setOnBlogPostAddedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
BaseController getController() {
|
||||
return feedController;
|
||||
}
|
||||
|
||||
void loadBlogPosts(final MessageId select) {
|
||||
feedController.loadBlogPosts(
|
||||
new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>(
|
||||
getActivity()) {
|
||||
@Override
|
||||
public void onResultUi(Collection<BlogPostItem> posts) {
|
||||
onBlogPostsLoaded(select, posts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
onBlogPostsLoadedException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user