diff options
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/people')
7 files changed, 2713 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteFragment.java new file mode 100644 index 000000000..32e8d2341 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteFragment.java @@ -0,0 +1,667 @@ +package org.wordpress.android.ui.people; + + +import android.app.Fragment; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.models.Blog; +import org.wordpress.android.models.Role; +import org.wordpress.android.ui.people.utils.PeopleUtils; +import org.wordpress.android.ui.people.utils.PeopleUtils.ValidateUsernameCallback.ValidationResult; +import org.wordpress.android.util.EditTextUtils; +import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.StringUtils; +import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.widgets.MultiUsernameEditText; +import org.wordpress.passcodelock.AppLockManager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class PeopleInviteFragment extends Fragment implements + RoleSelectDialogFragment.OnRoleSelectListener, + PeopleManagementActivity.InvitationSender { + + private static final String FLAG_SUCCESS = "SUCCESS"; + + private static final String ARG_BLOGID = "ARG_BLOGID"; + + private static final int MAX_NUMBER_OF_INVITEES = 10; + private static final String[] USERNAME_DELIMITERS = {" ", ","}; + private final Map<String, ViewGroup> mUsernameButtons = new LinkedHashMap<>(); + private final HashMap<String, String> mUsernameResults = new HashMap<>(); + private final Map<String, View> mUsernameErrorViews = new Hashtable<>(); + private ViewGroup mUsernamesContainer; + private MultiUsernameEditText mUsernameEditText; + private TextView mRoleTextView; + private EditText mCustomMessageEditText; + + private Role mRole; + private String mCustomMessage = ""; + private boolean mInviteOperationInProgress = false; + + public static PeopleInviteFragment newInstance(String dotComBlogId) { + PeopleInviteFragment peopleInviteFragment = new PeopleInviteFragment(); + + Bundle bundle = new Bundle(); + bundle.putString(ARG_BLOGID, dotComBlogId); + + peopleInviteFragment.setArguments(bundle); + return peopleInviteFragment; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.people_invite, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + menu.getItem(0).setEnabled(!mInviteOperationInProgress); // here pass the index of send menu item + super.onPrepareOptionsMenu(menu); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // retain this fragment across configuration changes + // WARNING: use setRetainInstance wisely. In this case we need this to be able to get the + // results of network connections in the same fragment if going through a configuration change + // (for example, device rotation occurs). Given the simplicity of this particular use case + // (the fragment state keeps only a couple of EditText components and the SAVE button, it is + // OK to use it here. + setRetainInstance(true); + } + + @Override + public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + setHasOptionsMenu(true); + return inflater.inflate(R.layout.people_invite_fragment, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mUsernamesContainer = (ViewGroup) view.findViewById(R.id.usernames); + mUsernamesContainer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + EditTextUtils.showSoftInput(mUsernameEditText); + } + }); + + Role role = mRole; + if (role == null) { + role = getDefaultRole(); + } + + mUsernameEditText = (MultiUsernameEditText) view.findViewById(R.id.invite_usernames); + + //handle key preses from hardware keyboard + mUsernameEditText.setOnKeyListener(new View.OnKeyListener() { + @Override + public boolean onKey(View view, int i, KeyEvent keyEvent) { + return keyEvent.getKeyCode() == KeyEvent.KEYCODE_DEL + && keyEvent.getAction() == KeyEvent.ACTION_DOWN + && removeLastEnteredUsername(); + } + }); + + mUsernameEditText.setOnBackspacePressedListener(new MultiUsernameEditText.OnBackspacePressedListener() { + @Override + public boolean onBackspacePressed() { + return removeLastEnteredUsername(); + } + }); + + mUsernameEditText.addTextChangedListener(new TextWatcher() { + private boolean shouldIgnoreChanges = false; + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (shouldIgnoreChanges) { //used to avoid double call after calling setText from this method + return; + } + + shouldIgnoreChanges = true; + if (mUsernameButtons.size() >= MAX_NUMBER_OF_INVITEES && !TextUtils.isEmpty(s)) { + resetEditTextContent(mUsernameEditText); + } else if (endsWithDelimiter(mUsernameEditText.getText().toString())) { + addUsername(mUsernameEditText, null); + } + shouldIgnoreChanges = false; + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + + mUsernameEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE || (event != null && event.getKeyCode() == KeyEvent + .KEYCODE_ENTER)) { + addUsername(mUsernameEditText, null); + return true; + } else { + return false; + } + } + }); + + mUsernameEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (!hasFocus && mUsernameEditText.getText().toString().length() > 0) { + addUsername(mUsernameEditText, null); + } + } + }); + + + if (mUsernameButtons.size() > 0) { + ArrayList<String> usernames = new ArrayList<>(mUsernameButtons.keySet()); + populateUsernameButtons(usernames); + } + + + view.findViewById(R.id.role_container).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + RoleSelectDialogFragment.show(PeopleInviteFragment.this, 0, isPrivateSite()); + } + }); + + mRoleTextView = (TextView) view.findViewById(R.id.role); + setRole(role); + ImageView imgRoleInfo = (ImageView) view.findViewById(R.id.imgRoleInfo); + imgRoleInfo.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Uri uri = Uri.parse(getString(R.string.role_info_url)); + AppLockManager.getInstance().setExtendedTimeout(); + startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + }); + + final int MAX_CHARS = getResources().getInteger(R.integer.invite_message_char_limit); + final TextView remainingCharsTextView = (TextView) view.findViewById(R.id.message_remaining); + + mCustomMessageEditText = (EditText) view.findViewById(R.id.message); + mCustomMessageEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mCustomMessage = mCustomMessageEditText.getText().toString(); + updateRemainingCharsView(remainingCharsTextView, mCustomMessage, MAX_CHARS); + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + updateRemainingCharsView(remainingCharsTextView, mCustomMessage, MAX_CHARS); + } + + private boolean endsWithDelimiter(String string) { + if (TextUtils.isEmpty(string)) { + return false; + } + + for (String usernameDelimiter : USERNAME_DELIMITERS) { + if (string.endsWith(usernameDelimiter)) { + return true; + } + } + + return false; + } + + private String removeDelimiterFromUsername(String username) { + if (TextUtils.isEmpty(username)) { + return username; + } + + String trimmedUsername = username.trim(); + + for (String usernameDelimiter : USERNAME_DELIMITERS) { + if (trimmedUsername.endsWith(usernameDelimiter)) { + return trimmedUsername.substring(0, trimmedUsername.length() - usernameDelimiter.length()); + } + } + + return trimmedUsername; + } + + private void resetEditTextContent(EditText editText) { + if (editText != null) { + editText.setText(""); + } + } + + private Role getDefaultRole() { + Role[] inviteRoles = Role.inviteRoles(isPrivateSite()); + return inviteRoles[0]; + } + + private void updateRemainingCharsView(TextView remainingCharsTextView, String currentString, int limit) { + remainingCharsTextView.setText(StringUtils.getQuantityString(getActivity(), + R.string.invite_message_remaining_zero, + R.string.invite_message_remaining_one, + R.string.invite_message_remaining_other, limit - (currentString == null ? 0 : currentString.length()))); + } + + private void populateUsernameButtons(Collection<String> usernames) { + if (usernames != null && usernames.size() > 0) { + + for (String username : usernames) { + mUsernameButtons.put(username, buttonizeUsername(username)); + } + + validateAndStyleUsername(usernames, null); + } + } + + private ViewGroup buttonizeUsername(final String username) { + if (!isAdded()) { + return null; + } + + final ViewGroup usernameButton = (ViewGroup) LayoutInflater.from(getActivity()).inflate(R.layout + .invite_username_button, null); + final TextView usernameTextView = (TextView) usernameButton.findViewById(R.id.username); + usernameTextView.setText(username); + + mUsernamesContainer.addView(usernameButton, mUsernamesContainer.getChildCount() - 1); + + final ImageButton delete = (ImageButton) usernameButton.findViewById(R.id.username_delete); + delete.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + removeUsername(username); + } + }); + + return usernameButton; + } + + private void addUsername(EditText editText, ValidationEndListener validationEndListener) { + String username = removeDelimiterFromUsername(editText.getText().toString()); + resetEditTextContent(editText); + + if (username.isEmpty() || mUsernameButtons.keySet().contains(username)) { + if (validationEndListener != null) { + validationEndListener.onValidationEnd(); + } + return; + } + + final ViewGroup usernameButton = buttonizeUsername(username); + + mUsernameButtons.put(username, usernameButton); + + validateAndStyleUsername(Collections.singletonList(username), validationEndListener); + } + + private void removeUsername(String username) { + final ViewGroup usernamesView = (ViewGroup) getView().findViewById(R.id.usernames); + + ViewGroup removedButton = mUsernameButtons.remove(username); + mUsernameResults.remove(username); + usernamesView.removeView(removedButton); + + updateUsernameError(username, null); + } + + private boolean isUserInInvitees(String username) { + return mUsernameButtons.get(username) != null; + } + + /** + * Deletes the last entered username. + * + * @return true if the username was deleted + */ + private boolean removeLastEnteredUsername() { + if (!TextUtils.isEmpty(mUsernameEditText.getText())) { + return false; + } + + //try and remove the last entered username + List<String> list = new ArrayList<>(mUsernameButtons.keySet()); + if (!list.isEmpty()) { + String username = list.get(list.size() - 1); + removeUsername(username); + return true; + } + return false; + } + + @Override + public void onRoleSelected(Role newRole) { + setRole(newRole); + + if (!mUsernameButtons.keySet().isEmpty()) { + // clear the username results list and let the 'validate' routine do the updates + mUsernameResults.clear(); + + validateAndStyleUsername(mUsernameButtons.keySet(), null); + } + } + + private void setRole(Role newRole) { + mRole = newRole; + mRoleTextView.setText(newRole.toDisplayString()); + } + + private void validateAndStyleUsername(Collection<String> usernames, final ValidationEndListener validationEndListener) { + List<String> usernamesToCheck = new ArrayList<>(); + + for (String username : usernames) { + if (mUsernameResults.containsKey(username)) { + String resultMessage = mUsernameResults.get(username); + styleButton(username, resultMessage); + updateUsernameError(username, resultMessage); + } else { + styleButton(username, null); + updateUsernameError(username, null); + + usernamesToCheck.add(username); + } + } + + if (usernamesToCheck.size() > 0) { + + String dotComBlogId = getArguments().getString(ARG_BLOGID); + PeopleUtils.validateUsernames(usernamesToCheck, mRole, dotComBlogId, new PeopleUtils.ValidateUsernameCallback() { + @Override + public void onUsernameValidation(String username, ValidationResult validationResult) { + if (!isAdded()) { + return; + } + + if (!isUserInInvitees(username)) { + //user is removed from invitees before validation + return; + } + + final String usernameResultString = getValidationErrorString(username, validationResult); + mUsernameResults.put(username, usernameResultString); + + styleButton(username, usernameResultString); + updateUsernameError(username, usernameResultString); + } + + @Override + public void onValidationFinished() { + if (validationEndListener != null) { + validationEndListener.onValidationEnd(); + } + } + + @Override + public void onError() { + // properly style the button + } + }); + } else { + if (validationEndListener != null) { + validationEndListener.onValidationEnd(); + } + } + } + + private void styleButton(String username, @Nullable String validationResultMessage) { + if (!isAdded()) { + return; + } + + TextView textView = (TextView) mUsernameButtons.get(username).findViewById(R.id.username); + textView.setTextColor(ContextCompat.getColor(getActivity(), + validationResultMessage == null ? R.color.grey_dark : + (validationResultMessage.equals(FLAG_SUCCESS) ? R.color.blue_wordpress : R.color.alert_red))); + } + + private + @Nullable + String getValidationErrorString(String username, ValidationResult validationResult) { + switch (validationResult) { + case USER_NOT_FOUND: + return getString(R.string.invite_username_not_found, username); + case ALREADY_MEMBER: + return getString(R.string.invite_already_a_member, username); + case ALREADY_FOLLOWING: + return getString(R.string.invite_already_following, username); + case BLOCKED_INVITES: + return getString(R.string.invite_user_blocked_invites, username); + case INVALID_EMAIL: + return getString(R.string.invite_invalid_email, username); + case USER_FOUND: + return FLAG_SUCCESS; + } + + return null; + } + + private void updateUsernameError(String username, @Nullable String usernameResult) { + if (!isAdded()) { + return; + } + + TextView usernameErrorTextView; + if (mUsernameErrorViews.containsKey(username)) { + usernameErrorTextView = (TextView) mUsernameErrorViews.get(username); + + if (usernameResult == null || usernameResult.equals(FLAG_SUCCESS)) { + // no error so we need to remove the existing error view + ((ViewGroup) usernameErrorTextView.getParent()).removeView(usernameErrorTextView); + mUsernameErrorViews.remove(username); + return; + } + } else { + if (usernameResult == null || usernameResult.equals(FLAG_SUCCESS)) { + // no error so no need to create a new error view + return; + } + + usernameErrorTextView = (TextView) LayoutInflater.from(getActivity()) + .inflate(R.layout.people_invite_error_view, null); + + final ViewGroup usernameErrorsContainer = (ViewGroup) getView() + .findViewById(R.id.username_errors_container); + usernameErrorsContainer.addView(usernameErrorTextView); + + mUsernameErrorViews.put(username, usernameErrorTextView); + } + usernameErrorTextView.setText(usernameResult); + } + + private void clearUsernames(Collection<String> usernames) { + for (String username : usernames) { + removeUsername(username); + } + + if (mUsernameButtons.size() == 0) { + setRole(getDefaultRole()); + resetEditTextContent(mCustomMessageEditText); + } + } + + @Override + public void send() { + if (!isAdded()) { + return; + } + + if (!NetworkUtils.checkConnection(getActivity())) { + enableSendButton(true); + return; + } + + enableSendButton(false); + + if (mUsernameEditText.getText().toString().length() > 0) { + addUsername(mUsernameEditText, new ValidationEndListener() { + @Override + public void onValidationEnd() { + if (!checkAndSend()) { + //re-enable SEND button if validation failed + enableSendButton(true); + } + } + }); + } else { + if (!checkAndSend()) { + //re-enable SEND button if validation failed + enableSendButton(true); + } + } + } + + /* + * returns true if send is attempted, false if validation failed + * */ + private boolean checkAndSend() { + if (!isAdded()) { + return false; + } + + if (!NetworkUtils.checkConnection(getActivity())) { + return false; + } + + if (mUsernameButtons.size() == 0) { + ToastUtils.showToast(getActivity(), R.string.invite_error_no_usernames); + return false; + } + + int invalidCount = 0; + for (String usernameResultString : mUsernameResults.values()) { + if (!usernameResultString.equals(FLAG_SUCCESS)) { + invalidCount++; + } + } + + if (invalidCount > 0) { + ToastUtils.showToast(getActivity(), StringUtils.getQuantityString(getActivity(), 0, + R.string.invite_error_invalid_usernames_one, + R.string.invite_error_invalid_usernames_multiple, invalidCount)); + return false; + } + + //set the "SEND" option disabled + enableSendButton(false); + + String dotComBlogId = getArguments().getString(ARG_BLOGID); + PeopleUtils.sendInvitations(new ArrayList<>(mUsernameButtons.keySet()), mRole, mCustomMessage, dotComBlogId, + new PeopleUtils.InvitationsSendCallback() { + @Override + public void onSent(List<String> succeededUsernames, Map<String, String> failedUsernameErrors) { + if (!isAdded()) { + return; + } + + clearUsernames(succeededUsernames); + + if (failedUsernameErrors.size() != 0) { + clearUsernames(failedUsernameErrors.keySet()); + + for (Map.Entry<String, String> error : failedUsernameErrors.entrySet()) { + final String username = error.getKey(); + final String errorMessage = error.getValue(); + mUsernameResults.put(username, getString(R.string.invite_error_for_username, + username, errorMessage)); + } + + populateUsernameButtons(failedUsernameErrors.keySet()); + + ToastUtils.showToast(getActivity(), succeededUsernames.isEmpty() + ? R.string.invite_error_sending : R.string.invite_error_some_failed); + } else { + ToastUtils.showToast(getActivity(), R.string.invite_sent, ToastUtils.Duration.LONG); + } + + //set the "SEND" option enabled again + enableSendButton(true); + } + + @Override + public void onError() { + if (!isAdded()) { + return; + } + + ToastUtils.showToast(getActivity(), R.string.invite_error_sending); + + //set the "SEND" option enabled again + enableSendButton(true); + + } + }); + + return true; + } + + private void enableSendButton(boolean enable) { + mInviteOperationInProgress = !enable; + if (getActivity() != null) { + getActivity().invalidateOptionsMenu(); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + //we need to remove focus listener when view is destroyed (ex. orientation change) to prevent mUsernameEditText + //content from being converted to username + if (mUsernameEditText != null) { + mUsernameEditText.setOnFocusChangeListener(null); + } + } + + private boolean isPrivateSite() { + String dotComBlogId = getArguments().getString(ARG_BLOGID); + Blog blog = WordPress.wpDB.getBlogForDotComBlogId(dotComBlogId); + return blog != null && blog.isPrivate(); + } + + public interface ValidationEndListener { + void onValidationEnd(); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleListFragment.java new file mode 100644 index 000000000..ca878b3cf --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleListFragment.java @@ -0,0 +1,432 @@ +package org.wordpress.android.ui.people; + +import android.app.Fragment; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.datasets.PeopleTable; +import org.wordpress.android.models.Blog; +import org.wordpress.android.models.FilterCriteria; +import org.wordpress.android.models.PeopleListFilter; +import org.wordpress.android.models.Person; +import org.wordpress.android.ui.EmptyViewMessageType; +import org.wordpress.android.ui.FilteredRecyclerView; +import org.wordpress.android.ui.prefs.AppPrefs; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.GravatarUtils; +import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.StringUtils; +import org.wordpress.android.widgets.WPNetworkImageView; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class PeopleListFragment extends Fragment { + private static final String ARG_LOCAL_TABLE_BLOG_ID = "local_table_blog_id"; + + private int mLocalTableBlogID; + private OnPersonSelectedListener mOnPersonSelectedListener; + private OnFetchPeopleListener mOnFetchPeopleListener; + + private FilteredRecyclerView mFilteredRecyclerView; + private PeopleListFilter mPeopleListFilter; + + public static PeopleListFragment newInstance(int localTableBlogID) { + PeopleListFragment peopleListFragment = new PeopleListFragment(); + Bundle bundle = new Bundle(); + bundle.putInt(ARG_LOCAL_TABLE_BLOG_ID, localTableBlogID); + peopleListFragment.setArguments(bundle); + return peopleListFragment; + } + + public void setOnPersonSelectedListener(OnPersonSelectedListener listener) { + mOnPersonSelectedListener = listener; + } + + public void setOnFetchPeopleListener(OnFetchPeopleListener listener) { + mOnFetchPeopleListener = listener; + } + + @Override + public void onDetach() { + super.onDetach(); + mOnPersonSelectedListener = null; + mOnFetchPeopleListener = null; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.people_list, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + setHasOptionsMenu(true); + + final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.people_list_fragment, container, false); + + mLocalTableBlogID = getArguments().getInt(ARG_LOCAL_TABLE_BLOG_ID); + final Blog blog = WordPress.getBlog(mLocalTableBlogID); + final boolean isPrivate = blog != null && blog.isPrivate(); + + mFilteredRecyclerView = (FilteredRecyclerView) rootView.findViewById(R.id.filtered_recycler_view); + mFilteredRecyclerView.addItemDecoration(new PeopleItemDecoration(getActivity(), R.drawable.people_list_divider)); + mFilteredRecyclerView.setLogT(AppLog.T.PEOPLE); + mFilteredRecyclerView.setSwipeToRefreshEnabled(false); + + // the following will change the look and feel of the toolbar to match the current design + mFilteredRecyclerView.setToolbarBackgroundColor(ContextCompat.getColor(getActivity(), R.color.blue_medium)); + mFilteredRecyclerView.setToolbarSpinnerTextColor(ContextCompat.getColor(getActivity(), R.color.white)); + mFilteredRecyclerView.setToolbarSpinnerDrawable(R.drawable.arrow); + mFilteredRecyclerView.setToolbarLeftAndRightPadding( + getResources().getDimensionPixelSize(R.dimen.margin_filter_spinner), + getResources().getDimensionPixelSize(R.dimen.margin_none)); + + mFilteredRecyclerView.setFilterListener(new FilteredRecyclerView.FilterListener() { + @Override + public List<FilterCriteria> onLoadFilterCriteriaOptions(boolean refresh) { + ArrayList<FilterCriteria> list = new ArrayList<>(); + Collections.addAll(list, PeopleListFilter.values()); + // Only a private blog can have viewers + if (!isPrivate) { + list.remove(PeopleListFilter.VIEWERS); + } + return list; + } + + @Override + public void onLoadFilterCriteriaOptionsAsync(FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener, boolean refresh) { + // no-op + } + + @Override + public FilterCriteria onRecallSelection() { + mPeopleListFilter = AppPrefs.getPeopleListFilter(); + + // if viewers is not available for this blog, set the filter to TEAM + if (mPeopleListFilter == PeopleListFilter.VIEWERS && !isPrivate) { + mPeopleListFilter = PeopleListFilter.TEAM; + AppPrefs.setPeopleListFilter(mPeopleListFilter); + } + return mPeopleListFilter; + } + + @Override + public void onLoadData() { + updatePeople(false); + } + + @Override + public void onFilterSelected(int position, FilterCriteria criteria) { + mPeopleListFilter = (PeopleListFilter) criteria; + AppPrefs.setPeopleListFilter(mPeopleListFilter); + } + + @Override + public String onShowEmptyViewMessage(EmptyViewMessageType emptyViewMsgType) { + int stringId = 0; + switch (emptyViewMsgType) { + case LOADING: + stringId = R.string.people_fetching; + break; + case NETWORK_ERROR: + stringId = R.string.no_network_message; + break; + case NO_CONTENT: + switch (mPeopleListFilter) { + case TEAM: + stringId = R.string.people_empty_list_filtered_users; + break; + case FOLLOWERS: + stringId = R.string.people_empty_list_filtered_followers; + break; + case EMAIL_FOLLOWERS: + stringId = R.string.people_empty_list_filtered_email_followers; + break; + case VIEWERS: + stringId = R.string.people_empty_list_filtered_viewers; + break; + } + break; + case GENERIC_ERROR: + switch (mPeopleListFilter) { + case TEAM: + stringId = R.string.error_fetch_users_list; + break; + case FOLLOWERS: + stringId = R.string.error_fetch_followers_list; + break; + case EMAIL_FOLLOWERS: + stringId = R.string.error_fetch_email_followers_list; + break; + case VIEWERS: + stringId = R.string.error_fetch_viewers_list; + break; + } + break; + } + return getString(stringId); + } + + @Override + public void onShowCustomEmptyView(EmptyViewMessageType emptyViewMsgType) { + + } + }); + + return rootView; + } + + @Override + public void onResume() { + super.onResume(); + + updatePeople(false); + } + + private void updatePeople(boolean loadMore) { + if (!NetworkUtils.isNetworkAvailable(getActivity())) { + mFilteredRecyclerView.updateEmptyView(EmptyViewMessageType.NETWORK_ERROR); + mFilteredRecyclerView.setRefreshing(false); + return; + } + + if (mOnFetchPeopleListener != null) { + if (loadMore) { + boolean isFetching = mOnFetchPeopleListener.onFetchMorePeople(mPeopleListFilter); + if (isFetching) { + mFilteredRecyclerView.showLoadingProgress(); + } + } else { + boolean isFetching = mOnFetchPeopleListener.onFetchFirstPage(mPeopleListFilter); + if (isFetching) { + mFilteredRecyclerView.updateEmptyView(EmptyViewMessageType.LOADING); + } else { + mFilteredRecyclerView.hideEmptyView(); + mFilteredRecyclerView.setRefreshing(false); + } + refreshPeopleList(isFetching); + } + } + } + + public void refreshPeopleList(boolean isFetching) { + if (!isAdded()) return; + + List<Person> peopleList; + switch (mPeopleListFilter) { + case TEAM: + peopleList = PeopleTable.getUsers(mLocalTableBlogID); + break; + case FOLLOWERS: + peopleList = PeopleTable.getFollowers(mLocalTableBlogID); + break; + case EMAIL_FOLLOWERS: + peopleList = PeopleTable.getEmailFollowers(mLocalTableBlogID); + break; + case VIEWERS: + peopleList = PeopleTable.getViewers(mLocalTableBlogID); + break; + default: + peopleList = new ArrayList<>(); + break; + } + PeopleAdapter peopleAdapter = (PeopleAdapter) mFilteredRecyclerView.getAdapter(); + if (peopleAdapter == null) { + peopleAdapter = new PeopleAdapter(getActivity(), peopleList); + mFilteredRecyclerView.setAdapter(peopleAdapter); + } else { + peopleAdapter.setPeopleList(peopleList); + } + + if (!peopleList.isEmpty()) { + // if the list is not empty, don't show any message + mFilteredRecyclerView.hideEmptyView(); + } else if (!isFetching) { + // if we are not fetching and list is empty, show no content message + mFilteredRecyclerView.updateEmptyView(EmptyViewMessageType.NO_CONTENT); + } + } + + public void fetchingRequestFinished(PeopleListFilter filter, boolean isFirstPage, boolean isSuccessful) { + if (mPeopleListFilter == filter) { + if (isFirstPage) { + mFilteredRecyclerView.setRefreshing(false); + if (!isSuccessful) { + mFilteredRecyclerView.updateEmptyView(EmptyViewMessageType.GENERIC_ERROR); + } + } else { + mFilteredRecyclerView.hideLoadingProgress(); + } + } + } + + // Container Activity must implement this interface + public interface OnPersonSelectedListener { + void onPersonSelected(Person person); + } + + public interface OnFetchPeopleListener { + boolean onFetchFirstPage(PeopleListFilter filter); + + boolean onFetchMorePeople(PeopleListFilter filter); + } + + public class PeopleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { + private final LayoutInflater mInflater; + private List<Person> mPeopleList; + private int mAvatarSz; + + public PeopleAdapter(Context context, List<Person> peopleList) { + mAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.people_avatar_sz); + mInflater = LayoutInflater.from(context); + mPeopleList = peopleList; + setHasStableIds(true); + } + + public void setPeopleList(List<Person> peopleList) { + mPeopleList = peopleList; + notifyDataSetChanged(); + } + + public Person getPerson(int position) { + if (mPeopleList == null) { + return null; + } + return mPeopleList.get(position); + } + + @Override + public int getItemCount() { + if (mPeopleList == null) { + return 0; + } + return mPeopleList.size(); + } + + @Override + public long getItemId(int position) { + Person person = getPerson(position); + if (person == null) { + return -1; + } + return person.getPersonID(); + } + + @Override + public PeopleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = mInflater.inflate(R.layout.people_list_row, parent, false); + + return new PeopleViewHolder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + PeopleViewHolder peopleViewHolder = (PeopleViewHolder) holder; + final Person person = getPerson(position); + + if (person != null) { + String avatarUrl = GravatarUtils.fixGravatarUrl(person.getAvatarUrl(), mAvatarSz); + peopleViewHolder.imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR); + peopleViewHolder.txtDisplayName.setText(StringUtils.unescapeHTML(person.getDisplayName())); + if (person.getRole() != null) { + peopleViewHolder.txtRole.setVisibility(View.VISIBLE); + peopleViewHolder.txtRole.setText(StringUtils.capitalize(person.getRole().toDisplayString())); + } else { + peopleViewHolder.txtRole.setVisibility(View.GONE); + } + if (!person.getUsername().isEmpty()) { + peopleViewHolder.txtUsername.setVisibility(View.VISIBLE); + peopleViewHolder.txtUsername.setText(String.format("@%s", person.getUsername())); + } else { + peopleViewHolder.txtUsername.setVisibility(View.GONE); + } + if (person.getPersonType() == Person.PersonType.USER + || person.getPersonType() == Person.PersonType.VIEWER) { + peopleViewHolder.txtSubscribed.setVisibility(View.GONE); + } else { + peopleViewHolder.txtSubscribed.setVisibility(View.VISIBLE); + String dateSubscribed = SimpleDateFormat.getDateInstance().format(person.getDateSubscribed()); + String dateText = getString(R.string.follower_subscribed_since, dateSubscribed); + peopleViewHolder.txtSubscribed.setText(dateText); + } + } + + // end of list is reached + if (position == getItemCount() - 1) { + updatePeople(true); + } + } + + public class PeopleViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + private final WPNetworkImageView imgAvatar; + private final TextView txtDisplayName; + private final TextView txtUsername; + private final TextView txtRole; + private final TextView txtSubscribed; + + public PeopleViewHolder(View view) { + super(view); + imgAvatar = (WPNetworkImageView) view.findViewById(R.id.person_avatar); + txtDisplayName = (TextView) view.findViewById(R.id.person_display_name); + txtUsername = (TextView) view.findViewById(R.id.person_username); + txtRole = (TextView) view.findViewById(R.id.person_role); + txtSubscribed = (TextView) view.findViewById(R.id.follower_subscribed_date); + + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + if (mOnPersonSelectedListener != null) { + Person person = getPerson(getAdapterPosition()); + mOnPersonSelectedListener.onPersonSelected(person); + } + } + } + } + + // Taken from http://stackoverflow.com/a/27037230 + private class PeopleItemDecoration extends RecyclerView.ItemDecoration { + private Drawable mDivider; + + // use a custom drawable + public PeopleItemDecoration(Context context, int resId) { + mDivider = ContextCompat.getDrawable(context, resId); + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + int left = parent.getPaddingLeft(); + int right = parent.getWidth() - parent.getPaddingRight(); + + int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = parent.getChildAt(i); + + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + + int top = child.getBottom() + params.bottomMargin; + int bottom = top + mDivider.getIntrinsicHeight(); + + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java new file mode 100644 index 000000000..3c0e43c70 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java @@ -0,0 +1,664 @@ +package org.wordpress.android.ui.people; + +import android.app.AlertDialog; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.datasets.PeopleTable; +import org.wordpress.android.models.Blog; +import org.wordpress.android.models.PeopleListFilter; +import org.wordpress.android.models.Person; +import org.wordpress.android.ui.people.utils.PeopleUtils; +import org.wordpress.android.util.AnalyticsUtils; +import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.ToastUtils; + +import java.util.List; + +import de.greenrobot.event.EventBus; + + +public class PeopleManagementActivity extends AppCompatActivity + implements PeopleListFragment.OnPersonSelectedListener, PeopleListFragment.OnFetchPeopleListener { + private static final String KEY_PEOPLE_LIST_FRAGMENT = "people-list-fragment"; + private static final String KEY_PERSON_DETAIL_FRAGMENT = "person-detail-fragment"; + private static final String KEY_PEOPLE_INVITE_FRAGMENT = "people-invite-fragment"; + private static final String KEY_TITLE = "page-title"; + + private static final String KEY_USERS_END_OF_LIST_REACHED = "users-end-of-list-reached"; + private static final String KEY_FOLLOWERS_END_OF_LIST_REACHED = "followers-end-of-list-reached"; + private static final String KEY_EMAIL_FOLLOWERS_END_OF_LIST_REACHED = "email-followers-end-of-list-reached"; + private static final String KEY_VIEWERS_END_OF_LIST_REACHED = "viewers-end-of-list-reached"; + + private static final String KEY_USERS_FETCH_REQUEST_IN_PROGRESS = "users-fetch-request-in-progress"; + private static final String KEY_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS = "followers-fetch-request-in-progress"; + private static final String KEY_EMAIL_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS = "email-followers-fetch-request-in-progress"; + private static final String KEY_VIEWERS_FETCH_REQUEST_IN_PROGRESS = "viewers-fetch-request-in-progress"; + + private static final String KEY_HAS_REFRESHED_USERS = "has-refreshed-users"; + private static final String KEY_HAS_REFRESHED_FOLLOWERS = "has-refreshed-followers"; + private static final String KEY_HAS_REFRESHED_EMAIL_FOLLOWERS = "has-refreshed-email-followers"; + private static final String KEY_HAS_REFRESHED_VIEWERS = "has-refreshed-viewers"; + + private static final String KEY_FOLLOWERS_LAST_FETCHED_PAGE = "followers-last-fetched-page"; + private static final String KEY_EMAIL_FOLLOWERS_LAST_FETCHED_PAGE = "email-followers-last-fetched-page"; + + // End of list reached variables will be true when there is no more data to fetch + private boolean mUsersEndOfListReached; + private boolean mFollowersEndOfListReached; + private boolean mEmailFollowersEndOfListReached; + private boolean mViewersEndOfListReached; + + // We only allow the lists to be refreshed once to avoid syncing and jumping animation issues + private boolean mHasRefreshedUsers; + private boolean mHasRefreshedFollowers; + private boolean mHasRefreshedEmailFollowers; + private boolean mHasRefreshedViewers; + + // If we are currently making a request for a certain filter + private boolean mUsersFetchRequestInProgress; + private boolean mFollowersFetchRequestInProgress; + private boolean mEmailFollowersFetchRequestInProgress; + private boolean mViewersFetchRequestInProgress; + + // Keep track of the last page we received from remote + private int mFollowersLastFetchedPage; + private int mEmailFollowersLastFetchedPage; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.people_management_activity); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setElevation(0); + } + + Blog blog = WordPress.getCurrentBlog(); + if (blog == null) { + ToastUtils.showToast(this, R.string.blog_not_found); + finish(); + return; + } + + FragmentManager fragmentManager = getFragmentManager(); + + if (savedInstanceState == null) { + // only delete cached people if there is a connection + if (NetworkUtils.isNetworkAvailable(this)) { + PeopleTable.deletePeopleExceptForFirstPage(blog.getLocalTableBlogId()); + } + + if (actionBar != null) { + actionBar.setTitle(R.string.people); + } + + PeopleListFragment peopleListFragment = PeopleListFragment.newInstance(blog.getLocalTableBlogId()); + peopleListFragment.setOnPersonSelectedListener(this); + peopleListFragment.setOnFetchPeopleListener(this); + + mUsersEndOfListReached = false; + mFollowersEndOfListReached = false; + mEmailFollowersEndOfListReached = false; + mViewersEndOfListReached = false; + + mHasRefreshedUsers = false; + mHasRefreshedFollowers = false; + mHasRefreshedEmailFollowers = false; + mHasRefreshedViewers = false; + + mUsersFetchRequestInProgress = false; + mFollowersFetchRequestInProgress = false; + mEmailFollowersFetchRequestInProgress = false; + mViewersFetchRequestInProgress = false; + mFollowersLastFetchedPage = 0; + mEmailFollowersLastFetchedPage = 0; + + + fragmentManager.beginTransaction() + .add(R.id.fragment_container, peopleListFragment, KEY_PEOPLE_LIST_FRAGMENT) + .commit(); + } else { + mUsersEndOfListReached = savedInstanceState.getBoolean(KEY_USERS_END_OF_LIST_REACHED); + mFollowersEndOfListReached = savedInstanceState.getBoolean(KEY_FOLLOWERS_END_OF_LIST_REACHED); + mEmailFollowersEndOfListReached = savedInstanceState.getBoolean(KEY_EMAIL_FOLLOWERS_END_OF_LIST_REACHED); + mViewersEndOfListReached = savedInstanceState.getBoolean(KEY_VIEWERS_END_OF_LIST_REACHED); + + mHasRefreshedUsers = savedInstanceState.getBoolean(KEY_HAS_REFRESHED_USERS); + mHasRefreshedFollowers = savedInstanceState.getBoolean(KEY_HAS_REFRESHED_FOLLOWERS); + mHasRefreshedEmailFollowers = savedInstanceState.getBoolean(KEY_HAS_REFRESHED_EMAIL_FOLLOWERS); + mHasRefreshedViewers = savedInstanceState.getBoolean(KEY_HAS_REFRESHED_VIEWERS); + + mUsersFetchRequestInProgress = savedInstanceState.getBoolean(KEY_USERS_FETCH_REQUEST_IN_PROGRESS); + mFollowersFetchRequestInProgress = savedInstanceState.getBoolean(KEY_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS); + mEmailFollowersFetchRequestInProgress = savedInstanceState.getBoolean(KEY_EMAIL_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS); + mViewersFetchRequestInProgress = savedInstanceState.getBoolean(KEY_VIEWERS_FETCH_REQUEST_IN_PROGRESS); + + mFollowersLastFetchedPage = savedInstanceState.getInt(KEY_FOLLOWERS_LAST_FETCHED_PAGE); + mEmailFollowersLastFetchedPage = savedInstanceState.getInt(KEY_EMAIL_FOLLOWERS_LAST_FETCHED_PAGE); + + CharSequence title = savedInstanceState.getCharSequence(KEY_TITLE); + if (actionBar != null && title != null) { + actionBar.setTitle(title); + } + + PeopleListFragment peopleListFragment = getListFragment(); + if (peopleListFragment != null) { + peopleListFragment.setOnPersonSelectedListener(this); + peopleListFragment.setOnFetchPeopleListener(this); + } + } + } + + @Override + public void onSaveInstanceState(Bundle outState){ + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_USERS_END_OF_LIST_REACHED, mUsersEndOfListReached); + outState.putBoolean(KEY_FOLLOWERS_END_OF_LIST_REACHED, mFollowersEndOfListReached); + outState.putBoolean(KEY_EMAIL_FOLLOWERS_END_OF_LIST_REACHED, mEmailFollowersEndOfListReached); + outState.putBoolean(KEY_VIEWERS_END_OF_LIST_REACHED, mViewersEndOfListReached); + + outState.putBoolean(KEY_HAS_REFRESHED_USERS, mHasRefreshedUsers); + outState.putBoolean(KEY_HAS_REFRESHED_FOLLOWERS, mHasRefreshedFollowers); + outState.putBoolean(KEY_HAS_REFRESHED_EMAIL_FOLLOWERS, mHasRefreshedEmailFollowers); + outState.putBoolean(KEY_HAS_REFRESHED_VIEWERS, mHasRefreshedViewers); + + outState.putBoolean(KEY_USERS_FETCH_REQUEST_IN_PROGRESS, mUsersFetchRequestInProgress); + outState.putBoolean(KEY_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS, mFollowersFetchRequestInProgress); + outState.putBoolean(KEY_EMAIL_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS, mEmailFollowersFetchRequestInProgress); + outState.putBoolean(KEY_VIEWERS_FETCH_REQUEST_IN_PROGRESS, mViewersFetchRequestInProgress); + + outState.putInt(KEY_FOLLOWERS_LAST_FETCHED_PAGE, mFollowersLastFetchedPage); + outState.putInt(KEY_EMAIL_FOLLOWERS_LAST_FETCHED_PAGE, mEmailFollowersLastFetchedPage); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + outState.putCharSequence(KEY_TITLE, actionBar.getTitle()); + } + } + + @Override + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + EventBus.getDefault().unregister(this); + super.onStop(); + } + + @Override + public void onBackPressed() { + if (!navigateBackToPeopleListFragment()) { + super.onBackPressed(); + } + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } else if (item.getItemId() == R.id.remove_person) { + confirmRemovePerson(); + return true; + } else if (item.getItemId() == R.id.invite) { + FragmentManager fragmentManager = getFragmentManager(); + Fragment peopleInviteFragment = fragmentManager.findFragmentByTag(KEY_PERSON_DETAIL_FRAGMENT); + + if (peopleInviteFragment == null) { + Blog blog = WordPress.getCurrentBlog(); + peopleInviteFragment = PeopleInviteFragment.newInstance(blog.getDotComBlogId()); + } + if (!peopleInviteFragment.isAdded()) { + FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.fragment_container, peopleInviteFragment, KEY_PEOPLE_INVITE_FRAGMENT); + fragmentTransaction.addToBackStack(null); + fragmentTransaction.commit(); + } + } else if (item.getItemId() == R.id.send_invitation) { + FragmentManager fragmentManager = getFragmentManager(); + Fragment peopleInviteFragment = fragmentManager.findFragmentByTag(KEY_PEOPLE_INVITE_FRAGMENT); + if (peopleInviteFragment != null) { + ((InvitationSender) peopleInviteFragment).send(); + } + } + return super.onOptionsItemSelected(item); + } + + private boolean fetchUsersList(String dotComBlogId, final int localTableBlogId, final int offset) { + if (mUsersEndOfListReached || mUsersFetchRequestInProgress || !NetworkUtils.checkConnection(this)) { + return false; + } + + mUsersFetchRequestInProgress = true; + + PeopleUtils.fetchUsers(dotComBlogId, localTableBlogId, offset, new PeopleUtils.FetchUsersCallback() { + @Override + public void onSuccess(List<Person> peopleList, boolean isEndOfList) { + boolean isFreshList = (offset == 0); + mHasRefreshedUsers = true; + mUsersEndOfListReached = isEndOfList; + PeopleTable.saveUsers(peopleList, localTableBlogId, isFreshList); + + PeopleListFragment peopleListFragment = getListFragment(); + if (peopleListFragment != null) { + peopleListFragment.fetchingRequestFinished(PeopleListFilter.TEAM, isFreshList, true); + } + + refreshOnScreenFragmentDetails(); + mUsersFetchRequestInProgress = false; + } + + @Override + public void onError() { + PeopleListFragment peopleListFragment = getListFragment(); + if (peopleListFragment != null) { + boolean isFirstPage = offset == 0; + peopleListFragment.fetchingRequestFinished(PeopleListFilter.TEAM, isFirstPage, false); + } + mUsersFetchRequestInProgress = false; + ToastUtils.showToast(PeopleManagementActivity.this, + R.string.error_fetch_users_list, + ToastUtils.Duration.SHORT); + } + }); + + return true; + } + + private boolean fetchFollowersList(String dotComBlogId, final int localTableBlogId, final int page) { + if (mFollowersEndOfListReached || mFollowersFetchRequestInProgress || !NetworkUtils.checkConnection(this)) { + return false; + } + + mFollowersFetchRequestInProgress = true; + + PeopleUtils.fetchFollowers(dotComBlogId, localTableBlogId, page, new PeopleUtils.FetchFollowersCallback() { + @Override + public void onSuccess(List<Person> peopleList, int pageFetched, boolean isEndOfList) { + boolean isFreshList = (page == 1); + mHasRefreshedFollowers = true; + mFollowersLastFetchedPage = pageFetched; + mFollowersEndOfListReached = isEndOfList; + PeopleTable.saveFollowers(peopleList, localTableBlogId, isFreshList); + + PeopleListFragment peopleListFragment = getListFragment(); + if (peopleListFragment != null) { + peopleListFragment.fetchingRequestFinished(PeopleListFilter.FOLLOWERS, isFreshList, true); + } + + refreshOnScreenFragmentDetails(); + mFollowersFetchRequestInProgress = false; + } + + @Override + public void onError() { + PeopleListFragment peopleListFragment = getListFragment(); + if (peopleListFragment != null) { + boolean isFirstPage = page == 1; + peopleListFragment.fetchingRequestFinished(PeopleListFilter.FOLLOWERS, isFirstPage, false); + } + mFollowersFetchRequestInProgress = false; + ToastUtils.showToast(PeopleManagementActivity.this, + R.string.error_fetch_followers_list, + ToastUtils.Duration.SHORT); + } + }); + + return true; + } + + private boolean fetchEmailFollowersList(String dotComBlogId, final int localTableBlogId, final int page) { + if (mEmailFollowersEndOfListReached || mEmailFollowersFetchRequestInProgress || !NetworkUtils.checkConnection(this)) { + return false; + } + + mEmailFollowersFetchRequestInProgress = true; + + PeopleUtils.fetchEmailFollowers(dotComBlogId, localTableBlogId, page, new PeopleUtils.FetchFollowersCallback() { + @Override + public void onSuccess(List<Person> peopleList, int pageFetched, boolean isEndOfList) { + boolean isFreshList = (page == 1); + mHasRefreshedEmailFollowers = true; + mEmailFollowersLastFetchedPage = pageFetched; + mEmailFollowersEndOfListReached = isEndOfList; + PeopleTable.saveEmailFollowers(peopleList, localTableBlogId, isFreshList); + + PeopleListFragment peopleListFragment = getListFragment(); + if (peopleListFragment != null) { + peopleListFragment.fetchingRequestFinished(PeopleListFilter.EMAIL_FOLLOWERS, isFreshList, true); + } + + refreshOnScreenFragmentDetails(); + mEmailFollowersFetchRequestInProgress = false; + } + + @Override + public void onError() { + PeopleListFragment peopleListFragment = getListFragment(); + if (peopleListFragment != null) { + boolean isFirstPage = page == 1; + peopleListFragment.fetchingRequestFinished(PeopleListFilter.EMAIL_FOLLOWERS, isFirstPage, false); + } + mEmailFollowersFetchRequestInProgress = false; + ToastUtils.showToast(PeopleManagementActivity.this, + R.string.error_fetch_email_followers_list, + ToastUtils.Duration.SHORT); + } + }); + + return true; + } + + private boolean fetchViewersList(String dotComBlogId, final int localTableBlogId, final int offset) { + if (mViewersEndOfListReached || mViewersFetchRequestInProgress || !NetworkUtils.checkConnection(this)) { + return false; + } + + mViewersFetchRequestInProgress = true; + + PeopleUtils.fetchViewers(dotComBlogId, localTableBlogId, offset, new PeopleUtils.FetchViewersCallback() { + @Override + public void onSuccess(List<Person> peopleList, boolean isEndOfList) { + boolean isFreshList = (offset == 0); + mHasRefreshedViewers = true; + mViewersEndOfListReached = isEndOfList; + PeopleTable.saveViewers(peopleList, localTableBlogId, isFreshList); + + PeopleListFragment peopleListFragment = getListFragment(); + if (peopleListFragment != null) { + peopleListFragment.fetchingRequestFinished(PeopleListFilter.VIEWERS, isFreshList, true); + } + + refreshOnScreenFragmentDetails(); + mViewersFetchRequestInProgress = false; + } + + @Override + public void onError() { + PeopleListFragment peopleListFragment = getListFragment(); + if (peopleListFragment != null) { + boolean isFirstPage = offset == 0; + peopleListFragment.fetchingRequestFinished(PeopleListFilter.VIEWERS, isFirstPage, false); + } + mViewersFetchRequestInProgress = false; + ToastUtils.showToast(PeopleManagementActivity.this, + R.string.error_fetch_viewers_list, + ToastUtils.Duration.SHORT); + } + }); + + return true; + } + + @Override + public void onPersonSelected(Person person) { + PersonDetailFragment personDetailFragment = getDetailFragment(); + + long personID = person.getPersonID(); + int localTableBlogID = person.getLocalTableBlogId(); + + if (personDetailFragment == null) { + personDetailFragment = PersonDetailFragment.newInstance(personID, localTableBlogID, person.getPersonType()); + } else { + personDetailFragment.setPersonDetails(personID, localTableBlogID); + } + if (!personDetailFragment.isAdded()) { + AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_PERSON); + FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.fragment_container, personDetailFragment, KEY_PERSON_DETAIL_FRAGMENT); + fragmentTransaction.addToBackStack(null); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(""); + } + + fragmentTransaction.commit(); + } + } + + public void onEventMainThread(RoleChangeDialogFragment.RoleChangeEvent event) { + if(!NetworkUtils.checkConnection(this)) { + return; + } + + final Person person = PeopleTable.getUser(event.personID, event.localTableBlogId); + if (person == null || event.newRole == null || person.getRole() == event.newRole) { + return; + } + + String blogId = WordPress.getCurrentRemoteBlogId(); + if (blogId == null) { + return; + } + + final PersonDetailFragment personDetailFragment = getDetailFragment(); + if (personDetailFragment != null) { + // optimistically update the role + personDetailFragment.changeRole(event.newRole); + } + + PeopleUtils.updateRole(blogId, person.getPersonID(), event.newRole, event.localTableBlogId, + new PeopleUtils.UpdateUserCallback() { + @Override + public void onSuccess(Person person) { + AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.PERSON_UPDATED); + PeopleTable.saveUser(person); + refreshOnScreenFragmentDetails(); + } + + @Override + public void onError() { + // change the role back to it's original value + if (personDetailFragment != null) { + personDetailFragment.refreshPersonDetails(); + } + ToastUtils.showToast(PeopleManagementActivity.this, + R.string.error_update_role, + ToastUtils.Duration.LONG); + } + }); + } + + private void confirmRemovePerson() { + Person person = getCurrentPerson(); + if (person == null) { + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Calypso_AlertDialog); + builder.setTitle(getString(R.string.person_remove_confirmation_title, person.getDisplayName())); + if (person.getPersonType() == Person.PersonType.USER) { + builder.setMessage(getString(R.string.user_remove_confirmation_message, person.getDisplayName())); + } else if(person.getPersonType() == Person.PersonType.VIEWER) { + builder.setMessage(R.string.viewer_remove_confirmation_message); + } else { + builder.setMessage(R.string.follower_remove_confirmation_message); + } + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.remove, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + removeSelectedPerson(); + } + }); + builder.show(); + } + + private void removeSelectedPerson() { + if(!NetworkUtils.checkConnection(this)) { + return; + } + + Person person = getCurrentPerson(); + if (person == null) { + return; + } + String blogId = WordPress.getCurrentRemoteBlogId(); + if (blogId == null) { + return; + } + + final Person.PersonType personType = person.getPersonType(); + final String displayName = person.getDisplayName(); + + PeopleUtils.RemovePersonCallback callback = new PeopleUtils.RemovePersonCallback() { + @Override + public void onSuccess(long personID, int localTableBlogId) { + if (personType == Person.PersonType.USER) { + AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.PERSON_REMOVED); + } + + // remove the person from db, navigate back to list fragment and refresh it + PeopleTable.deletePerson(personID, localTableBlogId, personType); + + String message = getString(R.string.person_removed, displayName); + ToastUtils.showToast(PeopleManagementActivity.this, message, ToastUtils.Duration.LONG); + + navigateBackToPeopleListFragment(); + refreshPeopleListFragment(); + } + + @Override + public void onError() { + int errorMessageRes; + switch (personType) { + case USER: + errorMessageRes = R.string.error_remove_user; + break; + case VIEWER: + errorMessageRes = R.string.error_remove_viewer; + break; + default: + errorMessageRes = R.string.error_remove_follower; + break; + } + ToastUtils.showToast(PeopleManagementActivity.this, + errorMessageRes, + ToastUtils.Duration.LONG); + } + }; + + if (personType == Person.PersonType.FOLLOWER || personType == Person.PersonType.EMAIL_FOLLOWER) { + PeopleUtils.removeFollower(blogId, person.getPersonID(), person.getLocalTableBlogId(), + personType, callback); + } else if(personType == Person.PersonType.VIEWER) { + PeopleUtils.removeViewer(blogId, person.getPersonID(), person.getLocalTableBlogId(), callback); + } else { + PeopleUtils.removeUser(blogId, person.getPersonID(), person.getLocalTableBlogId(), callback); + } + } + + // This helper method is used after a successful network request + private void refreshOnScreenFragmentDetails() { + refreshPeopleListFragment(); + refreshDetailFragment(); + } + + private void refreshPeopleListFragment() { + PeopleListFragment peopleListFragment = getListFragment(); + if (peopleListFragment != null) { + peopleListFragment.refreshPeopleList(false); + } + } + + private void refreshDetailFragment() { + PersonDetailFragment personDetailFragment = getDetailFragment(); + if (personDetailFragment != null) { + personDetailFragment.refreshPersonDetails(); + } + } + + private boolean navigateBackToPeopleListFragment() { + FragmentManager fragmentManager = getFragmentManager(); + if (fragmentManager.getBackStackEntryCount() > 0) { + fragmentManager.popBackStack(); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.people); + } + return true; + } + return false; + } + + private Person getCurrentPerson() { + PersonDetailFragment personDetailFragment = getDetailFragment(); + + if (personDetailFragment == null) { + return null; + } + + return personDetailFragment.loadPerson(); + } + + @Override + public boolean onFetchFirstPage(PeopleListFilter filter) { + Blog blog = WordPress.getCurrentBlog(); + if (filter == PeopleListFilter.TEAM && !mHasRefreshedUsers) { + return fetchUsersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), 0); + } else if (filter == PeopleListFilter.FOLLOWERS && !mHasRefreshedFollowers) { + return fetchFollowersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), 1); + } else if (filter == PeopleListFilter.EMAIL_FOLLOWERS && !mHasRefreshedEmailFollowers) { + return fetchEmailFollowersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), 1); + } else if (filter == PeopleListFilter.VIEWERS && !mHasRefreshedViewers) { + return fetchViewersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), 0); + } + return false; + } + + @Override + public boolean onFetchMorePeople(PeopleListFilter filter) { + if (filter == PeopleListFilter.TEAM && !mUsersEndOfListReached) { + Blog blog = WordPress.getCurrentBlog(); + int count = PeopleTable.getUsersCountForLocalBlogId(blog.getLocalTableBlogId()); + return fetchUsersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), count); + } else if (filter == PeopleListFilter.FOLLOWERS && !mFollowersEndOfListReached) { + Blog blog = WordPress.getCurrentBlog(); + int pageToFetch = mFollowersLastFetchedPage + 1; + return fetchFollowersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), pageToFetch); + } else if (filter == PeopleListFilter.EMAIL_FOLLOWERS && !mEmailFollowersEndOfListReached) { + Blog blog = WordPress.getCurrentBlog(); + int pageToFetch = mEmailFollowersLastFetchedPage + 1; + return fetchEmailFollowersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), pageToFetch); + } else if (filter == PeopleListFilter.VIEWERS && !mViewersEndOfListReached) { + Blog blog = WordPress.getCurrentBlog(); + int count = PeopleTable.getViewersCountForLocalBlogId(blog.getLocalTableBlogId()); + return fetchViewersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), count); + } + return false; + } + + private PeopleListFragment getListFragment() { + return (PeopleListFragment) getFragmentManager().findFragmentByTag(KEY_PEOPLE_LIST_FRAGMENT); + } + + private PersonDetailFragment getDetailFragment() { + return (PersonDetailFragment) getFragmentManager().findFragmentByTag(KEY_PERSON_DETAIL_FRAGMENT); + } + + public interface InvitationSender { + void send(); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/PersonDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/people/PersonDetailFragment.java new file mode 100644 index 000000000..0687e23a4 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/people/PersonDetailFragment.java @@ -0,0 +1,209 @@ +package org.wordpress.android.ui.people; + +import android.app.Fragment; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.datasets.PeopleTable; +import org.wordpress.android.models.Account; +import org.wordpress.android.models.AccountHelper; +import org.wordpress.android.models.Blog; +import org.wordpress.android.models.Capability; +import org.wordpress.android.models.Person; +import org.wordpress.android.models.Role; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.GravatarUtils; +import org.wordpress.android.util.StringUtils; +import org.wordpress.android.widgets.WPNetworkImageView; + +import java.text.SimpleDateFormat; + +public class PersonDetailFragment extends Fragment { + private static String ARG_PERSON_ID = "person_id"; + private static String ARG_LOCAL_TABLE_BLOG_ID = "local_table_blog_id"; + private static String ARG_PERSON_TYPE = "person_type"; + + private long mPersonID; + private int mLocalTableBlogID; + private Person.PersonType mPersonType; + + private WPNetworkImageView mAvatarImageView; + private TextView mDisplayNameTextView; + private TextView mUsernameTextView; + private LinearLayout mRoleContainer; + private TextView mRoleTextView; + private LinearLayout mSubscribedDateContainer; + private TextView mSubscribedDateTitleView; + private TextView mSubscribedDateTextView; + + public static PersonDetailFragment newInstance(long personID, int localTableBlogID, Person.PersonType personType) { + PersonDetailFragment personDetailFragment = new PersonDetailFragment(); + Bundle bundle = new Bundle(); + bundle.putLong(ARG_PERSON_ID, personID); + bundle.putInt(ARG_LOCAL_TABLE_BLOG_ID, localTableBlogID); + bundle.putSerializable(ARG_PERSON_TYPE, personType); + personDetailFragment.setArguments(bundle); + return personDetailFragment; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.person_detail, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.person_detail_fragment, container, false); + + mPersonID = getArguments().getLong(ARG_PERSON_ID); + mLocalTableBlogID = getArguments().getInt(ARG_LOCAL_TABLE_BLOG_ID); + mPersonType = (Person.PersonType) getArguments().getSerializable(ARG_PERSON_TYPE); + + mAvatarImageView = (WPNetworkImageView) rootView.findViewById(R.id.person_avatar); + mDisplayNameTextView = (TextView) rootView.findViewById(R.id.person_display_name); + mUsernameTextView = (TextView) rootView.findViewById(R.id.person_username); + mRoleContainer = (LinearLayout) rootView.findViewById(R.id.person_role_container); + mRoleTextView = (TextView) rootView.findViewById(R.id.person_role); + mSubscribedDateContainer = (LinearLayout) rootView.findViewById(R.id.subscribed_date_container); + mSubscribedDateTitleView = (TextView) rootView.findViewById(R.id.subscribed_date_title); + mSubscribedDateTextView = (TextView) rootView.findViewById(R.id.subscribed_date_text); + + Account account = AccountHelper.getDefaultAccount(); + boolean isCurrentUser = account.getUserId() == mPersonID; + Blog blog = WordPress.getBlog(mLocalTableBlogID); + if (!isCurrentUser && blog != null && blog.hasCapability(Capability.REMOVE_USERS)) { + setHasOptionsMenu(true); + } + + return rootView; + } + + @Override + public void onResume() { + super.onResume(); + + refreshPersonDetails(); + } + + public void refreshPersonDetails() { + if (!isAdded()) return; + + Person person = loadPerson(); + if (person != null) { + int avatarSz = getResources().getDimensionPixelSize(R.dimen.people_avatar_sz); + String avatarUrl = GravatarUtils.fixGravatarUrl(person.getAvatarUrl(), avatarSz); + + mAvatarImageView.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR); + mDisplayNameTextView.setText(StringUtils.unescapeHTML(person.getDisplayName())); + if (person.getRole() != null) { + mRoleTextView.setText(StringUtils.capitalize(person.getRole().toDisplayString())); + } + + if (!TextUtils.isEmpty(person.getUsername())) { + mUsernameTextView.setText(String.format("@%s", person.getUsername())); + } + + if (mPersonType == Person.PersonType.USER) { + mRoleContainer.setVisibility(View.VISIBLE); + setupRoleContainerForCapability(); + } else { + mRoleContainer.setVisibility(View.GONE); + } + + if (mPersonType == Person.PersonType.USER || mPersonType == Person.PersonType.VIEWER) { + mSubscribedDateContainer.setVisibility(View.GONE); + } else { + mSubscribedDateContainer.setVisibility(View.VISIBLE); + if (mPersonType == Person.PersonType.FOLLOWER) { + mSubscribedDateTitleView.setText(R.string.title_follower); + } else if (mPersonType == Person.PersonType.EMAIL_FOLLOWER) { + mSubscribedDateTitleView.setText(R.string.title_email_follower); + } + String dateSubscribed = SimpleDateFormat.getDateInstance().format(person.getDateSubscribed()); + String dateText = getString(R.string.follower_subscribed_since, dateSubscribed); + mSubscribedDateTextView.setText(dateText); + } + + // Adds extra padding to display name for email followers to make it vertically centered + int padding = mPersonType == Person.PersonType.EMAIL_FOLLOWER + ? (int) getResources().getDimension(R.dimen.margin_small) : 0; + changeDisplayNameTopPadding(padding); + } else { + AppLog.w(AppLog.T.PEOPLE, "Person returned null from DB for personID: " + mPersonID + + " & localTableBlogID: " + mLocalTableBlogID); + } + } + + public void setPersonDetails(long personID, int localTableBlogID) { + mPersonID = personID; + mLocalTableBlogID = localTableBlogID; + refreshPersonDetails(); + } + + // Checks current user's capabilities to decide whether she can change the role or not + private void setupRoleContainerForCapability() { + Blog blog = WordPress.getBlog(mLocalTableBlogID); + Account account = AccountHelper.getDefaultAccount(); + boolean isCurrentUser = account.getUserId() == mPersonID; + boolean canChangeRole = (blog != null) && !isCurrentUser && blog.hasCapability(Capability.PROMOTE_USERS); + if (canChangeRole) { + mRoleContainer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showRoleChangeDialog(); + } + }); + } else { + // Remove the selectableItemBackground if the user can't be edited + clearRoleContainerBackground(); + // Change transparency to give a visual cue to the user that it's disabled + mRoleContainer.setAlpha(0.5f); + } + } + + private void showRoleChangeDialog() { + Person person = loadPerson(); + if (person == null || person.getRole() == null) { + return; + } + + RoleChangeDialogFragment dialog = RoleChangeDialogFragment.newInstance(person.getPersonID(), + person.getLocalTableBlogId(), person.getRole()); + dialog.show(getFragmentManager(), null); + } + + // used to optimistically update the role + public void changeRole(Role newRole) { + mRoleTextView.setText(newRole.toDisplayString()); + } + + @SuppressWarnings("deprecation") + private void clearRoleContainerBackground() { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) { + mRoleContainer.setBackgroundDrawable(null); + } else { + mRoleContainer.setBackground(null); + } + } + + private void changeDisplayNameTopPadding(int newPadding) { + if (mDisplayNameTextView == null) { + return; + } + mDisplayNameTextView.setPadding(0, newPadding, 0 , 0); + } + + public Person loadPerson() { + return PeopleTable.getPerson(mPersonID, mLocalTableBlogID, mPersonType); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/RoleChangeDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/people/RoleChangeDialogFragment.java new file mode 100644 index 000000000..438231fe3 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/people/RoleChangeDialogFragment.java @@ -0,0 +1,148 @@ +package org.wordpress.android.ui.people; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.RadioButton; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.models.Role; + +import de.greenrobot.event.EventBus; + +public class RoleChangeDialogFragment extends DialogFragment { + private static final String PERSON_ID_TAG = "person_id"; + private static final String PERSON_LOCAL_TABLE_BLOG_ID_TAG = "local_table_blog_id"; + private static final String ROLE_TAG = "role"; + + private RoleListAdapter mRoleListAdapter; + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + Role role = mRoleListAdapter.getSelectedRole(); + outState.putSerializable(ROLE_TAG, role); + } + + public static RoleChangeDialogFragment newInstance(long personID, int localTableBlogId, Role role) { + RoleChangeDialogFragment roleChangeDialogFragment = new RoleChangeDialogFragment(); + Bundle args = new Bundle(); + + args.putLong(PERSON_ID_TAG, personID); + args.putInt(PERSON_LOCAL_TABLE_BLOG_ID_TAG, localTableBlogId); + if (role != null) { + args.putSerializable(ROLE_TAG, role); + } + + roleChangeDialogFragment.setArguments(args); + return roleChangeDialogFragment; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog); + builder.setTitle(R.string.role); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Role role = mRoleListAdapter.getSelectedRole(); + Bundle args = getArguments(); + if (args != null) { + long personID = args.getLong(PERSON_ID_TAG); + int localTableBlogId = args.getInt(PERSON_LOCAL_TABLE_BLOG_ID_TAG); + EventBus.getDefault().post(new RoleChangeEvent(personID, localTableBlogId, role)); + } + } + }); + + if (mRoleListAdapter == null) { + final Role[] userRoles = Role.userRoles(); + mRoleListAdapter = new RoleListAdapter(getActivity(), R.layout.role_list_row, userRoles); + } + if (savedInstanceState != null) { + Role savedRole = (Role) savedInstanceState.getSerializable(ROLE_TAG); + mRoleListAdapter.setSelectedRole(savedRole); + } else { + Bundle args = getArguments(); + if (args != null) { + Role role = (Role) args.getSerializable(ROLE_TAG); + mRoleListAdapter.setSelectedRole(role); + } + } + builder.setAdapter(mRoleListAdapter, null); + + return builder.create(); + } + + private class RoleListAdapter extends ArrayAdapter<Role> { + private Role mSelectedRole; + + public RoleListAdapter(Context context, int resource, Role[] objects) { + super(context, resource, objects); + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = View.inflate(getContext(), R.layout.role_list_row, null); + } + + final RadioButton radioButton = (RadioButton) convertView.findViewById(R.id.radio); + TextView mainText = (TextView) convertView.findViewById(R.id.role_label); + Role role = getItem(position); + mainText.setText(role.toDisplayString()); + + if (radioButton != null) { + radioButton.setChecked(role == mSelectedRole); + radioButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + changeSelection(position); + } + }); + } + + convertView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + changeSelection(position); + } + }); + + return convertView; + } + + private void changeSelection(int position) { + mSelectedRole = getItem(position); + notifyDataSetChanged(); + } + + public Role getSelectedRole() { + return mSelectedRole; + } + + public void setSelectedRole(Role role) { + mSelectedRole = role; + } + } + + public static class RoleChangeEvent { + public final long personID; + public final int localTableBlogId; + public final Role newRole; + + public RoleChangeEvent(long personID, int localTableBlogId, Role newRole) { + this.personID = personID; + this.localTableBlogId = localTableBlogId; + this.newRole = newRole; + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/RoleSelectDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/people/RoleSelectDialogFragment.java new file mode 100644 index 000000000..9c39fc559 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/people/RoleSelectDialogFragment.java @@ -0,0 +1,66 @@ +package org.wordpress.android.ui.people; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.DialogInterface; +import android.os.Bundle; + +import org.wordpress.android.R; +import org.wordpress.android.models.Role; + +public class RoleSelectDialogFragment extends DialogFragment { + private static final String IS_PRIVATE_TAG = "is_private"; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + boolean isPrivateSite = getArguments().getBoolean(IS_PRIVATE_TAG); + final Role[] roles = Role.inviteRoles(isPrivateSite); + final String[] stringRoles = new String[roles.length]; + for (int i = 0; i < roles.length; i++) { + stringRoles[i] = roles[i].toDisplayString(); + } + + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog); + builder.setTitle(R.string.role); + builder.setItems(stringRoles, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (!isAdded()) { + return; + } + + if (getTargetFragment() instanceof OnRoleSelectListener) { + ((OnRoleSelectListener) getTargetFragment()).onRoleSelected(roles[which]); + } else if (getActivity() instanceof OnRoleSelectListener) { + ((OnRoleSelectListener) getActivity()).onRoleSelected(roles[which]); + } + } + }); + + return builder.create(); + } + + public static <T extends Fragment & OnRoleSelectListener> void show(T parentFragment, int requestCode, + boolean isPrivateSite) { + RoleSelectDialogFragment roleChangeDialogFragment = new RoleSelectDialogFragment(); + Bundle args = new Bundle(); + args.putBoolean(IS_PRIVATE_TAG, isPrivateSite); + roleChangeDialogFragment.setArguments(args); + roleChangeDialogFragment.setTargetFragment(parentFragment, requestCode); + roleChangeDialogFragment.show(parentFragment.getFragmentManager(), null); + } + + public static <T extends Activity & OnRoleSelectListener> void show(T parentActivity) { + RoleSelectDialogFragment roleChangeDialogFragment = new RoleSelectDialogFragment(); + roleChangeDialogFragment.show(parentActivity.getFragmentManager(), null); + } + + // Container Activity must implement this interface + public interface OnRoleSelectListener { + void onRoleSelected(Role newRole); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/utils/PeopleUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/people/utils/PeopleUtils.java new file mode 100644 index 000000000..5c01c6ddf --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/people/utils/PeopleUtils.java @@ -0,0 +1,527 @@ +package org.wordpress.android.ui.people.utils; + +import com.android.volley.VolleyError; +import com.wordpress.rest.RestRequest; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.wordpress.android.WordPress; +import org.wordpress.android.models.Person; +import org.wordpress.android.models.Role; +import org.wordpress.android.ui.people.utils.PeopleUtils.ValidateUsernameCallback.ValidationResult; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class PeopleUtils { + // We limit followers we display to 1000 to avoid API performance issues + public static int FOLLOWER_PAGE_LIMIT = 50; + public static int FETCH_LIMIT = 20; + + public static void fetchUsers(final String blogId, final int localTableBlogId, final int offset, + final FetchUsersCallback callback) { + com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { + @Override + public void onResponse(JSONObject jsonObject) { + if (jsonObject != null && callback != null) { + try { + JSONArray jsonArray = jsonObject.getJSONArray("users"); + List<Person> people = peopleListFromJSON(jsonArray, localTableBlogId, Person.PersonType.USER); + int numberOfUsers = jsonObject.optInt("found"); + boolean isEndOfList = (people.size() + offset) >= numberOfUsers; + callback.onSuccess(people, isEndOfList); + } + catch (JSONException e) { + AppLog.e(T.API, "JSON exception occurred while parsing the response for sites/%s/users: " + e); + callback.onError(); + } + } + } + }; + + RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError volleyError) { + AppLog.e(T.API, volleyError); + if (callback != null) { + callback.onError(); + } + } + }; + + Map<String, String> params = new HashMap<>(); + params.put("number", Integer.toString(PeopleUtils.FETCH_LIMIT)); + params.put("offset", Integer.toString(offset)); + params.put("order_by", "display_name"); + params.put("order", "ASC"); + String path = String.format("sites/%s/users", blogId); + WordPress.getRestClientUtilsV1_1().get(path, params, null, listener, errorListener); + } + + public static void fetchFollowers(final String blogId, final int localTableBlogId, final int page, + final FetchFollowersCallback callback) { + fetchFollowers(blogId, localTableBlogId, page, callback, false); + } + + public static void fetchEmailFollowers(final String blogId, final int localTableBlogId, final int page, + final FetchFollowersCallback callback) { + fetchFollowers(blogId, localTableBlogId, page, callback, true); + } + + private static void fetchFollowers(final String blogId, final int localTableBlogId, final int page, + final FetchFollowersCallback callback, final boolean isEmailFollower) { + com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { + @Override + public void onResponse(JSONObject jsonObject) { + if (jsonObject != null && callback != null) { + try { + JSONArray jsonArray = jsonObject.getJSONArray("subscribers"); + Person.PersonType personType = isEmailFollower ? + Person.PersonType.EMAIL_FOLLOWER : Person.PersonType.FOLLOWER; + List<Person> people = peopleListFromJSON(jsonArray, localTableBlogId, personType); + int pageFetched = jsonObject.optInt("page"); + int numberOfPages = jsonObject.optInt("pages"); + boolean isEndOfList = page >= numberOfPages || page >= FOLLOWER_PAGE_LIMIT; + callback.onSuccess(people, pageFetched, isEndOfList); + } + catch (JSONException e) { + AppLog.e(T.API, "JSON exception occurred while parsing the response for " + + "sites/%s/stats/followers: " + e); + callback.onError(); + } + } + } + }; + + RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError volleyError) { + AppLog.e(T.API, volleyError); + if (callback != null) { + callback.onError(); + } + } + }; + + Map<String, String> params = new HashMap<>(); + params.put("max", Integer.toString(FETCH_LIMIT)); + params.put("page", Integer.toString(page)); + params.put("type", isEmailFollower ? "email" : "wp_com"); + String path = String.format("sites/%s/stats/followers", blogId); + WordPress.getRestClientUtilsV1_1().get(path, params, null, listener, errorListener); + } + + public static void fetchViewers(final String blogId, final int localTableBlogId, final int offset, + final FetchViewersCallback callback) { + com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { + @Override + public void onResponse(JSONObject jsonObject) { + if (jsonObject != null && callback != null) { + try { + JSONArray jsonArray = jsonObject.getJSONArray("viewers"); + List<Person> people = peopleListFromJSON(jsonArray, localTableBlogId, Person.PersonType.VIEWER); + int numberOfUsers = jsonObject.optInt("found"); + boolean isEndOfList = (people.size() + offset) >= numberOfUsers; + callback.onSuccess(people, isEndOfList); + } + catch (JSONException e) { + AppLog.e(T.API, "JSON exception occurred while parsing the response for " + + "sites/%s/viewers: " + e); + callback.onError(); + } + } + } + }; + + RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError volleyError) { + AppLog.e(T.API, volleyError); + if (callback != null) { + callback.onError(); + } + } + }; + + int page = (offset / FETCH_LIMIT) + 1; + Map<String, String> params = new HashMap<>(); + params.put("number", Integer.toString(FETCH_LIMIT)); + params.put("page", Integer.toString(page)); + String path = String.format("sites/%s/viewers", blogId); + WordPress.getRestClientUtilsV1_1().get(path, params, null, listener, errorListener); + } + + public static void updateRole(final String blogId, long personID, Role newRole, final int localTableBlogId, + final UpdateUserCallback callback) { + com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { + @Override + public void onResponse(JSONObject jsonObject) { + if (jsonObject != null && callback != null) { + try { + Person person = Person.userFromJSON(jsonObject, localTableBlogId); + if (person != null) { + callback.onSuccess(person); + } else { + AppLog.e(T.API, "Couldn't map jsonObject + " + jsonObject + " to person model."); + callback.onError(); + } + } catch (JSONException e) { + callback.onError(); + } + } + } + }; + + RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError volleyError) { + AppLog.e(T.API, volleyError); + if (callback != null) { + callback.onError(); + } + } + }; + + Map<String, String> params = new HashMap<>(); + params.put("roles", newRole.toRESTString()); + String path = String.format("sites/%s/users/%d", blogId, personID); + WordPress.getRestClientUtilsV1_1().post(path, params, null, listener, errorListener); + } + + public static void removeUser(String blogId, final long personID, final int localTableBlogId, + final RemovePersonCallback callback) { + com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { + @Override + public void onResponse(JSONObject jsonObject) { + if (jsonObject != null && callback != null) { + // check if the call was successful + boolean success = jsonObject.optBoolean("success"); + if (success) { + callback.onSuccess(personID, localTableBlogId); + } else { + callback.onError(); + } + } + } + }; + + RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError volleyError) { + AppLog.e(T.API, volleyError); + if (callback != null) { + callback.onError(); + } + } + }; + + String path = String.format("sites/%s/users/%d/delete", blogId, personID); + WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener); + } + + public static void removeFollower(String blogId, final long personID, final int localTableBlogId, + Person.PersonType personType, final RemovePersonCallback callback) { + com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { + @Override + public void onResponse(JSONObject jsonObject) { + if (jsonObject != null && callback != null) { + // check if the call was successful + boolean success = jsonObject.optBoolean("deleted"); + if (success) { + callback.onSuccess(personID, localTableBlogId); + } else { + callback.onError(); + } + } + } + }; + + RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError volleyError) { + AppLog.e(T.API, volleyError); + if (callback != null) { + callback.onError(); + } + } + }; + + String path; + if (personType == Person.PersonType.EMAIL_FOLLOWER) { + path = String.format("sites/%s/email-followers/%d/delete", blogId, personID); + } else { + path = String.format("sites/%s/followers/%d/delete", blogId, personID); + } + WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener); + } + + public static void removeViewer(String blogId, final long personID, final int localTableBlogId, + final RemovePersonCallback callback) { + com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { + @Override + public void onResponse(JSONObject jsonObject) { + if (jsonObject != null && callback != null) { + // check if the call was successful + boolean success = jsonObject.optBoolean("deleted"); + if (success) { + callback.onSuccess(personID, localTableBlogId); + } else { + callback.onError(); + } + } + } + }; + + RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError volleyError) { + AppLog.e(T.API, volleyError); + if (callback != null) { + callback.onError(); + } + } + }; + + String path = String.format("sites/%s/viewers/%d/delete", blogId, personID); + WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener); + } + + private static List<Person> peopleListFromJSON(JSONArray jsonArray, int localTableBlogId, + Person.PersonType personType) throws JSONException { + if (jsonArray == null) { + return null; + } + + ArrayList<Person> peopleList = new ArrayList<>(jsonArray.length()); + + for (int i = 0; i < jsonArray.length(); i++) { + Person person; + if (personType == Person.PersonType.USER) { + person = Person.userFromJSON(jsonArray.optJSONObject(i), localTableBlogId); + } else if (personType == Person.PersonType.VIEWER) { + person = Person.viewerFromJSON(jsonArray.optJSONObject(i), localTableBlogId); + } else { + boolean isEmailFollower = (personType == Person.PersonType.EMAIL_FOLLOWER); + person = Person.followerFromJSON(jsonArray.optJSONObject(i), localTableBlogId, isEmailFollower); + } + if (person != null) { + peopleList.add(person); + } + } + + return peopleList; + } + + public interface FetchUsersCallback extends Callback { + void onSuccess(List<Person> peopleList, boolean isEndOfList); + } + + public interface FetchFollowersCallback extends Callback { + void onSuccess(List<Person> peopleList, int pageFetched, boolean isEndOfList); + } + + public interface FetchViewersCallback extends Callback { + void onSuccess(List<Person> peopleList, boolean isEndOfList); + } + + public interface RemovePersonCallback extends Callback { + void onSuccess(long personID, int localTableBlogId); + } + + public interface UpdateUserCallback extends Callback { + void onSuccess(Person person); + } + + public interface Callback { + void onError(); + } + + public static void validateUsernames(final List<String> usernames, Role role, String dotComBlogId, final + ValidateUsernameCallback callback) { + com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { + @Override + public void onResponse(JSONObject jsonObject) { + if (jsonObject != null && callback != null) { + JSONObject errors = jsonObject.optJSONObject("errors"); + + int errorredUsernameCount = 0; + + if (errors != null) { + for (String username : usernames) { + JSONObject userError = errors.optJSONObject(username); + + if (userError == null) { + continue; + } + + errorredUsernameCount++; + + switch (userError.optString("code")) { + case "invalid_input": + switch (userError.optString("message")) { + case "User not found": + callback.onUsernameValidation(username, ValidationResult.USER_NOT_FOUND); + continue; + case "Invalid email": + callback.onUsernameValidation(username, ValidationResult.INVALID_EMAIL); + continue; + } + break; + case "invalid_input_has_role": + callback.onUsernameValidation(username, ValidationResult.ALREADY_MEMBER); + continue; + case "invalid_input_following": + callback.onUsernameValidation(username, ValidationResult.ALREADY_FOLLOWING); + continue; + case "invalid_user_blocked_invites": + callback.onUsernameValidation(username, ValidationResult.BLOCKED_INVITES); + continue; + } + + callback.onError(); + callback.onValidationFinished(); + return; + } + } + + JSONArray succeededUsernames = jsonObject.optJSONArray("success"); + if (succeededUsernames == null) { + callback.onError(); + callback.onValidationFinished(); + return; + } + + int succeededUsernameCount = 0; + + for (int i = 0; i < succeededUsernames.length(); i++) { + String username = succeededUsernames.optString(i); + if (usernames.contains(username)) { + succeededUsernameCount++; + callback.onUsernameValidation(username, ValidationResult.USER_FOUND); + } + } + + if (errorredUsernameCount + succeededUsernameCount != usernames.size()) { + callback.onError(); + callback.onValidationFinished(); + } + + callback.onValidationFinished(); + } + } + }; + + RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError volleyError) { + AppLog.e(AppLog.T.API, volleyError); + if (callback != null) { + callback.onError(); + } + } + }; + + String path = String.format("sites/%s/invites/validate", dotComBlogId); + Map<String, String> params = new HashMap<>(); + for (String username : usernames) { + params.put("invitees[" + username + "]", username); // specify an array key so to make the map key unique + } + params.put("role", role.toRESTString()); + WordPress.getRestClientUtilsV1_1().post(path, params, null, listener, errorListener); + } + + public interface ValidateUsernameCallback { + enum ValidationResult { + USER_NOT_FOUND, + ALREADY_MEMBER, + ALREADY_FOLLOWING, + BLOCKED_INVITES, + INVALID_EMAIL, + USER_FOUND + } + + void onUsernameValidation(String username, ValidationResult validationResult); + void onValidationFinished(); + void onError(); + } + + public static void sendInvitations(final List<String> usernames, Role role, String message, String dotComBlogId, + final InvitationsSendCallback callback) { + com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { + @Override + public void onResponse(JSONObject jsonObject) { + if (callback == null) { + return; + } + + if (jsonObject == null) { + callback.onError(); + return; + } + + Map<String, String> failedUsernames = new LinkedHashMap<>(); + + JSONObject errors = jsonObject.optJSONObject("errors"); + if (errors != null) { + for (String username : usernames) { + JSONObject userError = errors.optJSONObject(username); + + if (userError != null) { + failedUsernames.put(username, userError.optString("message")); + } + } + } + + List<String> succeededUsernames = new ArrayList<>(); + JSONArray succeededUsernamesJson = jsonObject.optJSONArray("sent"); + if (succeededUsernamesJson == null) { + callback.onError(); + return; + } + + for (int i = 0; i < succeededUsernamesJson.length(); i++) { + String username = succeededUsernamesJson.optString(i); + if (usernames.contains(username)) { + succeededUsernames.add(username); + } + } + + if (failedUsernames.size() + succeededUsernames.size() != usernames.size()) { + callback.onError(); + } + + callback.onSent(succeededUsernames, failedUsernames); + } + }; + + RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError volleyError) { + AppLog.e(AppLog.T.API, volleyError); + if (callback != null) { + callback.onError(); + } + } + }; + + String path = String.format("sites/%s/invites/new", dotComBlogId); + Map<String, String> params = new HashMap<>(); + for (String username : usernames) { + params.put("invitees[" + username + "]", username); // specify an array key so to make the map key unique + } + params.put("role", role.toRESTString()); + params.put("message", message); + WordPress.getRestClientUtilsV1_1().post(path, params, null, listener, errorListener); + } + + public interface InvitationsSendCallback { + void onSent(List<String> succeededUsernames, Map<String, String> failedUsernameErrors); + void onError(); + } +} |