Forum Sharing Client UI

This changes `ShareForumActivity` to use two fragments to facilitate
forum sharing with the new Forum Sharing Client backend.

The `ContactSelectorFragment` allows the user to select a
number of contacts. If there is an ongoing sharing session or the forum
is already shared with the contact, it is disabled in the list. If there
is at least one contact selected, a button appears in the toolbar that
brings the user to the `ShareForumMessageFragment` where the user can
write an optional message to be send along with the invitation.

After sending an invitation, the user is brought back to the forum that
she shared and there is a snackbar showing up briefly to indicate the
successful invitation.

The invitation is shown along with the message within the private
conversation of each contact. The person who shares the forum also sees
the invitation and the message as outgoing messages that also display
the current status of the messages.

A notification is shown like for other private messages as well.

Please note that this commit does not include a way for users to respond
to invitations.
This commit is contained in:
Torsten Grote
2016-04-26 20:27:03 -03:00
parent 2cc621ed1b
commit 3a9d66a85f
24 changed files with 1039 additions and 181 deletions

View File

@@ -1,6 +1,11 @@
package org.briarproject.android.forum;
import android.content.Context;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.Build;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -47,10 +52,16 @@ public class ContactSelectorAdapter
} else {
ui.checkBox.setChecked(false);
}
if (item.isDisabled()) {
// we share this forum already with that contact
ui.layout.setEnabled(false);
grayOutItem(ui);
}
}
public Collection<ContactId> getSelectedContactIds() {
Collection<ContactId> selected = new ArrayList<ContactId>();
Collection<ContactId> selected = new ArrayList<>();
for (int i = 0; i < contacts.size(); i++) {
SelectableContactListItem item =
@@ -78,4 +89,19 @@ public class ContactSelectorAdapter
return compareByName(c1, c2);
}
private void grayOutItem(final SelectableContactHolder ui) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
float alpha = 0.25f;
ui.avatar.setAlpha(alpha);
ui.name.setAlpha(alpha);
ui.checkBox.setAlpha(alpha);
} else {
ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY,
PorterDuff.Mode.MULTIPLY);
ui.avatar.setColorFilter(colorFilter);
ui.name.setEnabled(false);
ui.checkBox.setEnabled(false);
}
}
}

View File

