Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

View File

@@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.reactions;
import androidx.annotation.NonNull;
final class EmojiCount {
private final String emoji;
private final int count;
EmojiCount(@NonNull String emoji, int count) {
this.emoji = emoji;
this.count = count;
}
public @NonNull String getEmoji() {
return emoji;
}
public int getCount() {
return count;
}
}

View File

@@ -0,0 +1,113 @@
package org.thoughtcrime.securesms.reactions;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.Collections;
import java.util.List;
final class ReactionEmojiCountAdapter extends RecyclerView.Adapter<ReactionEmojiCountAdapter.ViewHolder> {
private List<EmojiCount> emojiCountList = Collections.emptyList();
private int selectedPosition = -1;
private final OnEmojiCountSelectedListener onEmojiCountSelectedListener;
ReactionEmojiCountAdapter(@NonNull OnEmojiCountSelectedListener onEmojiCountSelectedListener) {
this.onEmojiCountSelectedListener = onEmojiCountSelectedListener;
}
void updateData(@NonNull List<EmojiCount> newEmojiCount) {
if (selectedPosition != -1) {
EmojiCount oldSelection = emojiCountList.get(selectedPosition);
int newPosition = -1;
for (int i = 0; i < newEmojiCount.size(); i++) {
if (newEmojiCount.get(i).getEmoji().equals(oldSelection.getEmoji())) {
newPosition = i;
break;
}
}
if (newPosition == -1 && !newEmojiCount.isEmpty()) {
selectedPosition = 0;
onEmojiCountSelectedListener.onSelected(newEmojiCount.get(0));
} else {
selectedPosition = newPosition;
}
} else if (!newEmojiCount.isEmpty()) {
selectedPosition = 0;
onEmojiCountSelectedListener.onSelected(newEmojiCount.get(0));
}
this.emojiCountList = newEmojiCount;
notifyDataSetChanged();
}
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_emoji_item, parent, false), position -> {
if (position != -1 && position != selectedPosition) {
onEmojiCountSelectedListener.onSelected(emojiCountList.get(position));
int oldPosition = selectedPosition;
selectedPosition = position;
notifyItemChanged(oldPosition);
notifyItemChanged(position);
}
});
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(emojiCountList.get(position), selectedPosition);
}
@Override
public int getItemCount() {
return emojiCountList.size();
}
static final class ViewHolder extends RecyclerView.ViewHolder {
private final Drawable selected;
private final EmojiTextView emojiView;
private final TextView countView;
public ViewHolder(@NonNull View itemView, @NonNull OnViewHolderClickListener onClickListener) {
super(itemView);
emojiView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_emoji);
countView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_text);
selected = ThemeUtil.getThemedDrawable(itemView.getContext(), R.attr.reactions_bottom_dialog_fragment_emoji_selected);
itemView.setOnClickListener(v -> onClickListener.onClick(getAdapterPosition()));
}
void bind(@NonNull EmojiCount emojiCount, int selectedPosition) {
emojiView.setText(emojiCount.getEmoji());
countView.setText(String.valueOf(emojiCount.getCount()));
itemView.setBackground(getAdapterPosition() == selectedPosition ? selected : null);
}
}
interface OnViewHolderClickListener {
void onClick(int position);
}
interface OnEmojiCountSelectedListener {
void onSelected(@NonNull EmojiCount emojiCount);
}
}

View File

@@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.reactions;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AvatarUtil;
import java.util.Collections;
import java.util.List;
final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecipientsAdapter.ViewHolder> {
private List<Recipient> data = Collections.emptyList();
public void updateData(List<Recipient> newData) {
data = newData;
notifyDataSetChanged();
}
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.reactions_bottom_sheet_dialog_fragment_recipient_item,
parent,
false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
final class ViewHolder extends RecyclerView.ViewHolder {
private final AvatarImageView avatar;
private final TextView recipient;
public ViewHolder(@NonNull View itemView) {
super(itemView);
avatar = itemView.findViewById(R.id.reactions_bottom_view_recipient_avatar);
recipient = itemView.findViewById(R.id.reactions_bottom_view_recipient_name);
}
void bind(Recipient recipient) {
this.recipient.setText(recipient.getDisplayName(itemView.getContext()));
if (recipient.equals(Recipient.self())) {
AvatarUtil.loadIconIntoImageView(recipient, avatar);
} else {
this.avatar.setAvatar(GlideApp.with(avatar), recipient, false);
}
}
}
}

