Update Mention UI/UX to match latest designs.

master
Cody Henthorne 2020-08-13 09:54:33 -04:00 committed by Greyson Parrelli
parent d63e5165eb
commit 724f3e872b
30 changed files with 353 additions and 201 deletions

View File

@ -4,12 +4,6 @@ import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.widget.TextViewCompat;
import androidx.appcompat.widget.AppCompatTextView;
import android.text.Annotation; import android.text.Annotation;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
@ -17,12 +11,18 @@ import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.TypedValue; import android.util.TypedValue;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat;
import androidx.core.widget.TextViewCompat;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable; import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation; import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate; import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -242,4 +242,10 @@ public class EmojiTextView extends AppCompatTextView {
this.originalFontSize = TypedValue.applyDimension(unit, size, getResources().getDisplayMetrics()); this.originalFontSize = TypedValue.applyDimension(unit, size, getResources().getDisplayMetrics());
super.setTextSize(unit, size); super.setTextSize(unit, size);
} }
public void setMentionBackgroundTint(@ColorInt int mentionBackgroundTint) {
if (renderMentions) {
mentionRendererDelegate.setTint(mentionBackgroundTint);
}
}
} }

View File

@ -9,10 +9,10 @@ import android.text.Spanned;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Px; import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.DrawableUtil; import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
@ -27,25 +27,28 @@ public class MentionRendererDelegate {
private final MentionRenderer single; private final MentionRenderer single;
private final MentionRenderer multi; private final MentionRenderer multi;
private final int horizontalPadding; private final int horizontalPadding;
private final Drawable drawable;
private final Drawable drawableLeft;
private final Drawable drawableMid;
private final Drawable drawableEnd;
public MentionRendererDelegate(@NonNull Context context, @ColorInt int tint) { public MentionRendererDelegate(@NonNull Context context, @ColorInt int tint) {
this.horizontalPadding = ViewUtil.dpToPx(2); this.horizontalPadding = ViewUtil.dpToPx(2);
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.mention_text_bg); drawable = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg), tint);
Drawable drawableLeft = ContextCompat.getDrawable(context, R.drawable.mention_text_bg_left); drawableLeft = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg_left), tint);
Drawable drawableMid = ContextCompat.getDrawable(context, R.drawable.mention_text_bg_mid); drawableMid = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg_mid), tint);
Drawable drawableEnd = ContextCompat.getDrawable(context, R.drawable.mention_text_bg_right); drawableEnd = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg_right), tint);
//noinspection ConstantConditions
single = new MentionRenderer.SingleLineMentionRenderer(horizontalPadding, single = new MentionRenderer.SingleLineMentionRenderer(horizontalPadding,
0, 0,
DrawableUtil.tint(drawable, tint)); drawable);
//noinspection ConstantConditions
multi = new MentionRenderer.MultiLineMentionRenderer(horizontalPadding, multi = new MentionRenderer.MultiLineMentionRenderer(horizontalPadding,
0, 0,
DrawableUtil.tint(drawableLeft, tint), drawableLeft,
DrawableUtil.tint(drawableMid, tint), drawableMid,
DrawableUtil.tint(drawableEnd, tint)); drawableEnd);
} }
public void draw(@NonNull Canvas canvas, @NonNull Spanned text, @NonNull Layout layout) { public void draw(@NonNull Canvas canvas, @NonNull Spanned text, @NonNull Layout layout) {
@ -65,4 +68,11 @@ public class MentionRendererDelegate {
} }
} }
} }
public void setTint(@ColorInt int tint) {
DrawableCompat.setTint(drawable, tint);
DrawableCompat.setTint(drawableLeft, tint);
DrawableCompat.setTint(drawableMid, tint);
DrawableCompat.setTint(drawableEnd, tint);
}
} }

View File

