session-android/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ViewUtil.java
ceokot 16ca97d2d3
Add emoji reacts support (#889)
* feat: Add emoji reacts support

* Remove message multi-selection

* Add emoji reaction model

* Add emoji reaction panel

* Blur reacts panel background

* Show emoji keyboard

* Add emoji sprites

* Update reaction proto

* Emoji database updates

* Emoji database refactor

* Emoji reaction persistence

* Optimize reactions retrieval

* Fix emoji group query

* Display emojis

* Fix emoji persistence

* Cleanup

* Persistence refactor

* Add reactions bottom sheet

* Cleanup

* Ui tweaks

* React with any emoji

* Show emoji react notifications

* Remove reaction

* Show reactions modal on long press

* Click to react (+1) with an emoji

* Click to react with an emoji

* Enable emoji expand/collapse

* fix: some compile issues from merge conflicts

* fix: compile issues merging quote and media message UI

* fix: xml IDs and adding in legacy is selected for future inclusion

* Fix view constraints

* Fix merge issue

* Add message selection option in conversation context menu

* Add sogs emoji integration

* Handle sogs emoji reactions

* Enable sending/deleting sogs emojis

* fix: improve the visible message layout

* fix: add file IDs to request parameters for message send (#940)

* Fix open group polling from seqno instead of last hash (#939)

* fix: reset seqno to get recent messages from open groups

* build: upgrade build numbers

* fix: actually run the migration

* Using StringBuilder to construct request url

* Fix reaction filter

* fix: is_mms added in second projection query

* Update default emojis

* fix: include legacy and new open groups in server ID tracking (#941)

* feat: add hidden moderator and admin roles, separated as they may be used independently in future (#942)

* Cleanup

* Fix view constraints

* Add reactions capability check

* Fix reactions alignment

* Ui fixes

* Display reactions list

* feat: add formatted count strings

* fix: account for negatives and add tests

* Migrate old official open group locations for polling and adding (#932)

* feat: adding in first part of open group migrations and tests for migration logic / helpers

* feat: test code and migration logic for open groups in the case of no conflicts

* feat: add in extra test cases and refactor code for migrator

* refactor: migrate open group join URLs and references to server in adding new open groups to catch legacy and re-write it

* refactor: joining open groups using OpenGroupUrlParser.kt now

* fix: add in compile issues for renamed OpenGroupApi.kt from OpenGroupV2

* fix: prevent duplicates of http/https for new open group DNS and prevent adding new groups based on public key

* fix: room and server swapped parameters

* fix: replace default server for config messages

* fix: actually using public key to de-dupe didn't work for rooms

* build: bump version code and name

* Display reactions list on open groups for moderators

* Ui tweaks

* Ui tweaks for moderation

* Refactor

* fix: compile issue

* fix: de-duping joined queries in the get X from cursor

* Restore import

* fix: colouring the reaction overlay scrubber

* fix: highlight colour, show reaction count if 1 or above

* Cleanup

* fix: light mode accent

* fix: light / dark mode themeing in reactions dialog fragment

* Emoji notification blinded id check

* fix: show reaction list correctly and pass isUserModerator to bind methods

* fix: remove unnecessary places for the moderator

* fix: X button for removing own react not showing up properly

* feat: add clear all header view

* fix: migrate the clear all to the correct location

* fix: use display instead of base

* Truncate emoji sender ids

* feat: add notify thread function in thread db

* Notify threads on reaction received

* fix: design fixes for the reaction list

* fix: emoji reactions bottom sheet dialog UI designs

* feat: add unsupported emoji reaction

* fix: crash and doing vector properly

* Fix reaction database queries

* Fix background open group adder job

* Show new open group reactions

* Fetch a maximum of 5 reactors

* Handle open group reactions polling conflicts

* Add count to user reaction

* Show number of additional reactors

* fix: unreads set same as the unread query

* fix: design changes

* fix: update dependency to improve flexboxlayout behaviour, design consistencies

* Add select message icon and update long press menu items order and wording

* Fix crash on reactors dialog

* fix: colours and backgrounds to match designs

* fix: add header in recipient item

* fix: margins

* fix: alignments and layout issues for emoji reactions view

* feat: add overflow previews and logic for overflow

* Dim action bar

* Add emoji search

* Search index fix

* Set count for 1:1 and closed group reactions when inserting in local database

* Use on screen toolbar to allow overlaying

* Show/hide scroll to bottom button

* feat: add extended properties so it doesn't collapse on re-bind

* Cleanup

* feat: prevent keeping extended on rebinding if we get a new message ID

* fix: long press works on devices now, fix release lint issue and crash for emoji search DBs from emoji builds

* Display message timestamp

* Fix modal items alignment

* fix: sort order and emoji count in compareTo

* Scale down really large messages to fit

* Prevent closed group crash

* Fix reaction author

Co-authored-by: charles <charles@oxen.io>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
2022-09-04 21:03:32 +10:00

381 lines
13 KiB
Java

/**
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.conversation.v2;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import androidx.lifecycle.Lifecycle;
import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.Stub;
import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.SettableFuture;
public final class ViewUtil {
private ViewUtil() {
}
public static void focusAndMoveCursorToEndAndOpenKeyboard(@NonNull EditText input) {
int numberLength = input.getText().length();
input.setSelection(numberLength, numberLength);
focusAndShowKeyboard(input);
}
public static void focusAndShowKeyboard(@NonNull View view) {
view.requestFocus();
if (view.hasWindowFocus()) {
showTheKeyboardNow(view);
} else {
view.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus) {
showTheKeyboardNow(view);
view.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
}
}
});
}
}
private static void showTheKeyboardNow(@NonNull View view) {
if (view.isFocused()) {
view.post(() -> {
InputMethodManager inputMethodManager = ServiceUtil.getInputMethodManager(view.getContext());
inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
});
}
}
@SuppressWarnings("unchecked")
public static <T extends View> T inflateStub(@NonNull View parent, @IdRes int stubId) {
return (T)((ViewStub)parent.findViewById(stubId)).inflate();
}
public static <T extends View> Stub<T> findStubById(@NonNull Activity parent, @IdRes int resId) {
return new Stub<>(parent.findViewById(resId));
}
public static <T extends View> Stub<T> findStubById(@NonNull View parent, @IdRes int resId) {
return new Stub<>(parent.findViewById(resId));
}
private static Animation getAlphaAnimation(float from, float to, int duration) {
final Animation anim = new AlphaAnimation(from, to);
anim.setInterpolator(new FastOutSlowInInterpolator());
anim.setDuration(duration);
return anim;
}
public static void fadeIn(final @NonNull View view, final int duration) {
animateIn(view, getAlphaAnimation(0f, 1f, duration));
}
public static ListenableFuture<Boolean> fadeOut(final @NonNull View view, final int duration) {
return fadeOut(view, duration, View.GONE);
}
public static ListenableFuture<Boolean> fadeOut(@NonNull View view, int duration, int visibility) {
return animateOut(view, getAlphaAnimation(1f, 0f, duration), visibility);
}
public static ListenableFuture<Boolean> animateOut(final @NonNull View view, final @NonNull Animation animation) {
return animateOut(view, animation, View.GONE);
}
public static ListenableFuture<Boolean> animateOut(final @NonNull View view, final @NonNull Animation animation, final int visibility) {
final SettableFuture future = new SettableFuture();
if (view.getVisibility() == visibility) {
future.set(true);
} else {
view.clearAnimation();
animation.reset();
animation.setStartTime(0);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationRepeat(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
view.setVisibility(visibility);
future.set(true);
}
});
view.startAnimation(animation);
}
return future;
}
public static void animateIn(final @NonNull View view, final @NonNull Animation animation) {
if (view.getVisibility() == View.VISIBLE) return;
view.clearAnimation();
animation.reset();
animation.setStartTime(0);
view.setVisibility(View.VISIBLE);
view.startAnimation(animation);
}
@SuppressWarnings("unchecked")
public static <T extends View> T inflate(@NonNull LayoutInflater inflater,
@NonNull ViewGroup parent,
@LayoutRes int layoutResId)
{
return (T)(inflater.inflate(layoutResId, parent, false));
}
@SuppressLint("RtlHardcoded")
public static void setTextViewGravityStart(final @NonNull TextView textView, @NonNull Context context) {
if (isRtl(context)) {
textView.setGravity(Gravity.RIGHT);
} else {
textView.setGravity(Gravity.LEFT);
}
}
public static void mirrorIfRtl(View view, Context context) {
if (isRtl(context)) {
view.setScaleX(-1.0f);
}
}
public static boolean isLtr(@NonNull View view) {
return isLtr(view.getContext());
}
public static boolean isLtr(@NonNull Context context) {
return context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
}
public static boolean isRtl(@NonNull View view) {
return isRtl(view.getContext());
}
public static boolean isRtl(@NonNull Context context) {
return context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
}
public static float pxToDp(float px) {
return px / Resources.getSystem().getDisplayMetrics().density;
}
public static int dpToPx(Context context, int dp) {
return (int)((dp * context.getResources().getDisplayMetrics().density) + 0.5);
}
public static int dpToPx(int dp) {
return Math.round(dp * Resources.getSystem().getDisplayMetrics().density);
}
public static int dpToSp(int dp) {
return (int) (dpToPx(dp) / Resources.getSystem().getDisplayMetrics().scaledDensity);
}
public static int spToPx(float sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, Resources.getSystem().getDisplayMetrics());
}
public static void updateLayoutParams(@NonNull View view, int width, int height) {
view.getLayoutParams().width = width;
view.getLayoutParams().height = height;
view.requestLayout();
}
public static void updateLayoutParamsIfNonNull(@Nullable View view, int width, int height) {
if (view != null) {
updateLayoutParams(view, width, height);
}
}
public static void setVisibilityIfNonNull(@Nullable View view, int visibility) {
if (view != null) {
view.setVisibility(visibility);
}
}
public static int getLeftMargin(@NonNull View view) {
if (isLtr(view)) {
return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin;
}
return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).rightMargin;
}
public static int getRightMargin(@NonNull View view) {
if (isLtr(view)) {
return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).rightMargin;
}
return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin;
}
public static int getTopMargin(@NonNull View view) {
return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).topMargin;
}
public static void setLeftMargin(@NonNull View view, int margin) {
if (isLtr(view)) {
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin = margin;
} else {
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).rightMargin = margin;
}
view.forceLayout();
view.requestLayout();
}
public static void setRightMargin(@NonNull View view, int margin) {
if (isLtr(view)) {
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).rightMargin = margin;
} else {
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin = margin;
}
view.forceLayout();
view.requestLayout();
}
public static void setTopMargin(@NonNull View view, int margin) {
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).topMargin = margin;
view.requestLayout();
}
public static void setBottomMargin(@NonNull View view, int margin) {
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).bottomMargin = margin;
view.requestLayout();
}
public static int getWidth(@NonNull View view) {
return view.getLayoutParams().width;
}
public static void setPaddingTop(@NonNull View view, int padding) {
view.setPadding(view.getPaddingLeft(), padding, view.getPaddingRight(), view.getPaddingBottom());
}
public static void setPaddingBottom(@NonNull View view, int padding) {
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), padding);
}
public static void setPadding(@NonNull View view, int padding) {
view.setPadding(padding, padding, padding, padding);
}
public static void setPaddingStart(@NonNull View view, int padding) {
if (isLtr(view)) {
view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom());
} else {
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), padding, view.getPaddingBottom());
}
}
public static void setPaddingEnd(@NonNull View view, int padding) {
if (isLtr(view)) {
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), padding, view.getPaddingBottom());
} else {
view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom());
}
}
public static boolean isPointInsideView(@NonNull View view, float x, float y) {
int[] location = new int[2];
view.getLocationOnScreen(location);
int viewX = location[0];
int viewY = location[1];
return x > viewX && x < viewX + view.getWidth() &&
y > viewY && y < viewY + view.getHeight();
}
public static int getStatusBarHeight(@NonNull View view) {
int result = 0;
int resourceId = view.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = view.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
public static int getNavigationBarHeight(@NonNull View view) {
int result = 0;
int resourceId = view.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
result = view.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
public static void hideKeyboard(@NonNull Context context, @NonNull View view) {
InputMethodManager inputManager = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
/**
* Enables or disables a view and all child views recursively.
*/
public static void setEnabledRecursive(@NonNull View view, boolean enabled) {
view.setEnabled(enabled);
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
setEnabledRecursive(viewGroup.getChildAt(i), enabled);
}
}
}
public static @Nullable Lifecycle getActivityLifecycle(@NonNull View view) {
return getActivityLifecycle(view.getContext());
}
private static @Nullable Lifecycle getActivityLifecycle(@Nullable Context context) {
if (context instanceof ContextThemeWrapper) {
return getActivityLifecycle(((ContextThemeWrapper) context).getBaseContext());
}
if (context instanceof AppCompatActivity) {
return ((AppCompatActivity) context).getLifecycle();
}
return null;
}
}