View File

@@ -0,0 +1,115 @@
package org.thoughtcrime.securesms.reactions;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.loader.app.LoaderManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ThemeUtil;
public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogFragment {
private static final String ARGS_MESSAGE_ID = "reactions.args.message.id";
private static final String ARGS_IS_MMS = "reactions.args.is.mms";
private long messageId;
private RecyclerView recipientRecyclerView;
private RecyclerView emojiRecyclerView;
private ReactionsLoader reactionsLoader;
private ReactionRecipientsAdapter recipientsAdapter;
private ReactionEmojiCountAdapter emojiCountAdapter;
private ReactionsViewModel viewModel;
public static DialogFragment create(long messageId, boolean isMms) {
Bundle args = new Bundle();
DialogFragment fragment = new ReactionsBottomSheetDialogFragment();
args.putLong(ARGS_MESSAGE_ID, messageId);
args.putBoolean(ARGS_IS_MMS, isMms);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
if (ThemeUtil.isDarkTheme(requireContext())) {
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Design_BottomSheetDialog_Fixed);
} else {
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Design_Light_BottomSheetDialog_Fixed);
}
super.onCreate(savedInstanceState);
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.reactions_bottom_sheet_dialog_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
recipientRecyclerView = view.findViewById(R.id.reactions_bottom_view_recipient_recycler);
emojiRecyclerView = view.findViewById(R.id.reactions_bottom_view_emoji_recycler);
messageId = getArguments().getLong(ARGS_MESSAGE_ID);
setUpRecipientsRecyclerView();
setUpEmojiRecyclerView();
setUpViewModel();
LoaderManager.getInstance(requireActivity()).initLoader((int) messageId, null, reactionsLoader);
}
@Override
public void onDestroyView() {
LoaderManager.getInstance(requireActivity()).destroyLoader((int) messageId);
super.onDestroyView();
}
private void setUpRecipientsRecyclerView() {
recipientsAdapter = new ReactionRecipientsAdapter();
recipientRecyclerView.setAdapter(recipientsAdapter);
}
private void setUpEmojiRecyclerView() {
emojiCountAdapter = new ReactionEmojiCountAdapter((emojiCount -> viewModel.setFilterEmoji(emojiCount.getEmoji())));
emojiRecyclerView.setAdapter(emojiCountAdapter);
}
private void setUpViewModel() {
reactionsLoader = new ReactionsLoader(requireContext(),
getArguments().getLong(ARGS_MESSAGE_ID),
getArguments().getBoolean(ARGS_IS_MMS));
ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(reactionsLoader);
viewModel = ViewModelProviders.of(this, factory).get(ReactionsViewModel.class);
viewModel.getRecipients().observe(getViewLifecycleOwner(), reactions -> {
if (reactions.size() == 0) dismiss();
recipientsAdapter.updateData(reactions);
});
viewModel.getEmojiCounts().observe(getViewLifecycleOwner(), emojiCounts -> {
if (emojiCounts.size() == 0) dismiss();
emojiCountAdapter.updateData(emojiCounts);
});
}
}

View File