@@ -0,0 +1,247 @@
package org.briarproject.android.forum;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.transition.Fade;
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.AndroidComponent;
import org.briarproject.android.contact.BaseContactListAdapter;
import org.briarproject.android.contact.ContactListItem;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.db.DbException;
import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.forum.ShareForumActivity.CONTACTS;
import static org.briarproject.android.forum.ShareForumActivity.getContactsFromIds;
import static org.briarproject.api.forum.ForumConstants.GROUP_ID;
public class ContactSelectorFragment extends BaseFragment implements
BaseContactListAdapter.OnItemClickListener {
public final static String TAG = "ContactSelectorFragment";
private ShareForumActivity shareForumActivity;
private Menu menu;
private BriarRecyclerView list;
private ContactSelectorAdapter adapter;
private Collection<ContactId> selectedContacts;
private static final Logger LOG =
Logger.getLogger(ContactSelectorFragment.class.getName());
// Fields that are accessed from background threads must be volatile
protected volatile GroupId groupId;
@Inject
protected volatile ContactManager contactManager;
@Inject
protected volatile IdentityManager identityManager;
@Inject
protected volatile ForumSharingManager forumSharingManager;
public static ContactSelectorFragment newInstance(GroupId groupId) {
Bundle args = new Bundle();
args.putByteArray(GROUP_ID, groupId.getBytes());
ContactSelectorFragment fragment = new ContactSelectorFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
shareForumActivity = (ShareForumActivity) context;
} catch (ClassCastException e) {
throw new InstantiationError(
"This fragment is only meant to be attached to the ShareForumActivity");
}
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
groupId = new GroupId(getArguments().getByteArray(GROUP_ID));
if (groupId == null) throw new IllegalStateException("No GroupId");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View contentView =
inflater.inflate(R.layout.introduction_contact_chooser,
container, false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setExitTransition(new Fade());
}
adapter = new ContactSelectorAdapter(getActivity(), this);
list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
list.setEmptyText(getString(R.string.no_contacts));
// restore selected contacts if available
if (savedInstanceState != null) {
ArrayList<Integer> intContacts =
savedInstanceState.getIntegerArrayList(CONTACTS);
selectedContacts = ShareForumActivity.getContactsFromIntegers(intContacts);
}
return contentView;
}
@Override
public void onResume() {
super.onResume();
if (selectedContacts != null) {
loadContacts(Collections.unmodifiableCollection(selectedContacts));
} else {
loadContacts(null);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
if (adapter != null) {
selectedContacts = adapter.getSelectedContactIds();
outState.putIntegerArrayList(CONTACTS,
getContactsFromIds(selectedContacts));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.forum_share_actions, menu);
super.onCreateOptionsMenu(menu, inflater);
this.menu = menu;
// hide sharing action initially, if no contact is selected
updateMenuItem();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
// Handle presses on the action bar items
switch (item.getItemId()) {
case android.R.id.home:
shareForumActivity.onBackPressed();
return true;
case R.id.action_share_forum:
selectedContacts = adapter.getSelectedContactIds();
shareForumActivity.showMessageScreen(groupId, selectedContacts);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void onItemClick(View view, ContactListItem item) {
((SelectableContactListItem) item).toggleSelected();
adapter.notifyItemChanged(adapter.findItemPosition(item), item);
updateMenuItem();
}
private void loadContacts(final Collection<ContactId> selection) {
shareForumActivity.runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
List<ContactListItem> contacts =
new ArrayList<>();
for (Contact c : contactManager.getActiveContacts()) {
LocalAuthor localAuthor = identityManager
.getLocalAuthor(c.getLocalAuthorId());
// was this contact already selected?
boolean selected = selection != null &&
selection.contains(c.getId());
// do we have already some sharing with that contact?
boolean disabled =
!forumSharingManager.canBeShared(groupId, c);
contacts.add(
new SelectableContactListItem(c, localAuthor,
groupId, selected, disabled));
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayContacts(Collections.unmodifiableList(contacts));
} catch (DbException e) {
displayContacts(Collections.<ContactListItem>emptyList());
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayContacts(final List<ContactListItem> contacts) {
shareForumActivity.runOnUiThread(new Runnable() {
public void run() {
if (!contacts.isEmpty()) {
adapter.addAll(contacts);
} else {
list.showData();
}
updateMenuItem();
}
});
}
private void updateMenuItem() {
if (menu == null) return;
MenuItem item = menu.findItem(R.id.action_share_forum);
if (item == null) return;
selectedContacts = adapter.getSelectedContactIds();
if (selectedContacts.size() > 0) {
item.setVisible(true);
} else {
item.setVisible(false);
}
}
}

View File

@@ -3,6 +3,7 @@ package org.briarproject.android.forum;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v7.app.AlertDialog;
@@ -49,6 +50,7 @@ import javax.inject.Inject;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
@@ -68,6 +70,7 @@ public class ForumActivity extends BriarActivity implements EventListener,
public static final String MIN_TIMESTAMP = "briar.MIN_TIMESTAMP";
private static final int REQUEST_READ = 2;
private static final int REQUEST_FORUM_SHARED = 3;
private static final Logger LOG =
Logger.getLogger(ForumActivity.class.getName());
@@ -165,7 +168,9 @@ public class ForumActivity extends BriarActivity implements EventListener,
ActivityOptionsCompat options = ActivityOptionsCompat
.makeCustomAnimation(this, android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
ActivityCompat.startActivity(this, i2, options.toBundle());
ActivityCompat
.startActivityForResult(this, i2, REQUEST_FORUM_SHARED,
options.toBundle());
return true;
case R.id.action_forum_delete:
showUnsubscribeDialog();
@@ -297,6 +302,12 @@ public class ForumActivity extends BriarActivity implements EventListener,
if (position >= 0 && position < adapter.getCount())
displayPost(position);
}
else if (request == REQUEST_FORUM_SHARED && result == RESULT_OK) {
Snackbar s = Snackbar.make(list, R.string.forum_shared_snackbar,
LENGTH_LONG);
s.getView().setBackgroundResource(R.color.briar_primary);
s.show();
}
}
@Override

View File

@@ -11,14 +11,15 @@ import java.util.Collections;
// This class is not thread-safe
public class SelectableContactListItem extends ContactListItem {
private boolean selected;
private boolean selected, disabled;
public SelectableContactListItem(Contact contact, LocalAuthor localAuthor,
GroupId groupId, boolean selected) {
GroupId groupId, boolean selected, boolean disabled) {
super(contact, localAuthor, false, groupId, Collections.<ConversationItem>emptyList());
this.selected = selected;
this.disabled = disabled;
}
public void setSelected(boolean selected) {
@@ -33,4 +34,8 @@ public class SelectableContactListItem extends ContactListItem {
selected = !selected;
}
public boolean isDisabled() {
return disabled;
}
}

View File

@@ -2,98 +2,41 @@ package org.briarproject.android.forum;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.contact.BaseContactListAdapter;
import org.briarproject.android.contact.ContactListItem;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.contact.Contact;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.db.DbException;
import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
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 ShareForumActivity extends BriarActivity implements
BaseContactListAdapter.OnItemClickListener {
BaseFragment.BaseFragmentListener {
private static final Logger LOG =
Logger.getLogger(ShareForumActivity.class.getName());
private ContactSelectorAdapter adapter;
// Fields that are accessed from background threads must be volatile
@Inject protected volatile IdentityManager identityManager;
@Inject protected volatile ContactManager contactManager;
@Inject protected volatile ForumSharingManager forumSharingManager;
private volatile GroupId groupId;
public final static String CONTACTS = "contacts";
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.introduction_contact_chooser);
setContentView(R.layout.activity_share_forum);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException();
groupId = new GroupId(b);
if (b == null) throw new IllegalStateException("No GroupId");
GroupId groupId = new GroupId(b);
adapter = new ContactSelectorAdapter(this, this);
BriarRecyclerView list =
(BriarRecyclerView) findViewById(R.id.contactList);
list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter);
list.setEmptyText(getString(R.string.no_contacts));
}
@Override
public void onResume() {
super.onResume();
loadContactsAndVisibility();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu items for use in the action bar
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.forum_share_actions, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
// Handle presses on the action bar items
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.action_share_forum:
return true;
default:
return super.onOptionsItemSelected(item);
if (savedInstanceState == null) {
ContactSelectorFragment contactSelectorFragment =
ContactSelectorFragment.newInstance(groupId);
getSupportFragmentManager().beginTransaction()
.add(R.id.shareForumContainer, contactSelectorFragment)
.commit();
}
}
@@ -102,47 +45,59 @@ public class ShareForumActivity extends BriarActivity implements
component.inject(this);
}
private void loadContactsAndVisibility() {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
List<ContactListItem> contacts = new ArrayList<>();
Collection<ContactId> selectedContacts = new HashSet<>(
forumSharingManager.getSharedWith(groupId));
public void showMessageScreen(GroupId groupId,
Collection<ContactId> contacts) {
for (Contact c : contactManager.getActiveContacts()) {
LocalAuthor localAuthor = identityManager
.getLocalAuthor(c.getLocalAuthorId());
boolean selected = selectedContacts.contains(c.getId());
contacts.add(
new SelectableContactListItem(c, localAuthor,
groupId, selected));
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayContacts(contacts);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
ShareForumMessageFragment messageFragment =
ShareForumMessageFragment.newInstance(groupId, contacts);
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(android.R.anim.fade_in,
android.R.anim.fade_out,
android.R.anim.slide_in_left,
android.R.anim.slide_out_right)
.replace(R.id.shareForumContainer, messageFragment,
ContactSelectorFragment.TAG)
.addToBackStack(null)
.commit();
}
private void displayContacts(final List<ContactListItem> contact) {
runOnUiThread(new Runnable() {
public void run() {
adapter.addAll(contact);
}
});
public static ArrayList<Integer> getContactsFromIds(
Collection<ContactId> contacts) {
// transform ContactIds to Integers so they can be added to a bundle
ArrayList<Integer> intContacts = new ArrayList<>(contacts.size());
for (ContactId contactId : contacts) {
intContacts.add(contactId.getInt());
}
return intContacts;
}
public void sharingSuccessful(View v) {
setResult(RESULT_OK);
hideSoftKeyboard(v);
supportFinishAfterTransition();
}
protected static Collection<ContactId> getContactsFromIntegers(
ArrayList<Integer> intContacts) {
// turn contact integers from a bundle back to ContactIds
List<ContactId> contacts = new ArrayList<>(intContacts.size());
for(Integer c : intContacts) {
contacts.add(new ContactId(c));
}
return contacts;
}
@Override
public void onItemClick(View view, ContactListItem item) {
((SelectableContactListItem) item).toggleSelected();
adapter.notifyItemChanged(adapter.findItemPosition(item), item);
public void showLoadingScreen(boolean isBlocking, int stringId) {
// this is handled by the recycler view in ContactSelectorFragment
}
@Override
public void hideLoadingScreen() {
// this is handled by the recycler view in ContactSelectorFragment
}
}

View File

@@ -0,0 +1,177 @@
package org.briarproject.android.forum;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.db.DbException;
import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.sync.GroupId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.forum.ShareForumActivity.CONTACTS;
import static org.briarproject.android.forum.ShareForumActivity.getContactsFromIds;
import static org.briarproject.api.forum.ForumConstants.GROUP_ID;
public class ShareForumMessageFragment extends BaseFragment {
private static final Logger LOG =
Logger.getLogger(ShareForumMessageFragment.class.getName());
public final static String TAG = "IntroductionMessageFragment";
private ShareForumActivity shareForumActivity;
private ViewHolder ui;
// Fields that are accessed from background threads must be volatile
@Inject protected volatile ForumSharingManager forumSharingManager;
private volatile GroupId groupId;
private volatile Collection<ContactId> contacts;
public static ShareForumMessageFragment newInstance(GroupId groupId,
Collection<ContactId> contacts) {
ShareForumMessageFragment f = new ShareForumMessageFragment();
Bundle args = new Bundle();
args.putByteArray(GROUP_ID, groupId.getBytes());
args.putIntegerArrayList(CONTACTS, getContactsFromIds(contacts));
f.setArguments(args);
return f;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
shareForumActivity = (ShareForumActivity) context;
} catch (ClassCastException e) {
throw new InstantiationError(
"This fragment is only meant to be attached to the ShareForumActivity");
}
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// change toolbar text
ActionBar actionBar = shareForumActivity.getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(R.string.forum_share_button);
}
// allow for home button to act as back button
setHasOptionsMenu(true);
// inflate view
View v =
inflater.inflate(R.layout.share_forum_message, container,
false);
ui = new ViewHolder(v);
ui.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onButtonClick();
}
});
// get groupID and contactIDs from fragment arguments
groupId = new GroupId(getArguments().getByteArray(GROUP_ID));
ArrayList<Integer> intContacts =
getArguments().getIntegerArrayList(CONTACTS);
if (intContacts == null) throw new IllegalArgumentException();
contacts = ShareForumActivity.getContactsFromIntegers(intContacts);
return v;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
shareForumActivity.onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public String getUniqueTag() {
return TAG;
}
public void onButtonClick() {
// disable button to prevent accidental double invitations
ui.button.setEnabled(false);
String msg = ui.message.getText().toString();
shareForum(msg);
// don't wait for the introduction to be made before finishing activity
shareForumActivity.sharingSuccessful(ui.message);
}
private void shareForum(final String msg) {
shareForumActivity.runOnDbThread(new Runnable() {
public void run() {
try {
for (ContactId c : contacts) {
forumSharingManager
.sendForumInvitation(groupId, c, msg);
}
} catch (DbException e) {
sharingError();
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void sharingError() {
shareForumActivity.runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(shareForumActivity,
R.string.introduction_error, Toast.LENGTH_SHORT)
.show();
}
});
}
private static class ViewHolder {
final private TextView text;
final private EditText message;
final private Button button;
ViewHolder(View v) {
text = (TextView) v.findViewById(R.id.introductionText);
message = (EditText) v.findViewById(R.id.invitationMessageView);
button = (Button) v.findViewById(R.id.shareForumButton);
}
}
}