@ -52,6 +52,7 @@ import androidx.annotation.DimenRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
@ -63,6 +64,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.components.AlertView; import org.thoughtcrime.securesms.components.AlertView;
import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.BorderlessImageView;
import org.thoughtcrime.securesms.components.ConversationItemFooter; import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.components.ConversationItemThumbnail; import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.DocumentView;
@ -70,7 +72,6 @@ import org.thoughtcrime.securesms.components.LinkPreviewView;
import org.thoughtcrime.securesms.components.Outliner; import org.thoughtcrime.securesms.components.Outliner;
import org.thoughtcrime.securesms.components.QuoteView; import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.SharedContactView; import org.thoughtcrime.securesms.components.SharedContactView;
import org.thoughtcrime.securesms.components.BorderlessImageView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView; import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation; import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.contactshare.Contact;
@ -113,6 +114,7 @@ import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.SearchUtil; import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.VibrateUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub; import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -123,6 +125,8 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import static org.thoughtcrime.securesms.util.ThemeUtil.isDarkTheme;
/** /**
* A view that displays an individual conversation item within a conversation * A view that displays an individual conversation item within a conversation
* thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter. * thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter.
@ -564,6 +568,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati
bodyText.setOverflowText(null); bodyText.setOverflowText(null);
} }
if (messageRecord.isOutgoing()) {
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, isDarkTheme(context) ? R.color.core_grey_60 : R.color.core_grey_20));
} else {
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_40));
}
bodyText.setText(styledText); bodyText.setText(styledText);
bodyText.setVisibility(View.VISIBLE); bodyText.setVisibility(View.VISIBLE);
} }
@ -1423,6 +1433,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
@Override @Override
public void onClick(@NonNull View widget) { public void onClick(@NonNull View widget) {
if (eventListener != null && !Recipient.resolved(mentionedRecipientId).isLocalNumber()) { if (eventListener != null && !Recipient.resolved(mentionedRecipientId).isLocalNumber()) {
VibrateUtil.vibrateTick(context);
eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId()); eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId());
} }
} }

View File

@ -46,6 +46,6 @@ public class MentionViewHolder extends MappingViewHolder<MentionViewState> {
} }
public static MappingAdapter.Factory<MentionViewState> createFactory(@Nullable MentionEventsListener mentionEventsListener) { public static MappingAdapter.Factory<MentionViewState> createFactory(@Nullable MentionEventsListener mentionEventsListener) {
return new MappingAdapter.LayoutFactory<>(view -> new MentionViewHolder(view, mentionEventsListener), R.layout.mentions_recipient_list_item); return new MappingAdapter.LayoutFactory<>(view -> new MentionViewHolder(view, mentionEventsListener), R.layout.mentions_picker_recipient_list_item);
} }
} }

View File

@ -1,12 +1,25 @@
package org.thoughtcrime.securesms.conversation.ui.mentions; package org.thoughtcrime.securesms.conversation.ui.mentions;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionViewHolder.MentionEventsListener; import org.thoughtcrime.securesms.conversation.ui.mentions.MentionViewHolder.MentionEventsListener;
import org.thoughtcrime.securesms.util.MappingAdapter; import org.thoughtcrime.securesms.util.MappingAdapter;
import org.thoughtcrime.securesms.util.MappingModel;
import java.util.List;
public class MentionsPickerAdapter extends MappingAdapter { public class MentionsPickerAdapter extends MappingAdapter {
public MentionsPickerAdapter(@Nullable MentionEventsListener mentionEventsListener) { private final Runnable currentListChangedListener;
public MentionsPickerAdapter(@Nullable MentionEventsListener mentionEventsListener, @NonNull Runnable currentListChangedListener) {
this.currentListChangedListener = currentListChangedListener;
registerFactory(MentionViewState.class, MentionViewHolder.createFactory(mentionEventsListener)); registerFactory(MentionViewState.class, MentionViewHolder.createFactory(mentionEventsListener));
} }
@Override
public void onCurrentListChanged(@NonNull List<MappingModel<?>> previousList, @NonNull List<MappingModel<?>> currentList) {
super.onCurrentListChanged(previousList, currentList);
currentListChangedListener.run();
}
} }

View File

@ -17,25 +17,30 @@ import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.MappingModel; import org.thoughtcrime.securesms.util.MappingModel;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.VibrateUtil;
import java.util.Collections;
import java.util.List; import java.util.List;
public class MentionsPickerFragment extends LoggingFragment { public class MentionsPickerFragment extends LoggingFragment {
private MentionsPickerAdapter adapter; private MentionsPickerAdapter adapter;
private RecyclerView list; private RecyclerView list;
private View topDivider;
private View bottomDivider;
private BottomSheetBehavior<View> behavior; private BottomSheetBehavior<View> behavior;
private MentionsPickerViewModel viewModel; private MentionsPickerViewModel viewModel;
private int defaultPeekHeight;
@Override @Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.mentions_picker_fragment, container, false); View view = inflater.inflate(R.layout.mentions_picker_fragment, container, false);
list = view.findViewById(R.id.mentions_picker_list); list = view.findViewById(R.id.mentions_picker_list);
behavior = BottomSheetBehavior.from(view.findViewById(R.id.mentions_picker_bottom_sheet)); topDivider = view.findViewById(R.id.mentions_picker_top_divider);
defaultPeekHeight = view.getContext().getResources().getDimensionPixelSize(R.dimen.mentions_picker_peek_height); bottomDivider = view.findViewById(R.id.mentions_picker_bottom_divider);
behavior = BottomSheetBehavior.from(view.findViewById(R.id.mentions_picker_bottom_sheet));
initializeBehavior();
return view; return view;
} }
@ -43,24 +48,43 @@ public class MentionsPickerFragment extends LoggingFragment {
@Override @Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) { public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
viewModel = ViewModelProviders.of(requireActivity()).get(MentionsPickerViewModel.class);
initializeList(); initializeList();
viewModel = ViewModelProviders.of(requireActivity()).get(MentionsPickerViewModel.class);
viewModel.getMentionList().observe(getViewLifecycleOwner(), this::updateList); viewModel.getMentionList().observe(getViewLifecycleOwner(), this::updateList);
viewModel.isShowing().observe(getViewLifecycleOwner(), isShowing -> {
if (isShowing) {
VibrateUtil.vibrateTick(requireContext());
}
});
}
private void initializeBehavior() {
behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
adapter.submitList(Collections.emptyList());
} else {
showDividers(true);
}
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
showDividers(Float.isNaN(slideOffset) || slideOffset > -0.8f);
}
});
} }
private void initializeList() { private void initializeList() {
adapter = new MentionsPickerAdapter(this::handleMentionClicked); adapter = new MentionsPickerAdapter(this::handleMentionClicked, () -> updateBottomSheetBehavior(adapter.getItemCount()));
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(requireContext()) { list.setLayoutManager(new LinearLayoutManager(requireContext()));
@Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
updateBottomSheetBehavior(adapter.getItemCount());
}
};
list.setLayoutManager(layoutManager);
list.setAdapter(adapter); list.setAdapter(adapter);
list.setItemAnimator(null); list.setItemAnimator(null);
} }
@ -70,24 +94,31 @@ public class MentionsPickerFragment extends LoggingFragment {
} }
private void updateList(@NonNull List<MappingModel<?>> mappingModels) { private void updateList(@NonNull List<MappingModel<?>> mappingModels) {
adapter.submitList(mappingModels); if (adapter.getItemCount() > 0 && mappingModels.isEmpty()) {
if (mappingModels.isEmpty()) {
updateBottomSheetBehavior(0); updateBottomSheetBehavior(0);
} else {
adapter.submitList(mappingModels);
} }
list.scrollToPosition(0);
} }
private void updateBottomSheetBehavior(int count) { private void updateBottomSheetBehavior(int count) {
if (count > 0) { boolean isShowing = count > 0;
if (behavior.getPeekHeight() == 0) {
behavior.setPeekHeight(defaultPeekHeight, true); viewModel.setIsShowing(isShowing);
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else { if (isShowing) {
list.scrollToPosition(0); list.scrollToPosition(0);
}
} else {
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED); behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
behavior.setPeekHeight(0); list.post(() -> behavior.setHideable(false));
showDividers(true);
} else {
behavior.setHideable(true);
behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
} }
} }
private void showDividers(boolean showDividers) {
topDivider.setVisibility(showDividers ? View.VISIBLE : View.GONE);
bottomDivider.setVisibility(showDividers ? View.VISIBLE : View.GONE);
}
} }

View File

@ -29,11 +29,13 @@ public class MentionsPickerViewModel extends ViewModel {
private final LiveData<List<MappingModel<?>>> mentionList; private final LiveData<List<MappingModel<?>>> mentionList;
private final MutableLiveData<LiveGroup> group; private final MutableLiveData<LiveGroup> group;
private final MutableLiveData<Query> liveQuery; private final MutableLiveData<Query> liveQuery;
private final MutableLiveData<Boolean> isShowing;
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository) { MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository) {
group = new MutableLiveData<>(); group = new MutableLiveData<>();
liveQuery = new MutableLiveData<>(Query.NONE); liveQuery = new MutableLiveData<>(Query.NONE);
selectedRecipient = new SingleLiveEvent<>(); selectedRecipient = new SingleLiveEvent<>();
isShowing = new MutableLiveData<>(false);
LiveData<List<FullMember>> fullMembers = Transformations.distinctUntilChanged(Transformations.switchMap(group, LiveGroup::getFullMembers)); LiveData<List<FullMember>> fullMembers = Transformations.distinctUntilChanged(Transformations.switchMap(group, LiveGroup::getFullMembers));
LiveData<Query> query = Transformations.distinctUntilChanged(liveQuery); LiveData<Query> query = Transformations.distinctUntilChanged(liveQuery);
@ -50,10 +52,21 @@ public class MentionsPickerViewModel extends ViewModel {
selectedRecipient.setValue(recipient); selectedRecipient.setValue(recipient);
} }
void setIsShowing(boolean isShowing) {
if (Objects.equals(this.isShowing.getValue(), isShowing)) {
return;
}
this.isShowing.setValue(isShowing);
}
public @NonNull LiveData<Recipient> getSelectedRecipient() { public @NonNull LiveData<Recipient> getSelectedRecipient() {
return selectedRecipient; return selectedRecipient;
} }
public @NonNull LiveData<Boolean> isShowing() {
return isShowing;
}
public void onQueryChange(@Nullable String query) { public void onQueryChange(@Nullable String query) {
liveQuery.setValue(query == null ? Query.NONE : new Query(query)); liveQuery.setValue(query == null ? Query.NONE : new Query(query));
} }

View File

@ -56,7 +56,7 @@ public final class MentionUtil {
@WorkerThread @WorkerThread
public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithDisplayNames(@NonNull Context context, @NonNull CharSequence body, @NonNull List<Mention> mentions) { public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithDisplayNames(@NonNull Context context, @NonNull CharSequence body, @NonNull List<Mention> mentions) {
return update(body, mentions, m -> MENTION_STARTER + Recipient.resolved(m.getRecipientId()).getDisplayName(context)); return update(body, mentions, m -> MENTION_STARTER + Recipient.resolved(m.getRecipientId()).getMentionDisplayName(context));
} }
public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithPlaceholders(@Nullable CharSequence body, @NonNull List<Mention> mentions) { public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithPlaceholders(@Nullable CharSequence body, @NonNull List<Mention> mentions) {
@ -131,9 +131,6 @@ public final class MentionUtil {
public static @NonNull String getMentionSettingDisplayValue(@NonNull Context context, @NonNull MentionSetting mentionSetting) { public static @NonNull String getMentionSettingDisplayValue(@NonNull Context context, @NonNull MentionSetting mentionSetting) {
switch (mentionSetting) { switch (mentionSetting) {
case GLOBAL:
return context.getString(SignalStore.notificationSettings().isMentionNotifiesMeEnabled() ? R.string.GroupMentionSettingDialog_default_notify_me
: R.string.GroupMentionSettingDialog_default_dont_notify_me);
case ALWAYS_NOTIFY: case ALWAYS_NOTIFY:
return context.getString(R.string.GroupMentionSettingDialog_always_notify_me); return context.getString(R.string.GroupMentionSettingDialog_always_notify_me);
case DO_NOT_NOTIFY: case DO_NOT_NOTIFY:

View File

@ -279,7 +279,7 @@ public class RecipientDatabase extends Database {
} }
public enum MentionSetting { public enum MentionSetting {
GLOBAL(0), ALWAYS_NOTIFY(1), DO_NOT_NOTIFY(2); ALWAYS_NOTIFY(0), DO_NOT_NOTIFY(1);
private final int id; private final int id;
@ -336,7 +336,7 @@ public class RecipientDatabase extends Database {
GROUPS_V2_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " + GROUPS_V2_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " + STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " +
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ", " + DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ", " +
MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.GLOBAL.getId() + ");"; MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.ALWAYS_NOTIFY.getId() + ");";
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID + private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
" FROM " + TABLE_NAME + " FROM " + TABLE_NAME +
@ -1160,7 +1160,7 @@ public class RecipientDatabase extends Database {
} }
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null; byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
byte[] identityKey = identityKeyRaw.transform(Base64::decodeOrThrow).orNull();; byte[] identityKey = identityKeyRaw.transform(Base64::decodeOrThrow).orNull();
IdentityDatabase.VerifiedStatus identityStatus = identityStatusRaw.transform(IdentityDatabase.VerifiedStatus::forState).or(IdentityDatabase.VerifiedStatus.DEFAULT); IdentityDatabase.VerifiedStatus identityStatus = identityStatusRaw.transform(IdentityDatabase.VerifiedStatus::forState).or(IdentityDatabase.VerifiedStatus.DEFAULT);
@ -2288,7 +2288,7 @@ public class RecipientDatabase extends Database {
uuidValues.put(SYSTEM_CONTACT_URI, e164Settings.getSystemContactUri()); uuidValues.put(SYSTEM_CONTACT_URI, e164Settings.getSystemContactUri());
uuidValues.put(PROFILE_SHARING, uuidSettings.isProfileSharing() || e164Settings.isProfileSharing()); uuidValues.put(PROFILE_SHARING, uuidSettings.isProfileSharing() || e164Settings.isProfileSharing());
uuidValues.put(GROUPS_V2_CAPABILITY, uuidSettings.getGroupsV2Capability() != Recipient.Capability.UNKNOWN ? uuidSettings.getGroupsV2Capability().serialize() : e164Settings.getGroupsV2Capability().serialize()); uuidValues.put(GROUPS_V2_CAPABILITY, uuidSettings.getGroupsV2Capability() != Recipient.Capability.UNKNOWN ? uuidSettings.getGroupsV2Capability().serialize() : e164Settings.getGroupsV2Capability().serialize());
uuidValues.put(MENTION_SETTING, uuidSettings.getMentionSetting() != MentionSetting.GLOBAL ? uuidSettings.getMentionSetting().getId() : e164Settings.getMentionSetting().getId()); uuidValues.put(MENTION_SETTING, uuidSettings.getMentionSetting() != MentionSetting.ALWAYS_NOTIFY ? uuidSettings.getMentionSetting().getId() : e164Settings.getMentionSetting().getId());
if (uuidSettings.getProfileKey() != null) { if (uuidSettings.getProfileKey() != null) {
updateProfileValuesForMerge(uuidValues, uuidSettings); updateProfileValuesForMerge(uuidValues, uuidSettings);
} else if (e164Settings.getProfileKey() != null) { } else if (e164Settings.getProfileKey() != null) {

View File

@ -142,8 +142,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int REMAPPED_RECORDS = 67; private static final int REMAPPED_RECORDS = 67;
private static final int MENTIONS = 68; private static final int MENTIONS = 68;
private static final int PINNED_CONVERSATIONS = 69; private static final int PINNED_CONVERSATIONS = 69;
private static final int MENTION_GLOBAL_SETTING_MIGRATION = 70;
private static final int DATABASE_VERSION = 69; private static final int DATABASE_VERSION = 70;
private static final String DATABASE_NAME = "signal.db"; private static final String DATABASE_NAME = "signal.db";
private final Context context; private final Context context;
@ -997,6 +998,16 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("CREATE INDEX IF NOT EXISTS thread_pinned_index ON thread (pinned)"); db.execSQL("CREATE INDEX IF NOT EXISTS thread_pinned_index ON thread (pinned)");
} }
if (oldVersion < MENTION_GLOBAL_SETTING_MIGRATION) {
ContentValues updateAlways = new ContentValues();
updateAlways.put("mention_setting", 0);
db.update("recipient", updateAlways, "mention_setting = 1", null);
ContentValues updateNever = new ContentValues();
updateNever.put("mention_setting", 1);
db.update("recipient", updateNever, "mention_setting = 2", null);
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.MentionUtil; import org.thoughtcrime.securesms.database.MentionUtil;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.loaders.MediaLoader; import org.thoughtcrime.securesms.database.loaders.MediaLoader;
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader; import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
import org.thoughtcrime.securesms.groups.GroupAccessControl; import org.thoughtcrime.securesms.groups.GroupAccessControl;
@ -261,7 +260,7 @@ public class ManageGroupViewModel extends ViewModel {
} }
void handleMentionNotificationSelection() { void handleMentionNotificationSelection() {
manageGroupRepository.getRecipient(r -> GroupMentionSettingDialog.show(context, r.getMentionSetting(), mentionSetting -> manageGroupRepository.setMentionSetting(mentionSetting))); manageGroupRepository.getRecipient(r -> GroupMentionSettingDialog.show(context, r.getMentionSetting(), manageGroupRepository::setMentionSetting));
} }
private void onBlockAndLeaveConfirmed() { private void onBlockAndLeaveConfirmed() {

View File

@ -14,7 +14,6 @@ import androidx.core.util.Consumer;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting; import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
public final class GroupMentionSettingDialog { public final class GroupMentionSettingDialog {
@ -32,31 +31,24 @@ public final class GroupMentionSettingDialog {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private static View getView(@NonNull Context context, @NonNull MentionSetting mentionSetting, @NonNull SelectionCallback selectionCallback) { private static View getView(@NonNull Context context, @NonNull MentionSetting mentionSetting, @NonNull SelectionCallback selectionCallback) {
View root = LayoutInflater.from(context).inflate(R.layout.group_mention_setting_dialog, null, false); View root = LayoutInflater.from(context).inflate(R.layout.group_mention_setting_dialog, null, false);
CheckedTextView defaultOption = root.findViewById(R.id.group_mention_setting_default);
CheckedTextView alwaysNotify = root.findViewById(R.id.group_mention_setting_always_notify); CheckedTextView alwaysNotify = root.findViewById(R.id.group_mention_setting_always_notify);
CheckedTextView dontNotify = root.findViewById(R.id.group_mention_setting_dont_notify); CheckedTextView dontNotify = root.findViewById(R.id.group_mention_setting_dont_notify);
defaultOption.setText(SignalStore.notificationSettings().isMentionNotifiesMeEnabled() ? R.string.GroupMentionSettingDialog_default_notify_me
: R.string.GroupMentionSettingDialog_default_dont_notify_me);
View.OnClickListener listener = (v) -> { View.OnClickListener listener = (v) -> {
defaultOption.setChecked(defaultOption == v);
alwaysNotify.setChecked(alwaysNotify == v); alwaysNotify.setChecked(alwaysNotify == v);
dontNotify.setChecked(dontNotify == v); dontNotify.setChecked(dontNotify == v);
if (defaultOption.isChecked()) selectionCallback.selection = MentionSetting.GLOBAL; if (alwaysNotify.isChecked()) {
else if (alwaysNotify.isChecked()) selectionCallback.selection = MentionSetting.ALWAYS_NOTIFY; selectionCallback.selection = MentionSetting.ALWAYS_NOTIFY;
else if (dontNotify.isChecked()) selectionCallback.selection = MentionSetting.DO_NOT_NOTIFY; } else if (dontNotify.isChecked()) {
selectionCallback.selection = MentionSetting.DO_NOT_NOTIFY;
}
}; };
defaultOption.setOnClickListener(listener);
alwaysNotify.setOnClickListener(listener); alwaysNotify.setOnClickListener(listener);
dontNotify.setOnClickListener(listener); dontNotify.setOnClickListener(listener);
switch (mentionSetting) { switch (mentionSetting) {
case GLOBAL:
listener.onClick(defaultOption);
break;
case ALWAYS_NOTIFY: case ALWAYS_NOTIFY:
listener.onClick(alwaysNotify); listener.onClick(alwaysNotify);
break; break;

View File

@ -1,20 +0,0 @@
package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
public class NotificationSettings extends SignalStoreValues {
public static final String MENTIONS_NOTIFY_ME = "notifications.mentions.notify_me";
NotificationSettings(@NonNull KeyValueStore store) {
super(store);
}
@Override
void onFirstEverAppLaunch() {
}
public boolean isMentionNotifiesMeEnabled() {
return getBoolean(MENTIONS_NOTIFY_ME, true);
}
}

View File

@ -24,7 +24,6 @@ public final class SignalStore {
private final MiscellaneousValues misc; private final MiscellaneousValues misc;
private final InternalValues internalValues; private final InternalValues internalValues;
private final EmojiValues emojiValues; private final EmojiValues emojiValues;
private final NotificationSettings notificationSettings;
private SignalStore() { private SignalStore() {
this.store = ApplicationDependencies.getKeyValueStore(); this.store = ApplicationDependencies.getKeyValueStore();
@ -38,7 +37,6 @@ public final class SignalStore {
this.misc = new MiscellaneousValues(store); this.misc = new MiscellaneousValues(store);
this.internalValues = new InternalValues(store); this.internalValues = new InternalValues(store);
this.emojiValues = new EmojiValues(store); this.emojiValues = new EmojiValues(store);
this.notificationSettings = new NotificationSettings(store);
} }
public static void onFirstEverAppLaunch() { public static void onFirstEverAppLaunch() {
@ -51,7 +49,6 @@ public final class SignalStore {
tooltips().onFirstEverAppLaunch(); tooltips().onFirstEverAppLaunch();
misc().onFirstEverAppLaunch(); misc().onFirstEverAppLaunch();
internalValues().onFirstEverAppLaunch(); internalValues().onFirstEverAppLaunch();
notificationSettings().onFirstEverAppLaunch();
} }
public static @NonNull KbsValues kbsValues() { public static @NonNull KbsValues kbsValues() {
@ -94,10 +91,6 @@ public final class SignalStore {
return INSTANCE.emojiValues; return INSTANCE.emojiValues;
} }
public static @NonNull NotificationSettings notificationSettings() {
return INSTANCE.notificationSettings;
}
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() { public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
return new GroupsV2AuthorizationSignalStoreCache(getStore()); return new GroupsV2AuthorizationSignalStoreCache(getStore());
} }

View File

@ -1,13 +1,11 @@
package org.thoughtcrime.securesms.longmessage; package org.thoughtcrime.securesms.longmessage;
import androidx.lifecycle.ViewModelProviders;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan; import android.text.style.URLSpan;
@ -16,15 +14,19 @@ import android.util.TypedValue;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProviders;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.ConversationItemFooter; import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -36,6 +38,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.views.Stub; import org.thoughtcrime.securesms.util.views.Stub;
import static org.thoughtcrime.securesms.util.ThemeUtil.isDarkTheme;
public class LongMessageActivity extends PassphraseRequiredActivity { public class LongMessageActivity extends PassphraseRequiredActivity {
private static final String KEY_CONVERSATION_RECIPIENT = "recipient_id"; private static final String KEY_CONVERSATION_RECIPIENT = "recipient_id";
@ -144,7 +148,7 @@ public class LongMessageActivity extends PassphraseRequiredActivity {
bubble.getBackground().setColorFilter(message.get().getMessageRecord().getRecipient().getColor().toConversationColor(this), PorterDuff.Mode.MULTIPLY); bubble.getBackground().setColorFilter(message.get().getMessageRecord().getRecipient().getColor().toConversationColor(this), PorterDuff.Mode.MULTIPLY);
} }
TextView text = bubble.findViewById(R.id.longmessage_text); EmojiTextView text = bubble.findViewById(R.id.longmessage_text);
ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer); ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer);
CharSequence trimmedBody = getTrimmedBody(message.get().getFullBody(this)); CharSequence trimmedBody = getTrimmedBody(message.get().getFullBody(this));
@ -154,6 +158,11 @@ public class LongMessageActivity extends PassphraseRequiredActivity {
text.setText(styledBody); text.setText(styledBody);
text.setMovementMethod(LinkMovementMethod.getInstance()); text.setMovementMethod(LinkMovementMethod.getInstance());
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(this)); text.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(this));
if (message.get().getMessageRecord().isOutgoing()) {
text.setMentionBackgroundTint(ContextCompat.getColor(this, isDarkTheme(this) ? R.color.core_grey_60 : R.color.core_grey_20));
} else {
text.setMentionBackgroundTint(ContextCompat.getColor(this, R.color.transparent_black_40));
}
footer.setMessageRecord(message.get().getMessageRecord(), dynamicLanguage.getCurrentLocale()); footer.setMessageRecord(message.get().getMessageRecord(), dynamicLanguage.getCurrentLocale());
}); });
} }

View File

@ -23,9 +23,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ContextThemeWrapper; import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.util.Pair;
import androidx.core.util.Supplier; import androidx.core.util.Supplier;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -51,6 +54,7 @@ import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewMod
import org.thoughtcrime.securesms.imageeditor.model.EditorModel; import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.HudState;
import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.ViewOnceState; import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.ViewOnceState;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
@ -66,8 +70,11 @@ import org.thoughtcrime.securesms.util.IOFunction;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.thoughtcrime.securesms.util.views.Stub; import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.video.VideoUtil; import org.thoughtcrime.securesms.video.VideoUtil;
@ -82,6 +89,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
/** /**
@ -120,8 +128,9 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
private @Nullable LiveRecipient recipient; private @Nullable LiveRecipient recipient;
private TransportOption transport; private TransportOption transport;
private MediaSendViewModel viewModel; private MediaSendViewModel viewModel;
private MentionsPickerViewModel mentionsViewModel;
private InputAwareLayout hud; private InputAwareLayout hud;
private View captionAndRail; private View captionAndRail;
@ -317,8 +326,8 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
emojiToggle.setOnClickListener(this::onEmojiToggleClicked); emojiToggle.setOnClickListener(this::onEmojiToggleClicked);
} }
initViewModel();
if (FeatureFlags.mentions()) initializeMentionsViewModel(); if (FeatureFlags.mentions()) initializeMentionsViewModel();
initViewModel();
revealButton.setOnClickListener(v -> viewModel.onRevealButtonToggled()); revealButton.setOnClickListener(v -> viewModel.onRevealButtonToggled());
continueButton.setOnClickListener(v -> navigateToContactSelect()); continueButton.setOnClickListener(v -> navigateToContactSelect());
@ -610,6 +619,27 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
} }
private void initViewModel() { private void initViewModel() {
LiveData<Pair<HudState, Boolean>> hudStateAndMentionShowing = LiveDataUtil.combineLatest(viewModel.getHudState(),
mentionsViewModel != null ? mentionsViewModel.isShowing()
: new MutableLiveData<>(false),
Pair::new);
hudStateAndMentionShowing.observe(this, p -> {
HudState state = Objects.requireNonNull(p.first);
boolean isMentionPickerShowing = Objects.requireNonNull(p.second);
int captionBackground = R.color.transparent_black_40;
if (state.getRailState() == MediaSendViewModel.RailState.VIEWABLE) {
captionBackground = R.color.core_grey_90;
} else if (state.getViewOnceState() == ViewOnceState.ENABLED) {
captionBackground = 0;
} else if (isMentionPickerShowing){
captionBackground = ThemeUtil.getThemedResourceId(this, R.attr.mention_picker_background_color);
}
captionAndRail.setBackgroundResource(captionBackground);
});
viewModel.getHudState().observe(this, state -> { viewModel.getHudState().observe(this, state -> {
if (state == null) return; if (state == null) return;
@ -617,18 +647,6 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
composeContainer.setVisibility(state.isComposeVisible() ? View.VISIBLE : (state.getViewOnceState() == ViewOnceState.GONE ? View.GONE : View.INVISIBLE)); composeContainer.setVisibility(state.isComposeVisible() ? View.VISIBLE : (state.getViewOnceState() == ViewOnceState.GONE ? View.GONE : View.INVISIBLE));
captionText.setVisibility(state.isCaptionVisible() ? View.VISIBLE : View.GONE); captionText.setVisibility(state.isCaptionVisible() ? View.VISIBLE : View.GONE);
int captionBackground;
if (state.getRailState() == MediaSendViewModel.RailState.VIEWABLE) {
captionBackground = R.color.core_grey_90;
} else if (state.getViewOnceState() == ViewOnceState.ENABLED) {
captionBackground = 0;
} else {
captionBackground = R.color.transparent_black_40;
}
captionAndRail.setBackgroundResource(captionBackground);
switch (state.getButtonState()) { switch (state.getButtonState()) {
case SEND: case SEND:
sendButtonContainer.setVisibility(View.VISIBLE); sendButtonContainer.setVisibility(View.VISIBLE);
@ -766,7 +784,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
return; return;
} }
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class); mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
recipient.observe(this, mentionsViewModel::onRecipientChange); recipient.observe(this, mentionsViewModel::onRecipientChange);
composeText.setMentionQueryChangedListener(query -> { composeText.setMentionQueryChangedListener(query -> {
@ -799,6 +817,17 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
} }
composeText.replaceTextWithMention(replacementDisplayName, recipient.getId()); composeText.replaceTextWithMention(replacementDisplayName, recipient.getId());
}); });
MentionPickerPlacer mentionPickerPlacer = new MentionPickerPlacer();
mentionsViewModel.isShowing().observe(this, isShowing -> {
if (isShowing) {
composeRow.getViewTreeObserver().addOnGlobalLayoutListener(mentionPickerPlacer);
} else {
composeRow.getViewTreeObserver().removeOnGlobalLayoutListener(mentionPickerPlacer);
}
mentionPickerPlacer.onGlobalLayout();
});
} }
private void presentRecipient(@Nullable Recipient recipient) { private void presentRecipient(@Nullable Recipient recipient) {
@ -979,4 +1008,34 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
@Override @Override
public void onFocusChange(View v, boolean hasFocus) {} public void onFocusChange(View v, boolean hasFocus) {}
} }
private class MentionPickerPlacer implements ViewTreeObserver.OnGlobalLayoutListener {
private final int composeMargin;
private final ViewGroup parent;
private final Rect composeCoordinates;
private int previousBottomMargin;
public MentionPickerPlacer() {
parent = findViewById(android.R.id.content);
composeMargin = ViewUtil.dpToPx(12);
composeCoordinates = new Rect();
}
@Override
public void onGlobalLayout() {
composeRow.getDrawingRect(composeCoordinates);
parent.offsetDescendantRectToMyCoords(composeRow, composeCoordinates);
int marginBottom = parent.getHeight() - composeCoordinates.top + composeMargin;
if (marginBottom != previousBottomMargin) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mentionSuggestions.get().getLayoutParams();
params.setMargins(0, 0, 0, marginBottom);
mentionSuggestions.get().setLayoutParams(params);
previousBottomMargin = marginBottom;
}
}
}
} }

View File

@ -536,12 +536,9 @@ public class DefaultMessageNotifier implements MessageNotifier {
boolean includeMessage = true; boolean includeMessage = true;
if (threadRecipients != null && threadRecipients.isMuted()) { if (threadRecipients != null && threadRecipients.isMuted()) {
RecipientDatabase.MentionSetting mentionSetting = threadRecipients.getMentionSetting(); boolean mentionsOverrideMute = threadRecipients.getMentionSetting() == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY;
boolean overrideMuted = (mentionSetting == RecipientDatabase.MentionSetting.GLOBAL && SignalStore.notificationSettings().isMentionNotifiesMeEnabled()) || includeMessage = FeatureFlags.mentions() && mentionsOverrideMute && record.hasSelfMention();
mentionSetting == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY;
includeMessage = FeatureFlags.mentions() && overrideMuted && record.hasSelfMention();
} }
if (threadRecipients == null || includeMessage) { if (threadRecipients == null || includeMessage) {

View File

@ -9,21 +9,17 @@ import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings; import android.provider.Settings;
import android.text.TextUtils;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.preference.ListPreference; import androidx.preference.ListPreference;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceDataStore;
import android.text.TextUtils;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.NotificationSettings;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
@ -120,18 +116,11 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
initializeCallRingtoneSummary(findPreference(TextSecurePreferences.CALL_RINGTONE_PREF)); initializeCallRingtoneSummary(findPreference(TextSecurePreferences.CALL_RINGTONE_PREF));
initializeMessageVibrateSummary((SwitchPreferenceCompat)findPreference(TextSecurePreferences.VIBRATE_PREF)); initializeMessageVibrateSummary((SwitchPreferenceCompat)findPreference(TextSecurePreferences.VIBRATE_PREF));
initializeCallVibrateSummary((SwitchPreferenceCompat)findPreference(TextSecurePreferences.CALL_VIBRATE_PREF)); initializeCallVibrateSummary((SwitchPreferenceCompat)findPreference(TextSecurePreferences.CALL_VIBRATE_PREF));
if (FeatureFlags.mentions()) {
initializeMentionsNotifyMeSummary((SwitchPreferenceCompat)findPreference(NotificationSettings.MENTIONS_NOTIFY_ME));
}
} }
@Override @Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences_notifications); addPreferencesFromResource(R.xml.preferences_notifications);
if (FeatureFlags.mentions()) {
addPreferencesFromResource(R.xml.preferences_notifications_mentions);
}
} }
@Override @Override
@ -209,11 +198,6 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
pref.setChecked(TextSecurePreferences.isCallNotificationVibrateEnabled(getContext())); pref.setChecked(TextSecurePreferences.isCallNotificationVibrateEnabled(getContext()));
} }
private void initializeMentionsNotifyMeSummary(SwitchPreferenceCompat pref) {
pref.setPreferenceDataStore(SignalStore.getPreferenceDataStore());
pref.setChecked(SignalStore.notificationSettings().isMentionNotifiesMeEnabled());
}
public static CharSequence getSummary(Context context) { public static CharSequence getSummary(Context context) {
final int onCapsResId = R.string.ApplicationPreferencesActivity_On; final int onCapsResId = R.string.ApplicationPreferencesActivity_On;
final int offCapsResId = R.string.ApplicationPreferencesActivity_Off; final int offCapsResId = R.string.ApplicationPreferencesActivity_Off;

View File

@ -318,7 +318,7 @@ public class Recipient {
this.storageId = null; this.storageId = null;
this.identityKey = null; this.identityKey = null;
this.identityStatus = VerifiedStatus.DEFAULT; this.identityStatus = VerifiedStatus.DEFAULT;
this.mentionSetting = MentionSetting.GLOBAL; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
} }
public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) { public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) {
@ -413,6 +413,17 @@ public class Recipient {
return StringUtil.isolateBidi(name); return StringUtil.isolateBidi(name);
} }
public @NonNull String getMentionDisplayName(@NonNull Context context) {
String name = Util.getFirstNonEmpty(localNumber ? getProfileName().toString() : getName(context),
localNumber ? getName(context) : getProfileName().toString(),
getDisplayUsername(),
e164,
email,
context.getString(R.string.Recipient_unknown));
return StringUtil.isolateBidi(name);
}
public @NonNull String getShortDisplayName(@NonNull Context context) { public @NonNull String getShortDisplayName(@NonNull Context context) {
String name = Util.getFirstNonEmpty(getName(context), String name = Util.getFirstNonEmpty(getName(context),
getProfileName().getGivenName(), getProfileName().getGivenName(),

View File

@ -164,7 +164,7 @@ public class RecipientDetails {
this.storageId = null; this.storageId = null;
this.identityKey = null; this.identityKey = null;
this.identityStatus = VerifiedStatus.DEFAULT; this.identityStatus = VerifiedStatus.DEFAULT;
this.mentionSetting = MentionSetting.GLOBAL; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
} }
public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientSettings settings) { public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientSettings settings) {

View File

@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import java.util.Objects;
public final class ContextUtil {
private ContextUtil() {}
public static @NonNull Drawable requireDrawable(@NonNull Context context, @DrawableRes int drawable) {
return Objects.requireNonNull(ContextCompat.getDrawable(context, drawable));
}
}

View File

@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import androidx.annotation.NonNull;
public final class VibrateUtil {
private static final int TICK_LENGTH = 30;
private VibrateUtil() { }
public static void vibrateTick(@NonNull Context context) {
Vibrator vibrator = ServiceUtil.getVibrator(context);
if (Build.VERSION.SDK_INT >= 26) {
VibrationEffect effect = VibrationEffect.createOneShot(TICK_LENGTH, 64);
vibrator.vibrate(effect);
} else {
vibrator.vibrate(TICK_LENGTH);
}
}
}

View File

@ -15,21 +15,6 @@
android:paddingRight="?attr/dialogPreferredPadding" android:paddingRight="?attr/dialogPreferredPadding"
android:text="@string/GroupMentionSettingDialog_receive_notifications_when_youre_mentioned_in_muted_chats" /> android:text="@string/GroupMentionSettingDialog_receive_notifications_when_youre_mentioned_in_muted_chats" />
<CheckedTextView
android:id="@+id/group_mention_setting_default"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="20dp"
android:gravity="center_vertical"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:paddingStart="20dp"
android:paddingEnd="?attr/dialogPreferredPadding"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
tools:text="@string/GroupMentionSettingDialog_default_notify_me" />
<CheckedTextView <CheckedTextView
android:id="@+id/group_mention_setting_always_notify" android:id="@+id/group_mention_setting_always_notify"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -27,13 +27,6 @@
android:clickable="true" android:clickable="true"
android:background="@color/transparent_black_40"> android:background="@color/transparent_black_40">
<ViewStub
android:id="@+id/mediasend_mention_suggestions_stub"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout="@layout/conversation_mention_suggestions_stub"/>
<org.thoughtcrime.securesms.components.emoji.EmojiEditText <org.thoughtcrime.securesms.components.emoji.EmojiEditText
android:id="@+id/mediasend_caption" android:id="@+id/mediasend_caption"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -192,4 +185,17 @@
</org.thoughtcrime.securesms.components.InputAwareLayout> </org.thoughtcrime.securesms.components.InputAwareLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
android:id="@+id/mediasend_mention_suggestions_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:layout="@layout/conversation_mention_suggestions_stub"/>
</FrameLayout>
</FrameLayout> </FrameLayout>

View File

@ -2,28 +2,40 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:clipChildren="false">
<LinearLayout <FrameLayout
android:id="@+id/mentions_picker_bottom_sheet" android:id="@+id/mentions_picker_bottom_sheet"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
app:behavior_hideable="false" app:behavior_peekHeight="236dp"
app:behavior_peekHeight="0dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<View <View
android:id="@+id/mentions_picker_top_divider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="2dp"
android:background="?conversation_mention_divider_color"/> android:layout_gravity="top"
android:layout_marginTop="-2dp"
android:background="@drawable/compose_divider_background"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/mentions_picker_list" android:id="@+id/mentions_picker_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/windowBackground" /> android:background="?mention_picker_background_color" />
</LinearLayout> </FrameLayout>
<View
android:id="@+id/mentions_picker_bottom_divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_gravity="bottom"
android:background="@drawable/compose_divider_background"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,19 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="12dp" android:paddingStart="12dp"
android:paddingTop="10dp" android:paddingTop="8dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingBottom="10dp"> android:paddingBottom="8dp">
<org.thoughtcrime.securesms.components.AvatarImageView <org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/mention_recipient_avatar" android:id="@+id/mention_recipient_avatar"
android:layout_width="28dp" android:layout_width="36dp"
android:layout_height="28dp" android:layout_height="36dp"
app:fallbackImageSize="small"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<TextView <TextView

View File

@ -90,7 +90,8 @@
<attr name="conversation_title_color" format="reference" /> <attr name="conversation_title_color" format="reference" />
<attr name="conversation_subtitle_color" format="reference" /> <attr name="conversation_subtitle_color" format="reference" />
<attr name="conversation_mention_background_color" format="reference" /> <attr name="conversation_mention_background_color" format="reference" />
<attr name="conversation_mention_divider_color" format="reference" />
<attr name="mention_picker_background_color" format="reference" />
<attr name="emoji_tab_strip_background" format="color" /> <attr name="emoji_tab_strip_background" format="color" />
<attr name="emoji_tab_indicator" format="color" /> <attr name="emoji_tab_indicator" format="color" />

View File

@ -159,6 +159,4 @@
<dimen name="group_manage_fragment_row_horizontal_padding">16dp</dimen> <dimen name="group_manage_fragment_row_horizontal_padding">16dp</dimen>
<dimen name="wave_form_bar_width">2dp</dimen> <dimen name="wave_form_bar_width">2dp</dimen>
<dimen name="mentions_picker_peek_height">216dp</dimen>
</resources> </resources>

View File

@ -263,7 +263,8 @@
<item name="conversation_title_color">@color/white</item> <item name="conversation_title_color">@color/white</item>
<item name="conversation_subtitle_color">@color/transparent_white_90</item> <item name="conversation_subtitle_color">@color/transparent_white_90</item>
<item name="conversation_mention_background_color">@color/core_grey_20</item> <item name="conversation_mention_background_color">@color/core_grey_20</item>
<item name="conversation_mention_divider_color">@color/core_grey_05</item>
<item name="mention_picker_background_color">@color/core_white</item>
<item name="safety_number_change_dialog_button_background">@color/core_grey_05</item> <item name="safety_number_change_dialog_button_background">@color/core_grey_05</item>
<item name="safety_number_change_dialog_button_text_color">@color/core_ultramarine</item> <item name="safety_number_change_dialog_button_text_color">@color/core_ultramarine</item>
@ -624,11 +625,12 @@
<item name="conversation_popup_theme">@style/ThemeOverlay.AppCompat.Dark</item> <item name="conversation_popup_theme">@style/ThemeOverlay.AppCompat.Dark</item>
<item name="conversation_title_color">@color/transparent_white_90</item> <item name="conversation_title_color">@color/transparent_white_90</item>
<item name="conversation_subtitle_color">@color/transparent_white_80</item> <item name="conversation_subtitle_color">@color/transparent_white_80</item>
<item name="conversation_mention_background_color">@color/core_grey_75</item> <item name="conversation_mention_background_color">@color/core_grey_60</item>
<item name="conversation_mention_divider_color">@color/core_grey_25</item>
<item name="conversation_scroll_to_bottom_background">@drawable/scroll_to_bottom_background_dark</item> <item name="conversation_scroll_to_bottom_background">@drawable/scroll_to_bottom_background_dark</item>
<item name="conversation_scroll_to_bottom_foreground_color">@color/core_white</item> <item name="conversation_scroll_to_bottom_foreground_color">@color/core_white</item>
<item name="mention_picker_background_color">@color/core_grey_90</item>
<item name="reactions_overlay_toolbar_icon_tint">@color/core_white</item> <item name="reactions_overlay_toolbar_icon_tint">@color/core_white</item>
<item name="reactions_overlay_toolbar_overflow_style">@style/Signal.Toolbar.Overflow</item> <item name="reactions_overlay_toolbar_overflow_style">@style/Signal.Toolbar.Overflow</item>
<item name="reactions_overlay_toolbar_background_color">@color/action_mode_status_bar</item> <item name="reactions_overlay_toolbar_background_color">@color/action_mode_status_bar</item>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:layout="@layout/preference_divider"/>
<PreferenceCategory android:title="@string/preferences_notifications__mentions">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:key="notifications.mentions.notify_me"
android:title="@string/preferences_notifications__notify_me"
android:summary="@string/preferences_notifications__receive_notifications_when_youre_mentioned_in_muted_chats"
android:defaultValue="true" />
</PreferenceCategory>
</PreferenceScreen>