@@ -0,0 +1,130 @@
package org.thoughtcrime.securesms.reactions;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.Collections;
import java.util.List;
public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderManager.LoaderCallbacks<Cursor> {
private final long messageId;
private final boolean isMms;
private final Context appContext;
private MutableLiveData<List<Reaction>> internalLiveData = new MutableLiveData<>();
public ReactionsLoader(@NonNull Context context, long messageId, boolean isMms)
{
this.messageId = messageId;
this.isMms = isMms;
this.appContext = context.getApplicationContext();
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
return isMms ? new MmsMessageRecordCursorLoader(appContext, messageId)
: new SmsMessageRecordCursorLoader(appContext, messageId);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
SignalExecutors.BOUNDED.execute(() -> {
MessageRecord record = isMms ? DatabaseFactory.getMmsDatabase(appContext).readerFor(data).getNext()
: DatabaseFactory.getSmsDatabase(appContext).readerFor(data).getNext();
if (record == null) {
internalLiveData.postValue(Collections.emptyList());
} else {
internalLiveData.postValue(Stream.of(record.getReactions())
.map(reactionRecord -> new Reaction(Recipient.resolved(reactionRecord.getAuthor()),
reactionRecord.getEmoji(),
reactionRecord.getDateReceived()))
.toList());
}
});
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
// Do nothing?
}
@Override
public LiveData<List<Reaction>> getReactions() {
return internalLiveData;
}
private static final class MmsMessageRecordCursorLoader extends AbstractCursorLoader {
private final long messageId;
public MmsMessageRecordCursorLoader(@NonNull Context context, long messageId) {
super(context);
this.messageId = messageId;
}
@Override
public Cursor getCursor() {
return DatabaseFactory.getMmsDatabase(context).getMessage(messageId);
}
}
private static final class SmsMessageRecordCursorLoader extends AbstractCursorLoader {
private final long messageId;
public SmsMessageRecordCursorLoader(@NonNull Context context, long messageId) {
super(context);
this.messageId = messageId;
}
@Override
public Cursor getCursor() {
return DatabaseFactory.getSmsDatabase(context).getMessageCursor(messageId);
}
}
static class Reaction {
private final Recipient sender;
private final String emoji;
private final long timestamp;
private Reaction(@NonNull Recipient sender, @NonNull String emoji, long timestamp) {
this.sender = sender;
this.emoji = emoji;
this.timestamp = timestamp;
}
public @NonNull Recipient getSender() {
return sender;
}
public @NonNull String getEmoji() {
return emoji;
}
public long getTimestamp() {
return timestamp;
}
}
}

View File

@@ -0,0 +1,84 @@
package org.thoughtcrime.securesms.reactions;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import static org.thoughtcrime.securesms.reactions.ReactionsLoader.*;
public class ReactionsViewModel extends ViewModel {
private final Repository repository;
private final MutableLiveData<String> filterEmoji = new MutableLiveData<>();
public ReactionsViewModel(@NonNull Repository repository) {
this.repository = repository;
}
public @NonNull LiveData<List<Recipient>> getRecipients() {
return Transformations.switchMap(filterEmoji,
emoji -> Transformations.map(repository.getReactions(),
reactions -> Stream.of(reactions)
.filter(reaction -> reaction.getEmoji().equals(emoji))
.map(Reaction::getSender).toList()));
}
public @NonNull LiveData<List<EmojiCount>> getEmojiCounts() {
return Transformations.map(repository.getReactions(),
reactionList -> Stream.of(reactionList)
.groupBy(Reaction::getEmoji)
.sorted(this::compareReactions)
.map(entry -> new EmojiCount(entry.getKey(), entry.getValue().size()))
.toList());
}
public void setFilterEmoji(String filterEmoji) {
this.filterEmoji.setValue(filterEmoji);
}
private int compareReactions(@NonNull Map.Entry<String, List<Reaction>> lhs, @NonNull Map.Entry<String, List<Reaction>> rhs) {
int lengthComparison = -Integer.compare(lhs.getValue().size(), rhs.getValue().size());
if (lengthComparison != 0) return lengthComparison;
long latestTimestampLhs = getLatestTimestamp(lhs.getValue());
long latestTimestampRhs = getLatestTimestamp(rhs.getValue());
return -Long.compare(latestTimestampLhs, latestTimestampRhs);
}
private long getLatestTimestamp(List<Reaction> reactions) {
return Stream.of(reactions)
.max((a, b) -> Long.compare(a.getTimestamp(), b.getTimestamp()))
.map(Reaction::getTimestamp)
.orElse(-1L);
}
interface Repository {
LiveData<List<Reaction>> getReactions();
}
static final class Factory implements ViewModelProvider.Factory {
private final Repository repository;
Factory(@NonNull Repository repository) {
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return (T) new ReactionsViewModel(repository);
}
}
}