diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b0dba49ac5..173d50b50b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,18 +35,15 @@ task("checkEmojiKeyboard") { val isExperimentalBuild = extra["experimental"] as Boolean? ?: false val properties = extra["properties"] as Properties val projectName = extra["app_name"] as String +val versions = extra["versions"] as Properties android { namespace = "org.thunderdog.challegram" defaultConfig { - val versions = extra["versions"] as Properties - - val ndkVersion = versions.getProperty("version.ndk") val jniVersion = versions.getProperty("version.jni") val leveldbVersion = versions.getProperty("version.leveldb") - buildConfigString("NDK_VERSION", ndkVersion) buildConfigString("JNI_VERSION", jniVersion) buildConfigString("LEVELDB_VERSION", leveldbVersion) @@ -109,7 +106,15 @@ android { dimension = "abi" versionCode = (abi + 1) minSdk = variant.minSdkVersion - buildConfigBool("WEBP_ENABLED", variant.minSdkVersion < 19) + val ndkVersionKey = if (variant.is64Bit) { + "version.ndk_primary" + } else { + "version.ndk_legacy" + } + ndkVersion = versions.getProperty(ndkVersionKey) + ndkPath = File(sdkDirectory, "ndk/$ndkVersion").absolutePath + buildConfigString("NDK_VERSION", ndkVersion) + buildConfigBool("WEBP_ENABLED", true) // variant.minSdkVersion < 19 buildConfigBool("SIDE_LOAD_ONLY", variant.sideLoadOnly) ndk.abiFilters.clear() ndk.abiFilters.addAll(variant.filters) @@ -189,15 +194,15 @@ dependencies { implementation(project(":vkryl:leveldb")) implementation(project(":vkryl:android")) implementation(project(":vkryl:td")) - // AndroidX: https://developer.android.com/jetpack/androidx/releases/ + // AndroidX: https://developer.android.com/jetpack/androidx/versions implementation("androidx.activity:activity:1.7.2") implementation("androidx.palette:palette:1.0.0") - implementation("androidx.recyclerview:recyclerview:1.3.0") + implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.viewpager:viewpager:1.0.0") implementation("androidx.work:work-runtime:2.8.1") implementation("androidx.browser:browser:1.5.0") implementation("androidx.exifinterface:exifinterface:1.3.6") - implementation("androidx.collection:collection:1.2.0") + implementation("androidx.collection:collection:1.3.0") implementation("androidx.interpolator:interpolator:1.0.0") implementation("androidx.gridlayout:gridlayout:1.0.0") // CameraX: https://developer.android.com/jetpack/androidx/releases/camera @@ -220,26 +225,26 @@ dependencies { // Play In-App Updates: https://developer.android.com/reference/com/google/android/play/core/release-notes-in_app_updates implementation("com.google.android.play:app-update:2.1.0") // ExoPlayer: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md - implementation("com.google.android.exoplayer:exoplayer-core:2.19.0") + implementation("com.google.android.exoplayer:exoplayer-core:2.19.1") // 17.x version requires minSdk 19 or higher implementation("com.google.mlkit:language-id:16.1.1") // The Checker Framework: https://checkerframework.org/CHANGELOG.md - compileOnly("org.checkerframework:checker-qual:3.32.0") + compileOnly("org.checkerframework:checker-qual:3.40.0") // OkHttp: https://github.com/square/okhttp/blob/master/CHANGELOG.md - implementation("com.squareup.okhttp3:okhttp:4.9.3") + implementation("com.squareup.okhttp3:okhttp:4.11.0") // ShortcutBadger: https://github.com/leolin310148/ShortcutBadger implementation("me.leolin:ShortcutBadger:1.1.22@aar") // ReLinker: https://github.com/KeepSafe/ReLinker/blob/master/CHANGELOG.md implementation("com.getkeepsafe.relinker:relinker:1.4.5") - // Konfetti: https://github.com/DanielMartinus/Konfetti/blob/master/README.md - implementation("nl.dionsegijn:konfetti-xml:2.0.2") + // Konfetti: https://github.com/DanielMartinus/Konfetti/blob/main/README.md + implementation("nl.dionsegijn:konfetti-xml:2.0.3") // Transcoder: https://github.com/natario1/Transcoder/blob/master/docs/_about/changelog.md implementation("com.github.natario1:Transcoder:ba8f098c94") // https://github.com/mikereedell/sunrisesunsetlib-java implementation("com.luckycatlabs:SunriseSunsetCalculator:1.2") // ZXing: https://github.com/zxing/zxing/blob/master/CHANGES - implementation("com.google.zxing:core:3.4.1") + implementation("com.google.zxing:core:3.5.2") // subsampling-scale-image-view: https://github.com/davemorrissey/subsampling-scale-image-view implementation("com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0") diff --git a/app/jni/BuildTelegramVoIP.cmake b/app/jni/BuildTelegramVoIP.cmake index 1e19f3f8c9..ace83035ff 100644 --- a/app/jni/BuildTelegramVoIP.cmake +++ b/app/jni/BuildTelegramVoIP.cmake @@ -370,7 +370,6 @@ target_compile_definitions(tgvoip PRIVATE WEBRTC_ANDROID FIXED_POINT WEBRTC_NS_FLOAT - __STDC_LIMIT_MACROS ) target_include_directories(tgvoip PRIVATE . @@ -396,4 +395,5 @@ target_link_libraries(tgvoip PUBLIC OpenSLES opus ssl + jni-utils ) \ No newline at end of file diff --git a/app/jni/BuildTgCalls.cmake b/app/jni/BuildTgCalls.cmake index 6e6a09aa24..514b492b38 100644 --- a/app/jni/BuildTgCalls.cmake +++ b/app/jni/BuildTgCalls.cmake @@ -207,9 +207,11 @@ target_include_directories(${TGCALLS_LIB} PRIVATE "${YUV_DIR}/include" "${STUB_DIR}" ) -set_target_properties(${TGCALLS_LIB} PROPERTIES - ANDROID_ARM_MODE arm -) +if (${ANDROID_ABI} STREQUAL "armeabi-v7a" OR ${ANDROID_ABI} STREQUAL "arm64-v8a") + set_target_properties(${TGCALLS_LIB} PROPERTIES + ANDROID_ARM_MODE arm + ) +endif() target_compile_definitions(${TGCALLS_LIB} PUBLIC ${WEBRTC_OPTIONS}) target_compile_definitions(${TGCALLS_LIB} PRIVATE TDLIB_TDAPI_CLASS_PATH="org/drinkless/tdlib/TdApi" diff --git a/app/jni/CMakeLists.txt b/app/jni/CMakeLists.txt index 8cd33b45b7..f1c6a5ce8c 100644 --- a/app/jni/CMakeLists.txt +++ b/app/jni/CMakeLists.txt @@ -18,11 +18,12 @@ set(UTILS_DIR "${THIRDPARTY_DIR}/jni-utils") set(ENABLE_TG_CALLS yes) # Using webp only if building for 32-bit platform -if (${ANDROID_ABI} STREQUAL "armeabi-v7a" OR ${ANDROID_ABI} STREQUAL "x86") - set(USE_WEBP yes) -else() - set(USE_WEBP no) -endif() +#if (${ANDROID_ABI} STREQUAL "armeabi-v7a" OR ${ANDROID_ABI} STREQUAL "x86") +# set(USE_WEBP yes) +#else() +# set(USE_WEBP no) +#endif() +set(USE_WEBP yes) if (${ANDROID_ABI} STREQUAL "x86_64" OR ${ANDROID_ABI} STREQUAL "arm64-v8a") set(FFMPEG_ABI ${ANDROID_ABI}) diff --git a/app/jni/image.c b/app/jni/image.c index caa92921a1..5251cf6184 100644 --- a/app/jni/image.c +++ b/app/jni/image.c @@ -467,13 +467,25 @@ JNIEXPORT jboolean Java_org_thunderdog_challegram_N_loadWebpImage (JNIEnv *env, uint8_t *data = (uint8_t *) inputBuffer; size_t data_size = (size_t) len; - int bitmapWidth = 0; - int bitmapHeight = 0; - if (!WebPGetInfo(data, data_size, &bitmapWidth, &bitmapHeight)) { - (*env)->ThrowNew(env, jclass_RuntimeException, "Invalid WebP format"); + WebPBitstreamFeatures features; + VP8StatusCode resultCode = WebPGetFeatures(data, data_size, &features); + if (resultCode != VP8_STATUS_OK) { + const char *fmt = "WebPGetFeatures failed: %d"; + int code = (int) resultCode; + + int count = snprintf(NULL, 0, fmt, code); + char buffer[count + 1]; + buffer[count + 1] = '\0'; + snprintf(buffer, count + 1, fmt, code); + + (*env)->ThrowNew(env, jclass_RuntimeException, buffer); + return 0; } + int bitmapWidth = features.width; + int bitmapHeight = features.height; + if (options && (*env)->GetBooleanField(env, options, jclass_Options_inJustDecodeBounds) == JNI_TRUE) { (*env)->SetIntField(env, options, jclass_Options_outWidth, bitmapWidth); (*env)->SetIntField(env, options, jclass_Options_outHeight, bitmapHeight); diff --git a/app/jni/third_party/libtgvoip b/app/jni/third_party/libtgvoip index 4a6e810064..652bfa7745 160000 --- a/app/jni/third_party/libtgvoip +++ b/app/jni/third_party/libtgvoip @@ -1 +1 @@ -Subproject commit 4a6e810064edf6613777e489c855dc47a2de91f5 +Subproject commit 652bfa7745be1a5ac00718fd183c245766efa8f2 diff --git a/app/jni/third_party/webp b/app/jni/third_party/webp index b0a860891d..8bacd63a6d 160000 --- a/app/jni/third_party/webp +++ b/app/jni/third_party/webp @@ -1 +1 @@ -Subproject commit b0a860891dcd4c0c2d7c6149e5cccb6eb881cc21 +Subproject commit 8bacd63a6de1cc091f85a1692390401e7bbf55ac diff --git a/app/src/google/java/org/thunderdog/challegram/GoogleLocationRetriever.java b/app/src/google/java/org/thunderdog/challegram/location/GoogleLocationRetriever.java similarity index 97% rename from app/src/google/java/org/thunderdog/challegram/GoogleLocationRetriever.java rename to app/src/google/java/org/thunderdog/challegram/location/GoogleLocationRetriever.java index 4db87177a0..b28c43a31f 100644 --- a/app/src/google/java/org/thunderdog/challegram/GoogleLocationRetriever.java +++ b/app/src/google/java/org/thunderdog/challegram/location/GoogleLocationRetriever.java @@ -12,7 +12,7 @@ * * File created on 22/10/2022 */ -package org.thunderdog.challegram; +package org.thunderdog.challegram.location; import android.location.Location; @@ -26,6 +26,8 @@ import com.google.android.gms.location.LocationSettingsResult; import com.google.android.gms.location.LocationSettingsStatusCodes; +import org.thunderdog.challegram.BaseActivity; +import org.thunderdog.challegram.Log; import org.thunderdog.challegram.tool.Intents; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.unsorted.LocationRetriever; diff --git a/app/src/google/java/org/thunderdog/challegram/LocationRetrieverFactory.java b/app/src/google/java/org/thunderdog/challegram/location/LocationRetrieverFactory.java similarity index 89% rename from app/src/google/java/org/thunderdog/challegram/LocationRetrieverFactory.java rename to app/src/google/java/org/thunderdog/challegram/location/LocationRetrieverFactory.java index 3266da1e07..27eb57e31c 100644 --- a/app/src/google/java/org/thunderdog/challegram/LocationRetrieverFactory.java +++ b/app/src/google/java/org/thunderdog/challegram/location/LocationRetrieverFactory.java @@ -12,8 +12,9 @@ * * File created on 22/10/2022 */ -package org.thunderdog.challegram; +package org.thunderdog.challegram.location; +import org.thunderdog.challegram.BaseActivity; import org.thunderdog.challegram.unsorted.LocationRetriever; public class LocationRetrieverFactory { diff --git a/app/src/google/java/org/thunderdog/challegram/FirebaseTokenRetriever.java b/app/src/google/java/org/thunderdog/challegram/push/FirebaseTokenRetriever.java similarity index 81% rename from app/src/google/java/org/thunderdog/challegram/FirebaseTokenRetriever.java rename to app/src/google/java/org/thunderdog/challegram/push/FirebaseTokenRetriever.java index d0764ad5ff..7a44844aeb 100644 --- a/app/src/google/java/org/thunderdog/challegram/FirebaseTokenRetriever.java +++ b/app/src/google/java/org/thunderdog/challegram/push/FirebaseTokenRetriever.java @@ -12,14 +12,21 @@ * * File created on 22/10/2022 */ -package org.thunderdog.challegram; +package org.thunderdog.challegram.push; import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; import com.google.firebase.messaging.FirebaseMessaging; import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.Log; +import org.thunderdog.challegram.TDLib; +import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.util.TokenRetriever; import java.util.regex.Matcher; @@ -27,7 +34,7 @@ import me.vkryl.core.StringUtils; -public final class FirebaseTokenRetriever extends TokenRetriever { +final class FirebaseTokenRetriever extends TokenRetriever { @Override protected boolean performInitialization (Context context) { try { @@ -44,6 +51,19 @@ protected boolean performInitialization (Context context) { return false; } + @NonNull + @Override + public String getName () { + return "firebase"; + } + + @Override + @Nullable + public String getConfiguration () { + FirebaseOptions options = FirebaseOptions.fromResource(UI.getAppContext()); + return options != null ? options.toString() : null; + } + private static String extractFirebaseErrorName (Throwable e) { String message = e.getMessage(); if (!StringUtils.isEmpty(message)) { diff --git a/app/src/google/java/org/thunderdog/challegram/TokenRetrieverFactory.java b/app/src/google/java/org/thunderdog/challegram/push/TokenRetrieverFactory.java similarity index 89% rename from app/src/google/java/org/thunderdog/challegram/TokenRetrieverFactory.java rename to app/src/google/java/org/thunderdog/challegram/push/TokenRetrieverFactory.java index 81518abaf9..bcbd61ed88 100644 --- a/app/src/google/java/org/thunderdog/challegram/TokenRetrieverFactory.java +++ b/app/src/google/java/org/thunderdog/challegram/push/TokenRetrieverFactory.java @@ -12,13 +12,13 @@ * * File created on 22/10/2022 */ -package org.thunderdog.challegram; +package org.thunderdog.challegram.push; import android.content.Context; import org.thunderdog.challegram.util.TokenRetriever; -public class TokenRetrieverFactory { +public final class TokenRetrieverFactory { public static TokenRetriever newRetriever (Context context) { return new FirebaseTokenRetriever(); } diff --git a/app/src/google/java/org/thunderdog/challegram/service/FirebaseListenerService.java b/app/src/google/java/org/thunderdog/challegram/service/FirebaseListenerService.java index a1555411e0..4134dd0cc1 100644 --- a/app/src/google/java/org/thunderdog/challegram/service/FirebaseListenerService.java +++ b/app/src/google/java/org/thunderdog/challegram/service/FirebaseListenerService.java @@ -34,7 +34,7 @@ import me.vkryl.td.JSON; -public class FirebaseListenerService extends FirebaseMessagingService { +public final class FirebaseListenerService extends FirebaseMessagingService { @Override public void onNewToken (@NonNull String newToken) { UI.initApp(getApplicationContext()); diff --git a/app/src/google/java/org/thunderdog/challegram/ui/MapControllerFactory.java b/app/src/google/java/org/thunderdog/challegram/ui/MapControllerFactory.java index 08ab0f3ad3..6865337c91 100644 --- a/app/src/google/java/org/thunderdog/challegram/ui/MapControllerFactory.java +++ b/app/src/google/java/org/thunderdog/challegram/ui/MapControllerFactory.java @@ -18,7 +18,7 @@ import org.thunderdog.challegram.telegram.Tdlib; -public class MapControllerFactory { +public final class MapControllerFactory { public static MapController newMapController (Context context, Tdlib tdlib) { return new MapGoogleController(context, tdlib); } diff --git a/app/src/google/java/org/thunderdog/challegram/ui/MapGoogleController.java b/app/src/google/java/org/thunderdog/challegram/ui/MapGoogleController.java index da499bc8cd..dd255e3bf7 100644 --- a/app/src/google/java/org/thunderdog/challegram/ui/MapGoogleController.java +++ b/app/src/google/java/org/thunderdog/challegram/ui/MapGoogleController.java @@ -53,9 +53,8 @@ import org.thunderdog.challegram.loader.Watcher; import org.thunderdog.challegram.loader.WatcherReference; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibUi; -import org.thunderdog.challegram.theme.ColorId; -import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; @@ -67,7 +66,7 @@ import me.vkryl.core.lambda.Destroyable; import me.vkryl.td.MessageId; -public class MapGoogleController extends MapController implements OnMapReadyCallback, GoogleMap.OnMyLocationChangeListener, GoogleMap.OnCameraMoveStartedListener, GoogleMap.OnMarkerClickListener { +final class MapGoogleController extends MapController implements OnMapReadyCallback, GoogleMap.OnMyLocationChangeListener, GoogleMap.OnCameraMoveStartedListener, GoogleMap.OnMarkerClickListener { private static final float DEFAULT_ZOOM_LEVEL = 16.0f; private static final float CLICK_ZOOM_LEVEL = 17.0f; @@ -107,10 +106,10 @@ private MarkerOptions newPoint (LocationPoint point) { Bitmap bitmap = null; if (point.isSelfLocation) { TdApi.User user = tdlib.myUser(); - int avatarColorId = tdlib.cache().userAvatarColorId(user); + TdlibAccentColor accentColor = tdlib.cache().userAccentColor(user); Letters letters = tdlib.cache().userLetters(user); TdApi.File avatar = user != null && user.profilePhoto != null ? user.profilePhoto.small : null; - bitmap = newBitmap(this, avatarColorId, letters, avatar); + bitmap = newBitmap(this, accentColor, letters, avatar); } else if (point.isLiveLocation && point.message != null) { opts.zIndex(1f); this.isActive = ((TdApi.MessageLocation) point.message.content).expiresIn > 0; @@ -125,20 +124,20 @@ private MarkerOptions newPoint (LocationPoint point) { } private @Nullable Bitmap newBitmap (MarkerData data, TdApi.Message message) { - int avatarColorId; + TdlibAccentColor accentColor; Letters letters; TdApi.File avatar; switch (message.senderId.getConstructor()) { case TdApi.MessageSenderChat.CONSTRUCTOR: { TdApi.Chat chat = tdlib.chat(((TdApi.MessageSenderChat) message.senderId).chatId); - avatarColorId = tdlib.chatAvatarColorId(chat); + accentColor = tdlib.chatAccentColor(chat); letters = tdlib.chatLetters(chat); avatar = chat != null && chat.photo != null ? chat.photo.small : null; break; } case TdApi.MessageSenderUser.CONSTRUCTOR: { TdApi.User user = tdlib.cache().user(((TdApi.MessageSenderUser) message.senderId).userId); - avatarColorId = tdlib.cache().userAvatarColorId(user); + accentColor = tdlib.cache().userAccentColor(user); letters = tdlib.cache().userLetters(user); avatar = user != null && user.profilePhoto != null ? user.profilePhoto.small : null; break; @@ -146,7 +145,7 @@ private MarkerOptions newPoint (LocationPoint point) { default: throw new IllegalArgumentException(message.senderId.toString()); } - return newBitmap(data, avatarColorId, letters, avatar); + return newBitmap(data, accentColor, letters, avatar); } private Drawable liveBackground; @@ -165,7 +164,7 @@ private static void drawAvatar (Canvas c, Bitmap bitmap) { c.drawRoundRect(rect, Screen.dp(26), Screen.dp(26), roundPaint); } - private void drawAvatar (final MarkerData data, Canvas c, @ColorId int avatarColorId, Letters letters, TdApi.File avatar) { + private void drawAvatar (final MarkerData data, Canvas c, TdlibAccentColor accentColor, Letters letters, TdApi.File avatar) { int cx = Screen.dp(62) / 2; int cy = Screen.dp(62f) / 2; int radius = Screen.dp(26f); @@ -186,15 +185,15 @@ private void drawAvatar (final MarkerData data, Canvas c, @ColorId int avatarCol imageFile = null; } - c.drawCircle(cx, cy, radius, Paints.fillingPaint(Theme.getColor(avatarColorId))); - Paint paint = Paints.whiteMediumPaint(19f, letters.needFakeBold, false); + c.drawCircle(cx, cy, radius, Paints.fillingPaint(accentColor.getPrimaryColor())); + Paint paint = Paints.getMediumTextPaint(19f, accentColor.getPrimaryContentColor(), letters.needFakeBold); float textWidth = Paints.measureLetters(letters, 19f); c.drawText(letters.text, cx - textWidth / 2, cy + Screen.dp(6.5f), paint); data.requestFile(imageFile); } - private @Nullable Bitmap newBitmap (MarkerData data, @ColorId int avatarColorId, Letters letters, TdApi.File avatar) { + private @Nullable Bitmap newBitmap (MarkerData data, TdlibAccentColor accentColor, Letters letters, TdApi.File avatar) { Bitmap result = null; boolean success = false; try { @@ -210,7 +209,7 @@ private void drawAvatar (final MarkerData data, Canvas c, @ColorId int avatarCol data.canvas = c; data.bitmap = result; - drawAvatar(data, c, avatarColorId, letters, avatar); + drawAvatar(data, c, accentColor, letters, avatar); success = true; } catch (Throwable t) { Log.w(t); diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ea8db76ba8..d2ee541c9e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -456,6 +456,10 @@ + android:value="@bool/com_samsung_android_icon_container_has_icon_container" /> + + \ No newline at end of file diff --git a/app/src/main/java/org/thunderdog/challegram/BaseActivity.java b/app/src/main/java/org/thunderdog/challegram/BaseActivity.java index 254f5c0695..21a4ff6102 100644 --- a/app/src/main/java/org/thunderdog/challegram/BaseActivity.java +++ b/app/src/main/java/org/thunderdog/challegram/BaseActivity.java @@ -62,6 +62,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SparseArrayCompat; +import androidx.recyclerview.widget.RecyclerView; import org.drinkless.tdlib.TdApi; import org.drinkmore.Tracer; @@ -99,6 +100,7 @@ import org.thunderdog.challegram.telegram.TGLegacyManager; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibManager; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.ColorState; import org.thunderdog.challegram.theme.PropertyId; @@ -130,6 +132,7 @@ import org.thunderdog.challegram.widget.ForceTouchView; import org.thunderdog.challegram.widget.NetworkStatusBarView; import org.thunderdog.challegram.widget.PopupLayout; +import org.thunderdog.challegram.widget.StickersSuggestionsLayout; import java.lang.ref.Reference; import java.util.ArrayList; @@ -165,6 +168,9 @@ public abstract class BaseActivity extends ComponentActivity implements View.OnT private final ReferenceList activityListeners = new ReferenceList<>(); + private final TdlibMessageViewer.Listener messageViewListener = (manager, needRestrictScreenshots) -> + checkDisallowScreenshots(); + private int currentOrientation; private boolean mHasSoftwareKeys; @@ -235,6 +241,16 @@ public void addToRoot (View view, boolean ignoreStatusBar) { if (tooltipIndex != -1) { i = i == -1 ? tooltipIndex : Math.min(tooltipIndex, i); } + + if (view == inlineResultsView) { + View customEmojiSuggestonsView = rootView.findViewById(R.id.view_customEmojiSuggestions); + int customEmojiSuggestionsIndex = customEmojiSuggestonsView != null ? + rootView.indexOfChild(customEmojiSuggestonsView) : -1; + if (customEmojiSuggestionsIndex != -1) { + i = i == -1 ? customEmojiSuggestionsIndex : Math.min(customEmojiSuggestionsIndex, i); + } + } + if (i != -1) { rootView.addView(view, i); } else { @@ -269,7 +285,7 @@ public Invalidator invalidator () { } private View focusView; - private int activityState = UI.STATE_UNKNOWN; + private int activityState = UI.State.UNKNOWN; private RoundVideoController roundVideoController; private RecordAudioVideoController recordAudioVideoController; @@ -294,10 +310,12 @@ protected final void setTdlib (Tdlib tdlib) { if (this.tdlib != null) { wasOnline = this.tdlib.isOnline(); this.tdlib.setOnline(false); + this.tdlib.messageViewer().removeListener(messageViewListener); } this.tdlib = tdlib; recordAudioVideoController.setTdlib(tdlib); tdlib.setOnline(wasOnline); + tdlib.messageViewer().addListener(messageViewListener); if (drawer != null) { drawer.onCurrentTdlibChanged(tdlib); } @@ -338,7 +356,7 @@ public void onCreate (Bundle savedInstanceState) { this.isWindowLight = !Theme.isDark(); } // UI.resetSizes(); - setActivityState(UI.STATE_RESUMED); + setActivityState(UI.State.RESUMED); TdlibManager.instance().watchDog().onActivityCreate(this); Passcode.instance().checkAutoLock(); @@ -415,7 +433,7 @@ public void onCreate (Bundle savedInstanceState) { if (needTdlib()) { TdlibManager.instance().player().addTrackChangeListener(this); - TdlibManager.instance().resetBadge(); + TdlibManager.instance().resetBadge(false); } Lang.addLanguageListener(this); @@ -544,7 +562,7 @@ public final AlertDialog showAlert (AlertDialog.Builder b, ThemeDelegate theme) try { dialog = b.show(); } catch (Throwable t) { - if (UI.getUiState() == UI.STATE_RESUMED) + if (UI.getUiState() == UI.State.RESUMED) UI.showToast("Failed to display system pop-up, see application log for details", Toast.LENGTH_SHORT); Log.e("Cannot show dialog", t); return null; @@ -879,9 +897,9 @@ public final void removeSimpleStateListener (SimpleStateListener listener) { private void setActivityState (int newState) { if (this.activityState != newState) { final int prevState = this.activityState; - boolean prevResumed = prevState == UI.STATE_RESUMED; + boolean prevResumed = prevState == UI.State.RESUMED; this.activityState = newState; - if (newState != UI.STATE_RESUMED) { + if (newState != UI.State.RESUMED) { if (prevResumed) { handler.removeMessages(DISPATCH_ACTIVITY_STATE); } @@ -916,7 +934,7 @@ private void notifyActivityDestroy () { @Override public void onPause () { blockFocus(); - setActivityState(UI.STATE_PAUSED); + setActivityState(UI.State.PAUSED); if (camera != null) { camera.onActivityPause(); } @@ -966,7 +984,7 @@ public void onReceive (Context context, Intent intent) { public void onResume () { boolean lockBefore = isPasscodeShowing; UI.setContext(this); - setActivityState(UI.STATE_RESUMED); + setActivityState(UI.State.RESUMED); Passcode.instance().checkAutoLock(); checkPasscode(false); if (isPasscodeShowing && lockBefore && passcodeController != null) { @@ -1054,7 +1072,7 @@ public void onDestroy () { TGLegacyManager.instance().removeEmojiListener(this); TdlibManager.instance().watchDog().onActivityDestroy(this); Intents.revokeFileReadPermissions(); - setActivityState(UI.STATE_DESTROYED); + setActivityState(UI.State.DESTROYED); if (isPasscodeShowing && passcodeController != null) { passcodeController.onActivityDestroy(); } @@ -1263,8 +1281,8 @@ public final boolean handleMessage (Message msg) { break; } case DISPATCH_ACTIVITY_STATE: { - if (activityState == UI.STATE_RESUMED) { - if (!UI.setUiState(this, UI.STATE_RESUMED)) { + if (activityState == UI.State.RESUMED) { + if (!UI.setUiState(this, UI.State.RESUMED)) { TdlibManager.instance().watchDog().checkNetworkAvailability(); } } @@ -1791,7 +1809,7 @@ public void hideContextualPopups (boolean byNavigation) { PopupLayout window = windows.get(i); View boundView = window.getBoundView(); ViewController boundController = window.getBoundController(); - if (isContextual(boundView) || (byNavigation && boundView instanceof StickerSetWrap) || (boundView instanceof MediaLayout && !(navigation.getCurrentStackItem() instanceof MessagesController)) || (byNavigation && isContextual(boundController)) ) { + if (isContextual(boundView) || (byNavigation && boundView instanceof StickerSetWrap) || (boundView instanceof MediaLayout && !((navigation.getCurrentStackItem() instanceof MessagesController) || (((MediaLayout) boundView).getMode() == MediaLayout.MODE_AVATAR_PICKER))) || (byNavigation && isContextual(boundController)) ) { window.hideWindow(true); } } @@ -1817,7 +1835,7 @@ public int getControllerWidth (View view) { private StickerPreviewView stickerPreview; private StickerSmallView stickerPreviewControllerView; - public void openStickerPreview (Tdlib tdlib, StickerSmallView stickerView, TGStickerObj sticker, int cx, int cy, int maxWidth, int viewportHeight, boolean disableEmojis, boolean isEmojiStatus) { + public void openStickerPreview (Tdlib tdlib, StickerSmallView stickerView, TGStickerObj sticker, int cx, int cy, int maxWidth, int viewportHeight, boolean disableEmojis) { if (stickerPreview != null) { return; } @@ -1827,7 +1845,6 @@ public void openStickerPreview (Tdlib tdlib, StickerSmallView stickerView, TGSti stickerPreview = new StickerPreviewView(this); stickerPreview.setControllerView(stickerPreviewControllerView); stickerPreview.setSticker(tdlib, sticker, cx, cy, maxWidth, viewportHeight, disableEmojis); - stickerPreview.setIsEmojiStatus(isEmojiStatus); stickerPreviewWindow = new PopupLayout(this); stickerPreviewWindow.setBackListener(stickerPreview); @@ -1911,6 +1928,9 @@ public boolean openForceTouch (ForceTouchView.ForceTouchContext context) { forceTouchView.initWithContext(context); } catch (Throwable t) { Log.e("Unable to open force touch preview", t); + if (BuildConfig.DEBUG && t instanceof RuntimeException) { + throw (RuntimeException) t; + } return false; } @@ -1946,9 +1966,13 @@ public void closeForceTouch () { // Inline results + private StickersSuggestionsLayout emojiSuggestionsWrap; private InlineResultsWrap inlineResultsView; public void updateHackyOverlaysPositions () { + if (emojiSuggestionsWrap != null && emojiSuggestionsWrap.getParent() != null) { + emojiSuggestionsWrap.updatePosition(true); + } if (inlineResultsView != null && inlineResultsView.getParent() != null) { inlineResultsView.updatePosition(true); } @@ -1962,6 +1986,10 @@ public void updateHackyOverlaysPositions () { } public void showInlineResults (ViewController context, Tdlib tdlib, @Nullable ArrayList> results, boolean needBackground, @Nullable InlineResultsWrap.LoadMoreCallback callback) { + showInlineResults(context, tdlib, results, needBackground, callback, null, null); + } + + public void showInlineResults (ViewController context, Tdlib tdlib, @Nullable ArrayList> results, boolean needBackground, @Nullable InlineResultsWrap.LoadMoreCallback callback, @Nullable RecyclerView.OnScrollListener scrollCallback, @Nullable StickerSmallView.StickerMovementCallback stickerMovementCallback) { if (inlineResultsView == null) { if (results == null || results.isEmpty()) { return; @@ -1974,12 +2002,16 @@ public void showInlineResults (ViewController context, Tdlib tdlib, @Nullable addToRoot(inlineResultsView, false); } - inlineResultsView.showItems(context, results, needBackground, callback, !context.isFocused()); + inlineResultsView.showItems(context, results, needBackground, callback, scrollCallback, stickerMovementCallback, !context.isFocused()); } public void addInlineResults (ViewController context, ArrayList> results, InlineResultsWrap.LoadMoreCallback callback) { + addInlineResults(context, results, callback, null, null); + } + + public void addInlineResults (ViewController context, ArrayList> results, InlineResultsWrap.LoadMoreCallback callback, @Nullable RecyclerView.OnScrollListener scrollCallback, @Nullable StickerSmallView.StickerMovementCallback stickerMovementCallback) { if (inlineResultsView != null) { - inlineResultsView.addItems(context, results, callback); + inlineResultsView.addItems(context, results, callback, scrollCallback, stickerMovementCallback); } } @@ -1994,6 +2026,56 @@ public boolean areInlineResultsVisible () { return inlineResultsView != null && inlineResultsView.isDisplayingItems(); } + @Nullable + public InlineResultsWrap getInlineResultsView () { + return inlineResultsView; + } + + public void setEmojiSuggestions (MessagesController context, @Nullable ArrayList stickers, @Nullable RecyclerView.OnScrollListener scrollCallback, StickersSuggestionsLayout.Delegate choosingDelegate) { + if (emojiSuggestionsWrap == null) { + emojiSuggestionsWrap = new StickersSuggestionsLayout(context.context()); + emojiSuggestionsWrap.setId(R.id.view_customEmojiSuggestions); + emojiSuggestionsWrap.init(context, true); + } + emojiSuggestionsWrap.setChoosingDelegate(choosingDelegate); + emojiSuggestionsWrap.setOnScrollListener(scrollCallback); + emojiSuggestionsWrap.setStickers(context, stickers); + } + + public void updateEmojiSuggestionsPosition (boolean needTranslate) { + if (emojiSuggestionsWrap != null) { + emojiSuggestionsWrap.updatePosition(needTranslate); + } + } + + public void addEmojiSuggestions (MessagesController context, ArrayList stickers) { + if (emojiSuggestionsWrap != null && stickers != null && !stickers.isEmpty()) { + emojiSuggestionsWrap.addStickers(context, stickers); + } + } + + public void setEmojiSuggestionsVisible (boolean visible) { + if (emojiSuggestionsWrap != null) { + if (visible) { + emojiSuggestionsWrap.updatePosition(false); + } + emojiSuggestionsWrap.setStickersVisible(visible); + } + } + + public boolean hasEmojiSuggestions () { + return emojiSuggestionsWrap != null && emojiSuggestionsWrap.hasStickers(); + } + + public boolean isEmojiSuggestionsVisible () { + return emojiSuggestionsWrap != null && emojiSuggestionsWrap.isStickersVisible(); + } + + @Nullable + public StickersSuggestionsLayout getEmojiSuggestionsView () { + return emojiSuggestionsWrap; + } + // etc @Override @@ -2466,6 +2548,9 @@ public final void checkDisallowScreenshots () { } boolean disallowScreenshots = false; disallowScreenshots = (navigation.shouldDisallowScreenshots() || Passcode.instance().shouldDisallowScreenshots()); + if (tdlib != null && tdlib.messageViewer().needRestrictScreenshots()) { + disallowScreenshots = true; + } for (PopupLayout popupLayout : windows) { boolean shouldDisallowScreenshots = popupLayout.shouldDisallowScreenshots(); popupLayout.checkWindowFlags(); @@ -2548,7 +2633,7 @@ private boolean canOpenCamera () { return !( // getCurrentPopupWindow() != null || (cameraAnimator != null && cameraAnimator.isAnimating()) || - activityState != UI.STATE_RESUMED || + activityState != UI.State.RESUMED || recordAudioVideoController.isOpen() || isCameraOwnershipTaken || isNavigationBusy() @@ -2955,15 +3040,17 @@ public void checkCameraApi () { } private void initializeCamera (ViewController.CameraOpenOptions options) { - if (camera == null) { + final boolean needCreateCamera = camera == null; + if (needCreateCamera) { camera = new CameraController(this); - camera.setMode(options.mode, options.readyListener); - camera.setQrListener(options.qrCodeListener, options.qrModeSubtitle, options.qrModeDebug); + } + camera.setMode(options.mode, options.readyListener); + camera.setAvatarPickerMode(options.avatarPickerMode); + camera.setQrListener(options.qrCodeListener, options.qrModeSubtitle, options.qrModeDebug); + camera.setMediaEditorDelegates(options.delegate, options.selectDelegate, options.sendDelegate); + if (needCreateCamera) { camera.getValue(); // Ensure view creation addActivityListener(camera); - } else { - camera.setMode(options.mode, options.readyListener); - camera.setQrListener(options.qrCodeListener, options.qrModeSubtitle, options.qrModeDebug); } hideContextualPopups(false); closeAllMedia(true); @@ -3140,7 +3227,7 @@ public void onThemeAutoNightModeChanged (int autoNightMode) { } private void checkAutoNightMode () { - setRegisterLightSensor(activityState == UI.STATE_RESUMED && Settings.instance().getNightMode() == Settings.NIGHT_MODE_AUTO); + setRegisterLightSensor(activityState == UI.State.RESUMED && Settings.instance().getNightMode() == Settings.NIGHT_MODE_AUTO); setSystemNightMode(getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK); } @@ -3203,7 +3290,7 @@ private void setRegisterLightSensor (boolean register) { } private boolean needsLightSensorChanges () { - return (lightSensorRegistered && lightSensor != null && activityState == UI.STATE_RESUMED && Settings.instance().getNightMode() == Settings.NIGHT_MODE_AUTO); + return (lightSensorRegistered && lightSensor != null && activityState == UI.State.RESUMED && Settings.instance().getNightMode() == Settings.NIGHT_MODE_AUTO); } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/Log.java b/app/src/main/java/org/thunderdog/challegram/Log.java index 5152fecdaf..1f7031b0a6 100644 --- a/app/src/main/java/org/thunderdog/challegram/Log.java +++ b/app/src/main/java/org/thunderdog/challegram/Log.java @@ -18,7 +18,6 @@ import android.content.SharedPreferences; import android.os.Build; import android.os.Message; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -939,27 +938,8 @@ public static void a (@NonNull String fmt, Object... args) { log(0, LEVEL_ASSERT, fmt, args); } - public static void unexpectedTdlibResponse (TdApi.Object response, @SuppressWarnings("rawtypes") Class function, Class... objects) { - StringBuilder b = new StringBuilder("Unexpected TDLib response"); - if (function != null) { - b.append(" for "); - b.append(function.getName()); - } - b.append(". Expected: "); - boolean first = true; - for (Class object : objects) { - if (first) { - first = false; - } else { - b.append(", "); - } - b.append(object.getName()); - } - b.append(" but received: "); - b.append(response != null ? response : "null"); - String message = b.toString(); - UI.showToast(message, Toast.LENGTH_LONG); - Log.a("%s", message); + public static void ensureReturnType (Class> function, Class expectedReturnType) { + // Do nothing. } public static void fixme () { diff --git a/app/src/main/java/org/thunderdog/challegram/MainActivity.java b/app/src/main/java/org/thunderdog/challegram/MainActivity.java index f0afa95df1..fa7e1a8143 100644 --- a/app/src/main/java/org/thunderdog/challegram/MainActivity.java +++ b/app/src/main/java/org/thunderdog/challegram/MainActivity.java @@ -35,6 +35,7 @@ import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Background; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.helper.LiveLocationHelper; import org.thunderdog.challegram.loader.gif.LottieCache; import org.thunderdog.challegram.navigation.BackHeaderButton; @@ -65,6 +66,7 @@ import org.thunderdog.challegram.ui.CallController; import org.thunderdog.challegram.ui.CreateChannelController; import org.thunderdog.challegram.ui.CreateGroupController; +import org.thunderdog.challegram.ui.EditChatFolderController; import org.thunderdog.challegram.ui.EditNameController; import org.thunderdog.challegram.ui.IntroController; import org.thunderdog.challegram.ui.ListItem; @@ -81,6 +83,7 @@ import org.thunderdog.challegram.ui.SettingsCacheController; import org.thunderdog.challegram.ui.SettingsController; import org.thunderdog.challegram.ui.SettingsDataController; +import org.thunderdog.challegram.ui.SettingsFoldersController; import org.thunderdog.challegram.ui.SettingsNetworkStatsController; import org.thunderdog.challegram.ui.SettingsNotificationController; import org.thunderdog.challegram.ui.SettingsPrivacyController; @@ -245,7 +248,7 @@ private void cleanupStack (ViewController c, int accountId) { private void updateCounter () { Tdlib tdlib = currentTdlib(); - boolean animated = getActivityState() == UI.STATE_RESUMED; + boolean animated = getActivityState() == UI.State.RESUMED; @Tdlib.ResolvableProblem int problemType = tdlib.findResolvableProblem(); BackHeaderButton backButton = navigation.getHeaderView().getBackButton(); if (problemType != Tdlib.ResolvableProblem.NONE) { @@ -343,8 +346,10 @@ public void act () { } private void processAuthorizationStateChange (TdlibAccount account, TdApi.AuthorizationState authorizationState, @Tdlib.Status int status) { + ViewController current = navigation.isEmpty() ? null : navigation.getStack().getCurrent(); + boolean currentScreenBelongsToTargetAccount = current != null && current.isSameAccount(account); if (this.account.id != account.id) { - if (navigation.isEmpty() || !navigation.getCurrentStackItem().isSameAccount(account)) { + if (navigation.isEmpty() || !currentScreenBelongsToTargetAccount) { return; } } @@ -353,8 +358,6 @@ private void processAuthorizationStateChange (TdlibAccount account, TdApi.Author return; } - ViewController current = navigation.getStack().getCurrent(); - if (status == Tdlib.Status.READY) { ViewController first = navigation.getStack().get(0); boolean needThemeSwitch = this.account.id != account.id && isUnauthorizedController(current) && !isUnauthorizedController(first) && first.tdlibId() != account.id && current.tdlibId() == account.id; @@ -369,10 +372,15 @@ private void processAuthorizationStateChange (TdlibAccount account, TdApi.Author return; } - if (status == Tdlib.Status.UNAUTHORIZED && this.account.id == account.id) { - int nextAccountId = tdlib.context().findNextAccountId(this.account.id); - if (nextAccountId != TdlibAccount.NO_ID) { - tdlib.context().changePreferredAccountId(nextAccountId, TdlibManager.SWITCH_REASON_UNAUTHORIZED); + if (status == Tdlib.Status.UNAUTHORIZED) { + if (this.account.id == account.id) { + int nextAccountId = tdlib.context().findNextAccountId(account.id); + if (nextAccountId != TdlibAccount.NO_ID) { + tdlib.context().changePreferredAccountId(nextAccountId, TdlibManager.SWITCH_REASON_UNAUTHORIZED); + return; + } + } else if (currentScreenBelongsToTargetAccount && !current.isUnauthorized() && tdlib.context().hasActiveAccounts()) { + // MainController shall be shown in onAccountSwitched return; } } @@ -526,19 +534,12 @@ private void initController (Tdlib tdlib, @Tdlib.Status int status) { } private void showExperimentalAlert () { - if (BuildConfig.EXPERIMENTAL) { + if (BuildConfig.EXPERIMENTAL && !BuildConfig.DEBUG) { ViewController c = navigation.getCurrentStackItem(); if (c != null) { c.openAlert(R.string.ExperimentalBuildTitle, Strings.buildMarkdown(c, Lang.getStringSecure(R.string.ExperimentalBuildInfo), (view, span, clickedText) -> { - switch (span.getEntityType().getConstructor()) { - case TdApi.TextEntityTypeUrl.CONSTRUCTOR: - UI.openUrl(clickedText); - break; - case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: - UI.openUrl(((TdApi.TextEntityTypeTextUrl) span.getEntityType()).url); - break; - } + TD.handleLegacyClick(c, clickedText, span); return true; }), Lang.getOK(), @@ -1242,6 +1243,12 @@ private static ViewController restoreController (BaseActivity context, Tdlib restore = new PrivacyExceptionController(context, tdlib); } else if (id == R.id.controller_networkStats) { restore = new SettingsNetworkStatsController(context, tdlib); + } else if (id == R.id.controller_chatFolders) { + restore = new SettingsFoldersController(context, tdlib); + } else if (id == R.id.controller_editChatFolders) { + restore = new EditChatFolderController(context, tdlib); + } else if (id == R.id.controller_bug_killer) { + restore = new SettingsBugController(context, tdlib); } else { return null; } @@ -1439,6 +1446,7 @@ public void onResume () { // Log.e("%s", Strings.getHexColor(U.compositeColor(Theme.headerColor(), Theme.getColor(ColorId.statusBar)), false)); tdlib.contacts().makeSilentPermissionCheck(this); tdlib.context().global().notifyResolvableProblemAvailabilityMightHaveChanged(); + tdlib.context().dateManager().checkCurrentDate(); UI.startNotificationService(); if (!madeEmulatorChecks && !Settings.instance().isEmulator()) { madeEmulatorChecks = true; diff --git a/app/src/main/java/org/thunderdog/challegram/TDLib.java b/app/src/main/java/org/thunderdog/challegram/TDLib.java index a5735a5e85..6fa2bce034 100644 --- a/app/src/main/java/org/thunderdog/challegram/TDLib.java +++ b/app/src/main/java/org/thunderdog/challegram/TDLib.java @@ -41,7 +41,9 @@ private static String format (String format, Object... formatArgs) { } private static void log (int verbosityLevel, String format, Object... formatArgs) { - Client.execute(new TdApi.AddLogMessage(verbosityLevel, format(format, formatArgs))); + try { + Client.execute(new TdApi.AddLogMessage(verbosityLevel, format(format, formatArgs))); + } catch (Client.ExecutionException ignored) { } } public static void e (String format, Object... formatArgs) { diff --git a/app/src/main/java/org/thunderdog/challegram/U.java b/app/src/main/java/org/thunderdog/challegram/U.java index 6d5adf6ae9..f7ec2319ee 100644 --- a/app/src/main/java/org/thunderdog/challegram/U.java +++ b/app/src/main/java/org/thunderdog/challegram/U.java @@ -24,6 +24,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; +import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -51,6 +52,7 @@ import android.opengl.GLUtils; import android.os.Build; import android.os.Environment; +import android.os.LocaleList; import android.os.Parcelable; import android.os.PowerManager; import android.os.StatFs; @@ -69,10 +71,13 @@ import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; import android.webkit.MimeTypeMap; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.CheckResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.EnvironmentCompat; @@ -121,6 +126,7 @@ import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.ui.TextController; import org.thunderdog.challegram.util.AppBuildInfo; +import org.thunderdog.challegram.util.AppInstallationUtil; import org.thunderdog.challegram.util.Permissions; import org.thunderdog.challegram.widget.NoScrollTextView; @@ -141,6 +147,8 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; @@ -149,6 +157,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.zip.GZIPInputStream; @@ -156,6 +166,7 @@ import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGL11; +import me.vkryl.android.LocaleUtils; import me.vkryl.android.SdkVersion; import me.vkryl.core.ArrayUtils; import me.vkryl.core.BitwiseUtils; @@ -163,6 +174,7 @@ import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.collection.IntList; +import me.vkryl.core.collection.LongList; import me.vkryl.core.lambda.RunnableBool; import me.vkryl.core.lambda.RunnableData; import me.vkryl.core.util.LocalVar; @@ -180,12 +192,6 @@ public static boolean blurBitmap (Bitmap bitmap, int radius, int unpin) { return U.isValidBitmap(bitmap) && N.blurBitmap(bitmap, radius, unpin, 0) == 0; } - private static Boolean isAppSideLoaded; - - public static boolean isAppSideLoaded () { - return (isAppSideLoaded != null ? isAppSideLoaded : (isAppSideLoaded = isAppSideLoadedImpl())); - } - public static int getHeading (Location location) { if (location.hasBearing()) { int heading = MathUtils.modulo(Math.round(location.getBearing()), 360); @@ -194,28 +200,6 @@ public static int getHeading (Location location) { return 0; } - public static final String VENDOR_GOOGLE_PLAY = "com.android.vending"; - - @Nullable - public static String getInstallerPackageName () { - try { - String packageName = UI.getAppContext().getPackageName(); - String installerPackageName = UI.getAppContext().getPackageManager().getInstallerPackageName(packageName); - if (StringUtils.isEmpty(installerPackageName)) { - return null; - } - return installerPackageName; - } catch (Throwable t) { - Log.v("Unable to determine installer package", t); - return null; - } - } - - private static boolean isAppSideLoadedImpl () { - String installerId = getInstallerPackageName(); - return StringUtils.isEmpty(installerId) || !VENDOR_GOOGLE_PLAY.equals(installerId); - } - public static String gzipFileToString (String path) { try (BufferedSource buffer = Okio.buffer(Okio.source(new GZIPInputStream(new FileInputStream(new File(path)))))) { return buffer.readString(StringUtils.UTF_8); @@ -412,7 +396,7 @@ private static File getRootOfInnerSdCardFolder (File file) { String output = out.toString(); if (!output.trim().isEmpty()) { String[] devicePoints = output.split("\n"); - for (String voldPoint: devicePoints) { + for (String voldPoint : devicePoints) { String path = voldPoint.split(" ")[2]; if (!StringUtils.equalsOrBothEmpty(ignorePath, path)) { if (results == null) { @@ -703,8 +687,10 @@ public static void recycle (@Nullable Canvas c) { public static void recycle (@Nullable Bitmap bitmap) { if (bitmap != null) { try { - if (!bitmap.isRecycled()) - bitmap.recycle(); + synchronized (bitmap) { + if (!bitmap.isRecycled()) + bitmap.recycle(); + } } catch (Throwable ignored) { } } } @@ -832,15 +818,6 @@ public static long getFreeMemorySize (StatFs statFs) { } } - public static String getMarketUrl () { - String url = Lang.getStringSecure(R.string.MarketUrl); - return Strings.isValidLink(url) ? url : BuildConfig.MARKET_URL; - } - - /*public static boolean isAfter (int hour, int minute, int second, int afterHour, int afterMinute, int afterSecond) { - return hour > afterHour || (hour == afterHour && (minute > afterMinute || (minute == afterMinute && second > afterSecond))); - }*/ - public static String getOtherNotificationChannel () { return getNotificationChannel("other", R.string.NotificationChannelOther); } @@ -2420,9 +2397,9 @@ public static String getUsefulMetadata (@Nullable Tdlib tdlib) { "Build: `" + Build.FINGERPRINT + "`\n" + "Package: " + UI.getAppContext().getPackageName() + "\n" + "Locale: " + locale + (!locale.equals(appLocale) ? " (app: " + appLocale + ")" : ""); - String installerName = U.getInstallerPackageName(); + String installerName = AppInstallationUtil.getInstallerPrettyName(); if (!StringUtils.isEmpty(installerName)) { - metadata += "\nInstaller: " + (U.VENDOR_GOOGLE_PLAY.equals(installerName) ? "Google Play" : installerName); + metadata += "\nInstaller: " + installerName; } String fingerprint = U.getApkFingerprint("SHA1"); if (!StringUtils.isEmpty(fingerprint)) { @@ -3557,4 +3534,152 @@ public static boolean setRect (RectF rectF, float left, float top, float right, } return false; } + + public static String[] getInputLanguages () { + final List inputLanguages = new ArrayList<>(); + InputMethodManager imm = (InputMethodManager) UI.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + String inputLanguageCode = null; + try { + inputLanguageCode = toLanguageCode(imm.getCurrentInputMethodSubtype()); + } catch (Throwable ignored) { } + if (StringUtils.isEmpty(inputLanguageCode)) { + try { + inputLanguageCode = toLanguageCode(imm.getLastInputMethodSubtype()); + } catch (Throwable ignored) { } + } + if (!StringUtils.isEmpty(inputLanguageCode)) { + inputLanguages.add(inputLanguageCode); + } + + /*if (Strings.isEmpty(inputLanguageCode)) { + try { + String id = android.provider.Settings.Secure.getString( + UI.getAppContext().getContentResolver(), + android.provider.Settings.Secure.DEFAULT_INPUT_METHOD + ); + if (!Strings.isEmpty(id)) { + List list = imm.getInputMethodList(); + lookup: + for (InputMethodInfo info : list) { + if (id.equals(info.getId())) { + List subtypes = imm.getEnabledInputMethodSubtypeList(info, true); + for (InputMethodSubtype subtype : subtypes) { + String languageCode = toLanguageCode(subtype); + if (!Strings.isEmpty(languageCode)) { + inputLanguageCode = languageCode; + break lookup; + } + } + } + } + } + } catch (Throwable ignored) { } + } + if (Strings.isEmpty(inputLanguageCode) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + try { + LocaleList localeList = ((InputView) callback).getImeHintLocales(); + if (localeList != null) { + for (int i = 0; i < localeList.size(); i++) { + inputLanguageCode = U.toBcp47Language(localeList.get(i)); + if (!Strings.isEmpty(inputLanguageCode)) + break; + } + } + } catch (Throwable ignored) { } + }*/ + } + if (inputLanguages.isEmpty()) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + LocaleList locales = Resources.getSystem().getConfiguration().getLocales(); + for (int i = 0; i < locales.size(); i++) { + String code = LocaleUtils.toBcp47Language(locales.get(i)); + if (!StringUtils.isEmpty(code) && !inputLanguages.contains(code)) + inputLanguages.add(code); + } + } else { + String code = LocaleUtils.toBcp47Language(Resources.getSystem().getConfiguration().locale); + if (!StringUtils.isEmpty(code)) { + inputLanguages.add(code); + } + } + } catch (Throwable ignored) { } + } + if (!inputLanguages.isEmpty()) { + return inputLanguages.toArray(new String[0]); + } else { + return null; + } + } + + private static String toLanguageCode (InputMethodSubtype ims) { + if (ims != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + String languageTag = ims.getLanguageTag(); + if (!StringUtils.isEmpty(languageTag)) { + return languageTag; + } + } + String locale = ims.getLocale(); + if (!StringUtils.isEmpty(locale)) { + Locale l = U.getDisplayLocaleOfSubtypeLocale(locale); + if (l != null) { + return LocaleUtils.toBcp47Language(l); + } + } + } + return null; + } + + @CheckResult + public static long[] removeAll (long[] items, Set itemsToRemove) { + if (itemsToRemove.isEmpty() || items.length == 0) { + return items; + } + LongList itemList = new LongList(items.length); + for (long item : items) { + if (!itemsToRemove.contains(item)) { + itemList.append(item); + } + } + return itemList.get(); + } + + @CheckResult + public static long[] toArray(Collection collection) { + if (collection.isEmpty()) { + return ArrayUtils.EMPTY_LONGS; + } + int index = 0; + long[] array = new long[collection.size()]; + for (long element : collection) { + array[index++] = element; + } + return array; + } + + public static Set unmodifiableTreeSetOf (int[] array) { + if (array.length == 0) + return Collections.emptySet(); + if (array.length == 1) + return Collections.singleton(array[0]); + Set set = new TreeSet<>(); + for (int value : array) { + set.add(value); + } + return Collections.unmodifiableSet(set); + } + + public static Set unmodifiableTreeSetOf (long[] array) { + if (array.length == 0) + return Collections.emptySet(); + if (array.length == 1) + return Collections.singleton(array[0]); + Set set = new TreeSet<>(); + for (long value : array) { + set.add(value); + } + return Collections.unmodifiableSet(set); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaBottomGalleryController.java b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaBottomGalleryController.java index 05dc16e692..6ceb8083a5 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaBottomGalleryController.java +++ b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaBottomGalleryController.java @@ -45,6 +45,7 @@ import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.ImageGalleryFile; +import org.thunderdog.challegram.mediaview.AvatarPickerMode; import org.thunderdog.challegram.mediaview.MediaSelectDelegate; import org.thunderdog.challegram.mediaview.MediaSendDelegate; import org.thunderdog.challegram.mediaview.MediaViewController; @@ -56,6 +57,7 @@ import org.thunderdog.challegram.navigation.Menu; import org.thunderdog.challegram.navigation.MenuMoreWrap; import org.thunderdog.challegram.navigation.ToggleHeaderView; +import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Intents; @@ -72,6 +74,7 @@ import me.vkryl.android.AnimatorUtils; import me.vkryl.android.widget.FrameLayoutFix; import me.vkryl.core.lambda.CancellableRunnable; +import me.vkryl.td.ChatId; import me.vkryl.td.Td; public class MediaBottomGalleryController extends MediaBottomBaseController implements Media.GalleryCallback, MediaGalleryAdapter.Callback, Menu, View.OnClickListener, MediaBottomGalleryBucketAdapter.Callback, MediaViewDelegate, MediaSelectDelegate, MediaSendDelegate { @@ -100,7 +103,9 @@ protected int getMenuId () { @Override public void fillMenuItems (int id, HeaderView header, LinearLayout menu) { if (id == R.id.menu_more) { - header.addSearchButton(menu, this); + if (mediaLayout.getMode() != MediaLayout.MODE_AVATAR_PICKER) { + header.addSearchButton(menu, this); + } header.addMoreButton(menu, this); } else if (id == R.id.menu_clear) { header.addClearButton(menu, this); @@ -206,7 +211,8 @@ protected View onCreateView (Context context) { decoration = new GridSpacingItemDecoration(spanCount, Screen.dp(4f), true, true, true); GridLayoutManager manager = new RtlGridLayoutManager(context(), spanCount); - int options = MediaGalleryAdapter.OPTION_SELECTABLE | MediaGalleryAdapter.OPTION_ALWAYS_SELECTABLE; + int options = inAvatarPickerMode() ? MediaGalleryAdapter.OPTION_NEVER_SELECTABLE : + MediaGalleryAdapter.OPTION_SELECTABLE | MediaGalleryAdapter.OPTION_ALWAYS_SELECTABLE; /*if (U.deviceHasAnyCamera(context)) { options |= MediaGalleryAdapter.OPTION_CAMERA_AVAILABLE; }*/ @@ -228,7 +234,7 @@ protected View onCreateView (Context context) { if (mediaLayout.needCameraButton()) { cameraBadgeView = new CircleCounterBadgeView(this, R.id.btn_camera, this::onCameraButtonClick, null); cameraBadgeView.init(R.drawable.deproko_baseline_camera_26, 48f, 4f, ColorId.circleButtonChat, ColorId.circleButtonChatIcon); - cameraBadgeView.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(CircleCounterBadgeView.BUTTON_WRAPPER_WIDTH), Screen.dp(74f), Gravity.BOTTOM | Gravity.RIGHT, 0, 0, Screen.dp(12), Screen.dp(12 + 60))); + cameraBadgeView.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(CircleCounterBadgeView.BUTTON_WRAPPER_WIDTH), Screen.dp(74f), Gravity.BOTTOM | Gravity.RIGHT, 0, 0, Screen.dp(12), Screen.dp(12) + mediaLayout.getCameraButtonOffset())); contentView.addView(cameraBadgeView); } @@ -237,7 +243,7 @@ protected View onCreateView (Context context) { @Override public boolean allowSpoiler () { - return true; + return !ChatId.isSecret(getOutputChatId()); } @Override @@ -250,6 +256,12 @@ protected void onUpdateBottomBarFactor (float bottomBarFactor, float counterFact } private void onCameraButtonClick (View v) { + if (mediaLayout.getMode() == MediaLayout.MODE_AVATAR_PICKER) { + mediaLayout.hidePopupAndOpenCamera(new ViewController.CameraOpenOptions().anchor(v) + .setAvatarPickerMode(mediaLayout.getAvatarPickerMode()).setMediaEditorDelegates(this, this, this)); + return; + } + MessagesController c = mediaLayout.parentMessageController(); if (c == null) return; @@ -564,7 +576,7 @@ public boolean onPhotoOrVideoOpenRequested (ImageFile fromFile) { MediaViewController controller = new MediaViewController(context, tdlib); controller.setArguments( MediaViewController.Args.fromGallery(this, this, this, this, stack, mediaLayout.areScheduledOnly()) - .setReceiverChatId(mediaLayout.getTargetChatId()) + .setReceiverChatId(mediaLayout.getTargetChatId()).setAvatarPickerMode(mediaLayout.getAvatarPickerMode()) ); controller.open(); @@ -574,6 +586,10 @@ public boolean onPhotoOrVideoOpenRequested (ImageFile fromFile) { return false; } + private boolean inAvatarPickerMode () { + return mediaLayout.getAvatarPickerMode() != AvatarPickerMode.NONE; + } + @Override public boolean isMediaItemSelected (int index, MediaItem item) { return adapter.getSelectionIndex(item.getSourceGalleryFile()) >= 0; @@ -581,6 +597,9 @@ public boolean isMediaItemSelected (int index, MediaItem item) { @Override public void setMediaItemSelected (int index, MediaItem item, boolean isSelected) { + if (mediaLayout.getMode() == MediaLayout.MODE_AVATAR_PICKER) { + return; + } adapter.setSelected(item.getSourceGalleryFile(), isSelected); } @@ -870,27 +889,18 @@ private void searchInternal (final String q) { awaitingQuery = q; if (!bingUserLoading) { bingUserLoading = true; - tdlib.client().send(new TdApi.SearchPublicChat(tdlib.getPhotoSearchBotUsername()), object -> { - switch (object.getConstructor()) { - case TdApi.Chat.CONSTRUCTOR: { - TdApi.User user = tdlib.chatUser((TdApi.Chat) object); - if (user != null) { - bingUserId = user.id; - UI.post(() -> { - if (lastQuery.equals(awaitingQuery)) { - searchInternal(awaitingQuery); - } - }); - } - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.SearchPublicChat.class, TdApi.Chat.class); - break; + tdlib.send(new TdApi.SearchPublicChat(tdlib.getPhotoSearchBotUsername()), (publicChat, error) -> { + if (error != null) { + UI.showError(error); + } else { + TdApi.User user = tdlib.chatUser(publicChat); + if (user != null) { + bingUserId = user.id; + UI.post(() -> { + if (lastQuery.equals(awaitingQuery)) { + searchInternal(awaitingQuery); + } + }); } } }); diff --git a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaGalleryAdapter.java b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaGalleryAdapter.java index 9fdd05d898..005e51911a 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaGalleryAdapter.java +++ b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaGalleryAdapter.java @@ -37,11 +37,12 @@ public interface Callback { boolean onPhotoOrVideoOpenRequested (ImageFile fromFile); } - public static final int OPTION_SELECTABLE = 0x01; - public static final int OPTION_ALWAYS_SELECTABLE = 0x02; - public static final int OPTION_NEED_COUNTER = 0x04; - public static final int OPTION_CAMERA_AVAILABLE = 0x08; - public static final int OPTION_NEED_CAMERA = 0x10; + public static final int OPTION_SELECTABLE = 1; + public static final int OPTION_ALWAYS_SELECTABLE = 1 << 1; + public static final int OPTION_NEED_COUNTER = 1 << 2; + public static final int OPTION_CAMERA_AVAILABLE = 1 << 3; + public static final int OPTION_NEED_CAMERA = 1 << 4; + public static final int OPTION_NEVER_SELECTABLE = 1 << 5; private final Context context; private final RecyclerView parent; @@ -49,6 +50,7 @@ public interface Callback { private final Callback callback; private final boolean isSelectable; private final boolean isAlwaysSelectable; // when we click on a first photo, it will be selected + private final boolean isNeverSelectable; // hide all checkboxes private final boolean needCounter; private final boolean cameraAvailable; private boolean animationsEnabled; @@ -64,6 +66,7 @@ public MediaGalleryAdapter (Context context, RecyclerView parent, GridLayoutMana this.needCounter = (options & OPTION_NEED_COUNTER) != 0; this.cameraAvailable = false; // (options & OPTION_CAMERA_AVAILABLE) != 0 && (!Config.CUSTOM_CAMERA_ENABLED || (options & OPTION_NEED_CAMERA) != 0); this.showCamera = cameraAvailable && (options & OPTION_NEED_CAMERA) != 0; + this.isNeverSelectable = (options & OPTION_NEVER_SELECTABLE) != 0; this.selected = new ArrayList<>(); } @@ -244,6 +247,7 @@ public void onBindViewHolder (MediaHolder holder, int position) { ImageFile imageFile = images.get(showCamera ? position - 1 : position); holder.setImage(imageFile, getSelectionIndex(imageFile), isSelectable, isVisible(imageFile)); holder.setAnimationsDisabled(!animationsEnabled); + ((MediaGalleryImageView) holder.itemView).setAlwaysInvisible(isNeverSelectable); break; } case MediaHolder.VIEW_TYPE_COUNTER: { @@ -293,7 +297,7 @@ public int measureScrollTop (int position) { public void onClick (View view, boolean isSelect) { ImageFile image = ((MediaGalleryImageView) view).getImage(); - if (!isSelect && callback.onPhotoOrVideoOpenRequested(image)) { + if ((!isSelect || isNeverSelectable) && callback.onPhotoOrVideoOpenRequested(image)) { return; } diff --git a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaGalleryImageView.java b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaGalleryImageView.java index 981e997c7d..1912f929c7 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaGalleryImageView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaGalleryImageView.java @@ -81,6 +81,12 @@ public void setClickListener (ClickListener listener) { } private boolean isInvisible; + private boolean isAlwaysInvisible; + + public void setAlwaysInvisible (boolean alwaysInvisible) { + isAlwaysInvisible = alwaysInvisible; + invalidate(); + } public void setInvisible (boolean isInvisible, boolean needInvalidate) { if (this.isInvisible != isInvisible) { @@ -322,7 +328,7 @@ protected void onDraw (Canvas c) { c.restore(); } - if (!isInvisible) { + if (!isInvisible && !isAlwaysInvisible) { final int centerX = receiver.centerX() + (int) ((float) receiver.getWidth() * (1f - SCALE)) / 2; final int centerY = receiver.centerY() - (int) ((float) receiver.getHeight() * (1f - SCALE)) / 2; final int radius = Screen.dp(9f + 2f * factor); diff --git a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLayout.java b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLayout.java index 0ac80d23c9..c8b9d30dd6 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLayout.java +++ b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLayout.java @@ -18,10 +18,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.TargetApi; -import android.content.Context; import android.content.Intent; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; import android.os.Build; import android.text.TextUtils; import android.util.TypedValue; @@ -29,7 +26,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; @@ -49,6 +45,7 @@ import org.thunderdog.challegram.data.TGUser; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.ImageGalleryFile; +import org.thunderdog.challegram.mediaview.AvatarPickerMode; import org.thunderdog.challegram.navigation.ActivityResultHandler; import org.thunderdog.challegram.navigation.BackHeaderButton; import org.thunderdog.challegram.navigation.BackListener; @@ -67,10 +64,8 @@ import org.thunderdog.challegram.theme.ThemeChangeListener; import org.thunderdog.challegram.theme.ThemeListenerList; import org.thunderdog.challegram.theme.ThemeManager; -import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Fonts; import org.thunderdog.challegram.tool.Intents; -import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; @@ -80,9 +75,9 @@ import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.util.HapticMenuHelper; import org.thunderdog.challegram.util.Permissions; -import org.thunderdog.challegram.widget.AvatarView; import org.thunderdog.challegram.widget.NoScrollTextView; import org.thunderdog.challegram.widget.PopupLayout; +import org.thunderdog.challegram.widget.SendButton; import org.thunderdog.challegram.widget.ShadowView; import java.util.ArrayList; @@ -117,8 +112,10 @@ public interface MediaGalleryCallback extends MediaCallback { public static final int MODE_LOCATION = 1; public static final int MODE_GALLERY = 2; public static final int MODE_CUSTOM_POPUP = 3; + public static final int MODE_AVATAR_PICKER = 4; private int mode; + private @AvatarPickerMode int avatarPickerMode = AvatarPickerMode.NONE; private @Nullable MediaCallback callback; // Data @@ -131,7 +128,7 @@ public interface MediaGalleryCallback extends MediaCallback { private @Nullable ShadowView shadowView; private MediaBottomBaseController currentController; - private ViewGroup customBottomBar; + private View customBottomBar; private final ThemeListenerList themeListeners = new ThemeListenerList(); @@ -150,6 +147,18 @@ public void initDefault (MessagesController target) { init(MODE_DEFAULT, target); } + public int getMode () { + return mode; + } + + public int getAvatarPickerMode () { + return avatarPickerMode; + } + + public void setAvatarPickerMode (@AvatarPickerMode int avatarPickerMode) { + this.avatarPickerMode = avatarPickerMode; + } + private boolean rtl, needVote; public void init (int mode, MessagesController target) { @@ -168,6 +177,7 @@ public void init (int mode, MessagesController target) { index = 0; break; } + case MODE_AVATAR_PICKER: case MODE_GALLERY: { items = new MediaBottomBar.BarItem[] { new MediaBottomBar.BarItem(R.drawable.baseline_location_on_24, R.string.Gallery, ColorId.attachPhoto, Screen.dp(1f)) @@ -265,10 +275,7 @@ public void initCustom () { addView(controllerView); if (mode == MODE_DEFAULT) { - addView(customBottomBar = currentController.createCustomBottomBar()); - themeListeners.addThemeInvalidateListener(customBottomBar); - customBottomBar.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM)); - customBottomBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY); + setCustomBottomBar(currentController.createCustomBottomBar()); } setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); @@ -276,6 +283,17 @@ public void initCustom () { Lang.addLanguageListener(this); } + public void setCustomBottomBar (View bottomBar) { + addView(customBottomBar = bottomBar); + themeListeners.addThemeInvalidateListener(customBottomBar); + customBottomBar.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM)); + customBottomBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY); + } + + public View getCustomBottomBar () { + return customBottomBar; + } + @Override public void onLanguagePackEvent (int event, int arg1) { if (Lang.hasDirectionChanged(event, arg1)) { @@ -379,8 +397,11 @@ public MediaBottomBaseController createControllerForIndex (int index) { case MODE_LOCATION: { return new MediaBottomLocationController(this); } + case MODE_AVATAR_PICKER: case MODE_GALLERY: { - return new MediaBottomGalleryController(this); + MediaBottomGalleryController c = new MediaBottomGalleryController(this); + c.setArguments(new MediaBottomGalleryController.Arguments(mode == MODE_GALLERY)); + return c; } } if (rtl) { @@ -490,6 +511,10 @@ public void hide (boolean multi) { popupLayout.hideWindow(true); } + public boolean isVisible () { + return popupLayout != null && !popupLayout.isWindowHidden(); + } + // Callbacks @Override @@ -715,7 +740,7 @@ private void updateBarPosition () { int height = getBottomBarHeight(); float factor = Math.max(bottomBarFactor, counterFactor); float y = height - (int) ((float) height * factor); - if (!inSpecificMode()) { + if (!inSpecificMode() || mode == MODE_AVATAR_PICKER) { if (bottomBar != null) { if (currentController != null) { currentController.onUpdateBottomBarFactor(bottomBarFactor, counterFactor, y); @@ -723,6 +748,9 @@ private void updateBarPosition () { bottomBar.setTranslationY(y); onCurrentColorChanged(); } + if (currentController != null && mode == MODE_AVATAR_PICKER) { + currentController.onUpdateBottomBarFactor(bottomBarFactor, counterFactor, y); + } if (customBottomBar != null) { customBottomBar.setTranslationY(y); onCurrentColorChanged(); @@ -949,9 +977,8 @@ public void hidePopupAndOpenCamera (ViewController.CameraOpenOptions params) { @Override public void onPopupDismiss (PopupLayout popup) { if (cameraOpenOptions != null) { - MessagesController c = parentMessageController(); - if (c != null && !c.isDestroyed()) { - c.openInAppCamera(cameraOpenOptions); + if (parent != null && (parent instanceof MessagesController || mode == MODE_AVATAR_PICKER) && !parent.isDestroyed()) { + parent.openInAppCamera(cameraOpenOptions); } } performDestroy(); @@ -974,6 +1001,9 @@ public void performDestroy () { if (showKeyboardOnHide && target != null) { target.showKeyboard(); } + if (sendButton != null) { + sendButton.destroySlowModeCounterController(); + } ThemeManager.instance().removeThemeListener(this); Lang.removeLanguageListener(this); if (target != null) { @@ -1005,6 +1035,9 @@ public void pickDateOrProceed (TdlibUi.SimpleSendCallback sendCallback) { if (target != null && target.areScheduledOnly()) { tdlib().ui().showScheduleOptions(target, getTargetChatId(), false, sendCallback, null, null); } else { + if (showSlowModeRestriction(sendButton)) { + return; + } sendCallback.onSendRequested(Td.newSendOptions(), false); } } @@ -1115,6 +1148,10 @@ public void sendImage (ImageFile image, boolean isRemote) { } public boolean sendPhotosOrVideos (View view, ArrayList images, boolean areRemote, TdApi.MessageSendOptions options, boolean disableMarkdown, boolean asFiles, boolean disableAnimation) { + if (mode == MODE_AVATAR_PICKER) { + parent.context().forceCloseCamera(); + } + if (images == null || images.isEmpty()) { return false; } @@ -1274,12 +1311,11 @@ public void cancelMultiSelection () { // Counter private CounterHeaderView counterView; - private ImageView sendButton; + private SendButton sendButton; private HapticMenuHelper sendMenu; private BackHeaderButton closeButton; private TextView counterHintView; private ImageView groupMediaView, hotMediaView; - private @Nullable SenderSendIcon senderSendIcon; private float groupMediaFactor; private boolean needGroupMedia, needSpoiler; @@ -1346,37 +1382,28 @@ private void prepareCounter () { groupMediaFactor = needGroupMedia ? 1f : 0f; bottomBar.addView(counterHintView); - sendButton = new ImageView(getContext()) { + sendButton = new SendButton(getContext(), R.drawable.deproko_baseline_send_24) { @Override public boolean onTouchEvent (MotionEvent e) { return isEnabled() && Views.isValid(this) && super.onTouchEvent(e); } }; sendButton.setId(R.id.btn_send); - sendButton.setScaleType(ImageView.ScaleType.CENTER); - sendButton.setImageResource(R.drawable.deproko_baseline_send_24); - sendButton.setColorFilter(Theme.chatSendButtonColor()); + sendButton.getSlowModeCounterController(tdlib()).setCurrentChat(getTargetChatId()); themeListeners.addThemeFilterListener(sendButton, ColorId.chatSendButton); sendButton.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(55f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.RIGHT)); Views.setClickable(sendButton); sendButton.setOnClickListener(this); bottomBar.addView(sendButton); - TdApi.Chat chat = getTargetChat(); - if (chat != null && chat.messageSenderId != null) { - senderSendIcon = new SenderSendIcon(getContext(), tdlib(), chat.id); - senderSendIcon.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(19), Screen.dp(19), Gravity.RIGHT | Gravity.BOTTOM, 0, 0, Screen.dp(11), Screen.dp(8))); - senderSendIcon.update(chat.messageSenderId); - bottomBar.addView(senderSendIcon); - } - sendMenu = new HapticMenuHelper(list -> { + final TdApi.Chat chat = getTargetChat(); List items = tdlib().ui().fillDefaultHapticMenu(getTargetChatId(), false, getCurrentController().canRemoveMarkdown(), true); if (items == null) items = new ArrayList<>(); getCurrentController().addCustomItems(sendButton, items); - if (senderSendIcon != null) { - items.add(0, senderSendIcon.createHapticSenderItem(getTargetChat())); + if (chat != null && chat.messageSenderId != null) { + items.add(0, MediaLayout.createHapticSenderItem(tdlib(), chat)); } return !items.isEmpty() ? items : null; }, (menuItem, parentView, item) -> { @@ -1458,9 +1485,6 @@ public boolean onTouchEvent (MotionEvent e) { counterView.setAlpha(0f); sendButton.setAlpha(0f); - if (senderSendIcon != null) { - senderSendIcon.setAlpha(0f); - } closeButton.setAlpha(0f); counterHintView.setAlpha(0f); groupMediaView.setAlpha(0f); @@ -1641,9 +1665,6 @@ private void setCounterFactorInternal (float factor) { counterView.setAlpha(factor); sendButton.setAlpha(factor); closeButton.setAlpha(factor); - if (senderSendIcon != null) { - senderSendIcon.setAlpha(factor); - } checkCounterHint(); } setCounterEnabled(factor != 0f); @@ -1703,6 +1724,10 @@ private void animateCounterFactor (final float toFactor) { } public void setCounter (int count) { + if (mode == MODE_AVATAR_PICKER) { + return; + } + boolean init = counterFactor == 0f && count == 1; if (init) { prepareCounter(); @@ -1786,92 +1811,22 @@ public void setContentVisible (boolean visible) { } public boolean needCameraButton () { - return (parent instanceof MessagesController) && !((MessagesController) parent).isCameraButtonVisibleOnAttachPanel(); + return mode == MODE_AVATAR_PICKER || (parent instanceof MessagesController) && !((MessagesController) parent).isCameraButtonVisibleOnAttachPanel(); } - public static class SenderSendIcon extends FrameLayout { - private final AvatarView senderAvatarView; - private final Tdlib tdlib; - private final long chatId; - private boolean isPersonal; - private boolean isAnonymous; - private int backgroundColorId; - - public SenderSendIcon (@NonNull Context context, Tdlib tdlib, long chatId) { - super(context); - this.tdlib = tdlib; - this.chatId = chatId; - this.backgroundColorId = ColorId.filling; - - setWillNotDraw(false); - setLayoutParams(FrameLayoutFix.newParams(Screen.dp(19), Screen.dp(19))); - - senderAvatarView = new AvatarView(context); - senderAvatarView.setEnabled(false); - senderAvatarView.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(15), Screen.dp(15), Gravity.CENTER)); - addView(senderAvatarView); - } - - public void setBackgroundColorId (int backgroundColorId) { - this.backgroundColorId = backgroundColorId; - invalidate(); - } - - public AvatarView getSenderAvatarView () { - return senderAvatarView; - } - - public boolean isAnonymous () { - return isAnonymous; - } - - public boolean isPersonal () { - return isPersonal; - } - - @Override - protected void onDraw (Canvas c) { - float cx = getMeasuredWidth() / 2f; - float cy = getMeasuredHeight() / 2f; - c.drawCircle(cx, cy, Screen.dp(19f / 2f), Paints.fillingPaint(Theme.getColor(backgroundColorId))); - - if (isAnonymous) { - c.drawCircle(cx, cy, Screen.dp(15f / 2f), Paints.fillingPaint(Theme.iconLightColor())); - Drawable drawable = Drawables.get(getResources(), R.drawable.infanf_baseline_incognito_11); - Drawables.draw(c, drawable, cx - Screen.dp(5.5f), cy - Screen.dp(5.5f), Paints.getPorterDuffPaint(Theme.getColor(ColorId.badgeMutedText))); - } - - super.onDraw(c); - } - - public void update (TdApi.MessageSender sender) { - final boolean isUserSender = Td.getSenderId(sender) == tdlib.myUserId(); - final boolean isGroupSender = Td.getSenderId(sender) == chatId; - - if (sender == null || isUserSender || isGroupSender) { - update(null, isUserSender, isGroupSender); - } else { - update(sender, false, false); - } - } + public int getCameraButtonOffset () { + return Screen.dp(60); + } - private void update (TdApi.MessageSender sender, boolean isPersonal, boolean isAnonymous) { - this.senderAvatarView.setVisibility(sender != null ? VISIBLE: GONE); - this.senderAvatarView.setMessageSender(tdlib, sender); - this.isAnonymous = isAnonymous; - this.isPersonal = isPersonal; - setVisibility(!isPersonal ? VISIBLE: GONE); - invalidate(); - } + public static HapticMenuHelper.MenuItem createHapticSenderItem (Tdlib tdlib, @NonNull TdApi.Chat chat) { + final long senderId = Td.getSenderId(chat.messageSenderId); - public HapticMenuHelper.MenuItem createHapticSenderItem (TdApi.Chat chat) { - if (isAnonymous()) { - return new HapticMenuHelper.MenuItem(R.id.btn_openSendersMenu, Lang.getString(R.string.SendAs), chat != null ? tdlib.getMessageSenderTitle(chat.messageSenderId): null, R.drawable.dot_baseline_acc_anon_24); - } else if (isPersonal()) { - return new HapticMenuHelper.MenuItem(R.id.btn_openSendersMenu, Lang.getString(R.string.SendAs), chat != null ? tdlib.getMessageSenderTitle(chat.messageSenderId): null, R.drawable.dot_baseline_acc_personal_24); - } else { - return new HapticMenuHelper.MenuItem(R.id.btn_openSendersMenu, Lang.getString(R.string.SendAs), chat != null ? tdlib.getMessageSenderTitle(chat.messageSenderId): null, 0, tdlib, chat != null ? chat.messageSenderId: null, false); - } + if (senderId == chat.id) { + return new HapticMenuHelper.MenuItem(R.id.btn_openSendersMenu, Lang.getString(R.string.SendAs), tdlib.getMessageSenderTitle(chat.messageSenderId), R.drawable.dot_baseline_acc_anon_24); + } else if (senderId == tdlib.myUserId()) { + return new HapticMenuHelper.MenuItem(R.id.btn_openSendersMenu, Lang.getString(R.string.SendAs), tdlib.getMessageSenderTitle(chat.messageSenderId), R.drawable.dot_baseline_acc_personal_24); + } else { + return new HapticMenuHelper.MenuItem(R.id.btn_openSendersMenu, Lang.getString(R.string.SendAs), tdlib.getMessageSenderTitle(chat.messageSenderId), 0, tdlib, chat.messageSenderId, false); } } @@ -1879,11 +1834,11 @@ private void openSetSenderPopup () { TdApi.Chat chat = getTargetChat(); if (chat == null) return; - tdlib().send(new TdApi.GetChatAvailableMessageSenders(getTargetChatId()), result -> { + tdlib().send(new TdApi.GetChatAvailableMessageSenders(getTargetChatId()), (result, error) -> { UI.post(() -> { - if (result.getConstructor() == TdApi.ChatMessageSenders.CONSTRUCTOR) { + if (result != null) { final SetSenderController c = new SetSenderController(getContext(), tdlib()); - c.setArguments(new SetSenderController.Args(chat, ((TdApi.ChatMessageSenders) result).senders, chat.messageSenderId)); + c.setArguments(new SetSenderController.Args(chat, result.senders, chat.messageSenderId)); c.setDelegate(this::setNewMessageSender); c.show(); } @@ -1892,13 +1847,19 @@ private void openSetSenderPopup () { } private void setNewMessageSender (TdApi.ChatMessageSender sender) { - tdlib().send(new TdApi.SetChatMessageSender(getTargetChatId(), sender.sender), o -> { - UI.post(() -> { - TdApi.Chat chat = getTargetChat(); - if (senderSendIcon != null) { - senderSendIcon.update(chat != null ? chat.messageSenderId : null); - } - }); - }); + tdlib().send(new TdApi.SetChatMessageSender(getTargetChatId(), sender.sender), tdlib().typedOkHandler()); + } + + public boolean showSlowModeRestriction (View v) { + CharSequence restriction = tdlib().getSlowModeRestrictionText(getTargetChatId()); + if (restriction != null) { + parent.context().tooltipManager() + .builder(v) + .controller(parent) + .show(parent.tdlib(), restriction).hideDelayed(); + return true; + } + + return false; } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLocationPlaceView.java b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLocationPlaceView.java index a4973bd7cb..0b1cc3ce5f 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLocationPlaceView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLocationPlaceView.java @@ -34,8 +34,9 @@ import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.RippleSupport; -import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.DrawAlgorithms; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Fonts; @@ -214,19 +215,19 @@ public void performDestroy () { // Data - private int circleColorId = ColorId.fileAttach; + private TdlibAccentColor accentColor; private Letters letters; private float lettersWidth; - public void setLocation (String title, String subtitle, @ColorId int circleColorId, Letters letters, boolean isFaded, int livePeriod, long expiresAt) { + public void setLocation (String title, String subtitle, TdlibAccentColor accentColor, Letters letters, boolean isFaded, int livePeriod, long expiresAt) { clearLiveLocation(); setIsFaded(isFaded); timerView.setLivePeriod(livePeriod, expiresAt); titleView.setText(title); addressView.setText(subtitle); boolean needInvalidate = false; - if (this.circleColorId != circleColorId) { - this.circleColorId = circleColorId; + if (this.accentColor != accentColor) { + this.accentColor = accentColor; needInvalidate = true; } if (!StringUtils.equalsOrBothEmpty(this.letters != null ? this.letters.text : null, letters != null ? letters.text : null)) { @@ -371,7 +372,7 @@ protected void onDraw (Canvas c) { c.drawCircle(cx, cy, Screen.dp(IMAGE_RADIUS), Paints.fillingPaint(ColorUtils.alphaColor(alpha, Theme.getColor(ColorId.fileRed)))); if (progressFactor < 1f) { - Paint bitmapPaint = Paints.getPorterDuffPaint(0xffffffff); + Paint bitmapPaint = Paints.whitePorterDuffPaint(); bitmapPaint.setAlpha((int) (255f * (1f - progressFactor) * alpha)); Drawables.draw(c, iconSmall, cx - iconSmall.getMinimumWidth() / 2, cy - iconSmall.getMinimumHeight() / 2, bitmapPaint); bitmapPaint.setAlpha(255); @@ -397,14 +398,14 @@ protected void onDraw (Canvas c) { return; } - c.drawCircle(cx, cy, Screen.dp(IMAGE_RADIUS), Paints.fillingPaint(ColorUtils.alphaColor(alpha, Theme.getColor(circleColorId)))); + c.drawCircle(cx, cy, Screen.dp(IMAGE_RADIUS), Paints.fillingPaint(ColorUtils.alphaColor(alpha, accentColor != null ? accentColor.getPrimaryColor() : Theme.getColor(ColorId.fileAttach)))); if (letters != null) { Paints.drawLetters(c, letters, cx - lettersWidth / 2, cy + Screen.dp(6f), 17f, alpha); } if (letters == null || receiver.getCurrentFile() != null) { if (receiver.needPlaceholder()) { float iconAlpha = alpha - receiver.getDisplayAlpha(); - Paint paint = Paints.getPorterDuffPaint(0xffffffff); + Paint paint = Paints.whitePorterDuffPaint(); paint.setAlpha((int) (255f * iconAlpha)); Drawables.draw(c, iconBig, cx - iconBig.getMinimumWidth() / 2, cy - iconBig.getMinimumHeight() / 2, paint); paint.setAlpha(255); diff --git a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLocationPointView.java b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLocationPointView.java index eb076dd7ce..33cecf5a87 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLocationPointView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/attach/MediaLocationPointView.java @@ -101,13 +101,13 @@ protected void onDraw (Canvas c) { c.drawCircle(cx, cy, Screen.dp(20f), Paints.fillingPaint(color)); progressComponent.draw(c); if (activeFactor > 0f && placeFactor < 1f) { - Paint paint = Paints.getPorterDuffPaint(0xffffffff); + Paint paint = Paints.whitePorterDuffPaint(); paint.setAlpha((int) (255f * activeFactor * (1f - placeFactor))); Drawables.draw(c, locationIcon, cx - locationIcon.getMinimumWidth() / 2, cy - locationIcon.getMinimumHeight() / 2, paint); paint.setAlpha(255); } if (placeFactor > 0f) { - Paint paint = Paints.getPorterDuffPaint(0xffffffff); + Paint paint = Paints.whitePorterDuffPaint(); paint.setAlpha((int) (255f * placeFactor)); c.save(); c.scale(.7f, .7f, cx, cy); diff --git a/app/src/main/java/org/thunderdog/challegram/component/base/SettingView.java b/app/src/main/java/org/thunderdog/challegram/component/base/SettingView.java index 99a55f207c..db04307b3d 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/base/SettingView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/base/SettingView.java @@ -32,6 +32,7 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.Px; import androidx.annotation.StringRes; import org.drinkless.tdlib.TdApi; @@ -40,16 +41,19 @@ import org.thunderdog.challegram.component.user.RemoveHelper; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.loader.ComplexReceiver; +import org.thunderdog.challegram.loader.ComplexReceiverProvider; import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.navigation.TooltipOverlayView; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.RippleSupport; import org.thunderdog.challegram.telegram.TGLegacyManager; import org.thunderdog.challegram.telegram.Tdlib; -import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; @@ -76,10 +80,11 @@ import me.vkryl.android.widget.FrameLayoutFix; import me.vkryl.core.BitwiseUtils; import me.vkryl.core.ColorUtils; +import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.lambda.Destroyable; -public class SettingView extends FrameLayoutFix implements FactorAnimator.Target, TGLegacyManager.EmojiLoadListener, AttachDelegate, Destroyable, RemoveHelper.RemoveDelegate, TextColorSet, TooltipOverlayView.LocationProvider { +public class SettingView extends FrameLayoutFix implements FactorAnimator.Target, TGLegacyManager.EmojiLoadListener, AttachDelegate, Destroyable, RemoveHelper.RemoveDelegate, TextColorSet, TooltipOverlayView.LocationProvider, ComplexReceiverProvider { public static final int TYPE_INFO = 0x01; public static final int TYPE_SETTING = 0x02; public static final int TYPE_RADIO = 0x03; @@ -238,10 +243,23 @@ public ImageReceiver getReceiver () { return receiver; } + @Override public ComplexReceiver getComplexReceiver () { return complexReceiver; } + public @Px float getMeasuredNameTop () { + return pTop; + } + + public @Px float getMeasuredNameStart () { + return pLeft; + } + + public @Px int getMeasuredNameWidth () { + return displayItemNameWidth; + } + public void setTextColorId (@ColorId int textColorId) { if (textColorId == ColorId.NONE) textColorId = ColorId.text; @@ -437,6 +455,7 @@ public void setText (TextWrapper text) { if (getMeasuredHeight() != getCurrentHeight() && getMeasuredHeight() != 0) { requestLayout(); } + checkEmojiListener(); invalidate(); } @@ -618,7 +637,7 @@ private void buildLayout (int totalWidth, int totalHeight) { private boolean subscribedToEmojiUpdates; private void checkEmojiListener () { - boolean needEmojiListener = this.displayItemNameLayout != null || this.displayItemDataLayout != null || (this.displayItemNameText != null && this.displayItemNameText.hasMedia()); + boolean needEmojiListener = this.displayItemNameLayout != null || this.displayItemDataLayout != null || (this.displayItemNameText != null && this.displayItemNameText.hasBuiltInEmoji()) || (this.text != null && this.text.hasBuiltInEmoji()); if (this.subscribedToEmojiUpdates != needEmojiListener) { this.subscribedToEmojiUpdates = needEmojiListener; if (needEmojiListener) { @@ -629,6 +648,12 @@ private void checkEmojiListener () { } } + private final BoolAnimator iconRotated = new BoolAnimator(this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180L, false); + + public void setIconRotated (boolean rotated, boolean animated) { + iconRotated.setValue(rotated, animated); + } + private final BoolAnimator isEnabled = new BoolAnimator(this, AnimatorUtils.DECELERATE_INTERPOLATOR, 168l, true); public void setEnabledAnimated (boolean enabled) { @@ -694,6 +719,14 @@ public void setVisuallyEnabled (boolean enabled, boolean animated) { isEnabled.setValue(enabled, animated); } + public boolean isVisuallyEnabled () { + return isEnabled.getValue(); + } + + public float getVisuallyEnabledFactor () { + return isEnabled.getFloatValue(); + } + private static void drawText (Canvas c, CharSequence text, Layout layout, float x, float y, int textY, Paint paint, boolean rtl, int viewWidth, float textWidth, Text wrap, TextColorSet textColorSet, EmojiStatusHelper emojiStatusHelper) { if (wrap != null) { wrap.draw(c, (int) x, (int) (viewWidth - x), 0, textY, textColorSet != null ? textColorSet : TextColorSets.Regular.NEGATIVE); @@ -708,9 +741,9 @@ private static void drawText (Canvas c, CharSequence text, Layout layout, float } } - private int iconColorId; + private @PorterDuffColorId int iconColorId = ColorId.NONE; - public void setIconColorId (int colorId) { + public void setIconColorId (@PorterDuffColorId int colorId) { if (this.iconColorId != colorId) { this.iconColorId = colorId; if (icon != null) @@ -799,7 +832,19 @@ protected void onDraw (Canvas c) { int width = getMeasuredWidth(); if (icon != null) { int x = (int) (rtl ? width - pIconLeft - icon.getMinimumWidth() : pIconLeft) + Screen.dp(24f) / 2 - icon.getMinimumWidth() / 2; - Drawables.draw(c, icon, x, pIconTop, lastIconResource == 0 ? Paints.getBitmapPaint() : iconColorId != 0 ? Paints.getPorterDuffPaint(Theme.getColor(iconColorId)) : Paints.getIconGrayPorterDuffPaint()); + float y = pIconTop; + final boolean needRotateIcon = iconRotated.getFloatValue() > 0; + if (needRotateIcon) { + float cx = x + icon.getMinimumWidth() / 2f; + float cy = y + icon.getMinimumHeight() / 2f; + c.save(); + c.rotate(MathUtils.fromTo(0, 90, iconRotated.getFloatValue()), cx, cy); + } + Drawables.draw(c, icon, x, y, lastIconResource == 0 ? Paints.getBitmapPaint() : iconColorId != 0 ? PorterDuffPaint.get(iconColorId) : Paints.getIconGrayPorterDuffPaint()); + if (needRotateIcon) { + c.restore(); + } + // c.drawBitmap(icon, x, pIconTop, paint); if (overlay != null) { c.save(); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/ChatBottomBarView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/ChatBottomBarView.java index b105c0a9b1..261cb86453 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/ChatBottomBarView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/ChatBottomBarView.java @@ -32,6 +32,7 @@ import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.util.SimpleDrawable; @@ -174,11 +175,8 @@ public void draw (Canvas c, View view, float collapseFactor, float factor) { drawingText.draw(c, cx - drawingText.getWidth() / 2, cy - drawingText.getHeight() / 2, null, factor * (1f - collapseFactor)); } if (collapseFactor > 0f && drawable != null) { - Paint paint = Paints.getPorterDuffPaint(Theme.getColor(ColorId.circleButtonChatIcon)); - final int restoreAlpha = paint.getAlpha(); - paint.setAlpha((int) ((float) restoreAlpha * factor * collapseFactor)); + Paint paint = PorterDuffPaint.get(ColorId.circleButtonChatIcon, factor * collapseFactor); Drawables.draw(c, drawable, cx - drawable.getMinimumWidth() / 2, cy - drawable.getMinimumHeight() / 2, paint); - paint.setAlpha(restoreAlpha); } if (needScale) { Views.restore(c, saveCount); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/ChatHeaderView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/ChatHeaderView.java index 5987ca2012..99a46b04fe 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/ChatHeaderView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/ChatHeaderView.java @@ -111,7 +111,7 @@ public void setChat (Tdlib tdlib, TdApi.Chat chat, @Nullable ThreadInfo messageT setUseRedHighlight(false); attachChatStatus(messageThread.getChatId(), messageThread.getMessageThreadId()); } else { - setEmojiStatus(tdlib.isSelfChat(chat) ? null: tdlib.chatUser(chat)); + setEmojiStatus(tdlib.isSelfChat(chat) ? null : tdlib.chatUser(chat)); setText(tdlib.chatTitle(chat), !StringUtils.isEmpty(forcedSubtitle) ? forcedSubtitle : tdlib.status().chatStatus(chat)); setExpandedSubtitle(tdlib.status().chatStatusExpanded(chat)); setUseRedHighlight(tdlib.isRedTeam(chat.id)); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/ChatMembersSearcher.java b/app/src/main/java/org/thunderdog/challegram/component/chat/ChatMembersSearcher.java index 77ccd6b703..d4cc6553ed 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/ChatMembersSearcher.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/ChatMembersSearcher.java @@ -5,15 +5,16 @@ import androidx.annotation.UiThread; import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.Log; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibThread; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.util.text.Highlight; import java.util.ArrayList; -import java.util.Objects; import me.vkryl.core.ArrayUtils; +import me.vkryl.core.ObjectUtils; import me.vkryl.core.StringUtils; import me.vkryl.td.ChatId; import me.vkryl.td.Td; @@ -74,12 +75,16 @@ private void performRequest (String contextId, long chatId, String query, Handle boolean isSupergroup = supergroupId != 0; if (currentFilter == FILTER_TYPE_GLOBAL_GLOBAL) { + Log.ensureReturnType(TdApi.SearchPublicChats.class, TdApi.Chats.class); tdlib.client().send(new TdApi.SearchPublicChats(query), o -> onResult(contextId, o)); } else if (currentFilter == FILTER_TYPE_GLOBAL_LOCAL) { + Log.ensureReturnType(TdApi.SearchChatsOnServer.class, TdApi.Chats.class); tdlib.client().send(new TdApi.SearchChatsOnServer(query, 50), o -> onResult(contextId, o)); } else if (isSupergroup) { + Log.ensureReturnType(TdApi.GetSupergroupMembers.class, TdApi.ChatMembers.class); tdlib.client().send(new TdApi.GetSupergroupMembers(supergroupId, makeSupergroupFilter(query, currentFilter), currentOffset, LIMIT), o -> onResult(contextId, o)); } else { + Log.ensureReturnType(TdApi.SearchChatMembers.class, TdApi.ChatMembers.class); tdlib.client().send(new TdApi.SearchChatMembers(chatId, query, LIMIT, makeBasicGroupFilter(currentFilter)), o -> onResult(contextId, o)); } } @@ -131,6 +136,8 @@ private void onResult (String contextId, TdApi.Object object) { UI.showError(object); break; } + default: + throw new UnsupportedOperationException(object.toString()); } if (needSetNextFilter) { @@ -209,7 +216,7 @@ private boolean queryUserCheck (long userId, String query) { } private void performRequestForUserChat (long userId, String query, Handler handler) { - long otherUserId = ChatId.isSecret(userId) ? tdlib.chatUserId(userId): userId; + long otherUserId = ChatId.isSecret(userId) ? tdlib.chatUserId(userId) : userId; long myUserId = tdlib.myUserId(); boolean otherUserOk = queryUserCheck(otherUserId, query); @@ -230,7 +237,7 @@ private void performRequestForUserChat (long userId, String query, Handler handl } private boolean checkContextId (String contextId) { - if (!Objects.equals(contextId, currentContextId)) { + if (!ObjectUtils.equals(contextId, currentContextId)) { currentContextId = contextId; reset(); return true; diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/ChatSearchMembersView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/ChatSearchMembersView.java index e268db64e2..030a09ac97 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/ChatSearchMembersView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/ChatSearchMembersView.java @@ -15,7 +15,6 @@ import androidx.recyclerview.widget.RecyclerView; import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.attach.CustomItemAnimator; import org.thunderdog.challegram.core.Lang; @@ -266,8 +265,7 @@ private void processData (final TdApi.Object object) { break; } default: { - Log.unexpectedTdlibResponse(object, TdApi.GetChats.class, TdApi.Chats.class); - return; + throw new UnsupportedOperationException(object.toString()); } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/DetachedChatHeaderView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/DetachedChatHeaderView.java index 57cec84929..8317703148 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/DetachedChatHeaderView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/DetachedChatHeaderView.java @@ -29,7 +29,8 @@ import org.thunderdog.challegram.loader.ImageFileLocal; import org.thunderdog.challegram.navigation.HeaderView; import org.thunderdog.challegram.telegram.Tdlib; -import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.telegram.TdlibAccentColor; +import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; @@ -45,7 +46,7 @@ public class DetachedChatHeaderView extends SparseDrawableView implements Destroyable { private final DoubleImageReceiver avatar = new DoubleImageReceiver(this, 0); - private int avatarPlaceholderColor; + private TdlibAccentColor accentColor; private Drawable avatarPlaceholderDrawable; private final Drawable topShadow; @@ -87,6 +88,7 @@ public void bindWith (Tdlib tdlib, TdApi.ChatInviteLinkInfo linkInfo) { .clipTextArea() .allBold() .build(); + accentColor = tdlib.chatAccentColor(linkInfo.chatId); subtitle = new Text.Builder(Lang.pluralMembers(linkInfo.memberCount, 0, isChannel).toString(), getMeasuredWidth(), Paints.robotoStyleProvider(14), TextColorSets.WHITE).singleLine().build(); @@ -107,11 +109,9 @@ public void bindWith (Tdlib tdlib, TdApi.ChatInviteLinkInfo linkInfo) { fileNetwork.setScaleType(ImageFile.CENTER_CROP); avatarPlaceholderDrawable = null; - avatarPlaceholderColor = 0; avatar.requestFile(file, fileNetwork); } else { - avatarPlaceholderDrawable = getSparseDrawable(isChannel ? R.drawable.baseline_bullhorn_56 : R.drawable.baseline_group_56, 0); - avatarPlaceholderColor = TD.getAvatarColorId(linkInfo.chatId, tdlib.myUserId()); + avatarPlaceholderDrawable = getSparseDrawable(isChannel ? R.drawable.baseline_bullhorn_56 : R.drawable.baseline_group_56, ColorId.NONE); avatar.clear(); } } @@ -135,7 +135,7 @@ protected void onDraw (Canvas canvas) { int cx = avatar.centerX(); int cy = avatar.centerY(); - canvas.drawColor(Theme.getColor(avatarPlaceholderColor)); + canvas.drawColor(accentColor.getPrimaryColor()); float placeholderScale = (avatar.getWidth() / 2f / ((float) getMeasuredWidth() / 2f)); canvas.save(); canvas.scale(placeholderScale, placeholderScale, cx, cy); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/EmojiToneHelper.java b/app/src/main/java/org/thunderdog/challegram/component/chat/EmojiToneHelper.java index c5eae8daf0..e2228bbcc4 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/EmojiToneHelper.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/EmojiToneHelper.java @@ -15,10 +15,6 @@ package org.thunderdog.challegram.component.chat; import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; @@ -30,33 +26,37 @@ import androidx.annotation.Nullable; +import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.sticker.TGStickerObj; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.emoji.Emoji; -import org.thunderdog.challegram.emoji.EmojiInfo; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.RippleSupport; +import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.EmojiData; import org.thunderdog.challegram.tool.Fonts; -import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.widget.BubbleLayout; import org.thunderdog.challegram.widget.NoScrollTextView; +import org.thunderdog.challegram.widget.emoji.EmojiToneListView; import me.vkryl.android.AnimatorUtils; import me.vkryl.android.animator.BoolAnimator; import me.vkryl.android.animator.FactorAnimator; import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; +import me.vkryl.core.lambda.RunnableData; public class EmojiToneHelper implements FactorAnimator.Target { public interface Delegate { + default long getCurrentChatId () { return 0; }; int[] displayBaseViewWithAnchor (EmojiToneHelper context, View anchorView, View viewToDisplay, int viewWidth, int viewHeight, int horizontalMargin, int horizontalOffset, int verticalOffset); void removeView (EmojiToneHelper context, View displayedView); } @@ -80,17 +80,20 @@ public static int[] defaultDisplay (EmojiToneHelper context, View anchorView, Vi return new int[] {left - x, top - y}; } + private final Tdlib tdlib; private final Context context; private final Delegate delegate; private final @Nullable ViewController themeProvider; - public EmojiToneHelper (Context context, Delegate delegate, @Nullable ViewController themeProvider) { + public EmojiToneHelper (Context context, Delegate delegate, Tdlib tdlib, @Nullable ViewController themeProvider) { + this.tdlib = tdlib; this.context = context; this.delegate = delegate; this.themeProvider = themeProvider; } // Entry point + private View visibleAnchorView; private String emoji; private int emojiColorState; @@ -104,10 +107,14 @@ public EmojiToneHelper (Context context, Delegate delegate, @Nullable ViewContro private static final int ANIMATOR_POSITION = 1; - public boolean openForEmoji (View anchorView, float startX, float startY, String emoji, int emojiColorState, String currentTone, String[] currentOtherTones) { + private TdApi.Sticker[] installedStickers, recommendedStickers; + + public boolean openForEmoji (View anchorView, float startX, float startY, String emoji, int emojiColorState, String currentTone, String[] currentOtherTones, @Nullable TdApi.Sticker[] installedStickers, @Nullable TdApi.Sticker[] recommendedStickers) { this.emoji = emoji; this.emojiColorState = emojiColorState; this.currentTone = currentTone; + this.installedStickers = installedStickers; + this.recommendedStickers = recommendedStickers; int colorCount = emojiColorState >= EmojiData.STATE_HAS_TWO_COLORS ? (emojiColorState - EmojiData.STATE_HAS_TWO_COLORS + 1) : 0; if (colorCount > 0) { if (currentOtherTones != null) { @@ -125,7 +132,10 @@ public boolean openForEmoji (View anchorView, float startX, float startY, String this.startX = startX; this.offsetLeft = offsetTop = 0; this.buttonOffsetLeft = buttonOffsetTop = 0; + this.visibleAnchorView = anchorView; + setIsVisible(anchorView, true); + return true; } @@ -149,29 +159,17 @@ public void processMovement (View anchorView, MotionEvent e, float x, float y) { if (startedMoving) { float relativeX = x - offsetLeft; float relativeY = y - offsetTop; - switch (emojiColorState) { - case EmojiData.STATE_NO_COLORS: - // Do nothing - break; - case EmojiData.STATE_HAS_ONE_COLOR: - // Offer to apply tone to all emoji - processSingleToneMovement(anchorView, e, x, y, relativeX, relativeY); - break; - case EmojiData.STATE_HAS_TWO_COLORS: - default: - // Show picker of the more tones, no apply to all - processMultipleToneMovement(anchorView, e, x, y, relativeX, relativeY, emojiColorState - EmojiData.STATE_HAS_TWO_COLORS + 1); - break; - } + + processSingleToneMovement(anchorView, e, x, y, relativeX, relativeY); } } private void processSingleToneMovement (View anchorView, MotionEvent e, float x, float y, float relativeX, float relativeY) { boolean updatePosition = false; - if (relativeY >= 0 && !applyButtonPressed && emojiTonePicker.changeIndex(relativeX)) { + if (relativeY >= 0 && !applyButtonPressed && emojiTonePicker.changeIndex(relativeX, relativeY)) { toneChanged = true; - currentTone = EmojiData.emojiColors[emojiTonePicker.toneIndex]; + currentTone = EmojiData.emojiColors[emojiTonePicker.getToneIndex()]; updatePosition = true; if (!needPositionAnimation()) { updatePosition = false; @@ -179,15 +177,15 @@ private void processSingleToneMovement (View anchorView, MotionEvent e, float x, } } - boolean canShowApply = Emoji.instance().canApplyDefaultTone(currentTone); + boolean canShowApply = emojiTonePicker.hasToneEmoji() && emojiTonePicker.getToneIndexVertical() == 0 && Emoji.instance().canApplyDefaultTone(currentTone); if (showingApplyButton) { - if (!canShowApply || (relativeY > Screen.dp(46f) && !(toneChanged && (Config.SHOW_EMOJI_TONE_PICKER_ALWAYS || Settings.instance().needTutorial(Settings.TUTORIAL_EMOJI_TONE_ALL))))) { + if (!canShowApply || (relativeY > Screen.dp(EmojiToneListView.ITEM_SIZE + EmojiToneListView.ITEM_PADDING) && !(toneChanged && (Config.SHOW_EMOJI_TONE_PICKER_ALWAYS || Settings.instance().needTutorial(Settings.TUTORIAL_EMOJI_TONE_ALL))))) { setApplyButtonPressed(anchorView, false, e, x - buttonOffsetLeft - applyButton.getTranslationX(), y - buttonOffsetTop); setShowingApplyButton(anchorView, false); updatePosition = false; } } else if (canShowApply) { - if (relativeY < Screen.dp(46f) * .95f || (toneChanged && (Config.SHOW_EMOJI_TONE_PICKER_ALWAYS || Settings.instance().needTutorial(Settings.TUTORIAL_EMOJI_TONE_ALL)))) { + if (relativeY < Screen.dp(EmojiToneListView.ITEM_SIZE + EmojiToneListView.ITEM_PADDING) * .95f || (toneChanged && (Config.SHOW_EMOJI_TONE_PICKER_ALWAYS || Settings.instance().needTutorial(Settings.TUTORIAL_EMOJI_TONE_ALL)))) { setShowingApplyButton(anchorView, true); // UI.forceVibrate(anchorView, false); } @@ -205,29 +203,13 @@ private void processSingleToneMovement (View anchorView, MotionEvent e, float x, } } - private void processMultipleToneMovement (View anchorView, MotionEvent e, float x, float y, float relativeX, float relativeY, int toneCount) { - processSingleToneMovement(anchorView, e, x, y, relativeX, relativeY); - // TODO - /*int canShowCount; - if (Strings.isEmpty(currentTone)) { - canShowCount = 1; - } else { - canShowCount = 2; - for (String otherTone : currentOtherTones) { - if (Strings.isEmpty(otherTone)) - break; - canShowCount++; - } - }*/ - } - private float positionFactor; private FactorAnimator positionAnimator; private void setPositionFactor (float factor, boolean animated) { if (animated) { if (positionAnimator == null) { - positionAnimator = new FactorAnimator(ANIMATOR_POSITION, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180l, this.positionFactor); + positionAnimator = new FactorAnimator(ANIMATOR_POSITION, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180L, this.positionFactor); } positionAnimator.animateTo(factor); } else { @@ -293,6 +275,7 @@ private boolean isVisible () { return visibilityAnimator != null && visibilityAnimator.getValue(); } private void setIsVisible (View anchorView, boolean isVisible) { + visibleAnchorView = isVisible ? anchorView : null; boolean wasVisible = isVisible(); if (wasVisible != isVisible) { if (visibilityAnimator == null) { @@ -352,13 +335,12 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca private void updateApplyPosition () { if (applyButton != null) { - int paddingHorizontal = Screen.dp(4.5f); - int totalWidth = (Screen.dp(TONES_WIDTH) - paddingHorizontal * 2); + int totalWidth = Screen.dp(EmojiToneListView.ITEM_SIZE * 6 + (EmojiToneListView.VIEW_PADDING_HORIZONTAL + EmojiToneListView.ITEM_PADDING) * 2); int itemWidth = totalWidth / EmojiData.emojiColors.length; float factor = positionFactor / (float) (EmojiData.emojiColors.length - 1); int buttonWidth = applyButton.getMeasuredWidth(); - int margin = itemWidth / 2; + int margin = itemWidth / 2 + Screen.dp(EmojiToneListView.VIEW_PADDING_HORIZONTAL + EmojiToneListView.ITEM_PADDING); applyButton.setCornerCenterX(margin + (int) ((float) (buttonWidth - margin * 2) * factor)); applyButton.setTranslationX((float) (totalWidth - buttonWidth) * factor); } @@ -392,8 +374,8 @@ private void setShowingApplyButton (View anchorView, boolean showingApplyButton) return; } - int paddingHorizontal = Screen.dp(4.5f); - int itemWidth = paddingHorizontal + (Screen.dp(TONES_WIDTH) - paddingHorizontal * 2) / EmojiData.emojiColors.length; + int paddingHorizontal = Screen.dp(EmojiToneListView.VIEW_PADDING_HORIZONTAL); + int itemWidth = paddingHorizontal + Screen.dp(EmojiToneListView.ITEM_SIZE); applyButton = new BubbleLayout(context, themeProvider, false) { @Override @@ -431,7 +413,7 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { sendView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); applyButton.addView(sendView); - int tonesHeight = Screen.dp(46f) + Screen.dp(2f) + Screen.dp(4f) + Screen.dp(8f); + int tonesHeight = emojiTonePicker.calcViewHeight() + Screen.dp(4f); int[] result = delegate.displayBaseViewWithAnchor(this, anchorView, applyButton, ViewGroup.LayoutParams.WRAP_CONTENT, Screen.dp(48f) + applyButton.getPaddingTop() + applyButton.getPaddingBottom(), Screen.dp(4f), offsetLeft, Screen.dp(8f) - tonesHeight + Screen.dp(6f)); @@ -446,9 +428,9 @@ private boolean needPositionAnimation () { } private void animatePosition (boolean animate) { - if (positionIndex != emojiTonePicker.toneIndex || !animate) { - positionIndex = emojiTonePicker.toneIndex; - setPositionFactor(emojiTonePicker.toneIndex, animate); + if (positionIndex != emojiTonePicker.getToneIndex() || !animate) { + positionIndex = emojiTonePicker.getToneIndex(); + setPositionFactor(emojiTonePicker.getToneIndex(), animate); } } @@ -458,135 +440,29 @@ private boolean prepareWrap (View anchorView) { } emojiTonePicker = new EmojiToneListView(context); - emojiTonePicker.init(themeProvider, false); - emojiTonePicker.setEmoji(emoji, currentTone); + emojiTonePicker.init(themeProvider, tdlib); + emojiTonePicker.setEmoji(emoji, currentTone, emojiColorState); + emojiTonePicker.setCustomEmoji(installedStickers, recommendedStickers); if (themeProvider != null) { themeProvider.addThemeInvalidateListener(emojiTonePicker); } animatePosition(false); - int tonesWidth = Screen.dp(TONES_WIDTH); - int tonesHeight = Screen.dp(46f) + Screen.dp(2f) + Screen.dp(4f) + Screen.dp(8f); + int tonesWidth = emojiTonePicker.calcViewWidth(); + int tonesHeight = emojiTonePicker.calcViewHeight() + Screen.dp(4f); int[] result = delegate.displayBaseViewWithAnchor(this, anchorView, emojiTonePicker, tonesWidth, tonesHeight, Screen.dp(4f), anchorView.getMeasuredWidth() / 2 - Math.min(Screen.dp(23f), tonesWidth / 2), Screen.dp(8f)); offsetLeft = result[0]; offsetTop = result[1]; emojiTonePicker.setAnchorView(anchorView, offsetLeft); - - return true; - } - - private static final float TONES_WIDTH = 240f; - - private static class EmojiToneListView extends View { - private EmojiInfo[] tones; - private Drawable backgroundDrawable, cornerDrawable; - - public EmojiToneListView (Context context) { - super(context); - } - - private void init (ViewController themeProvider, boolean is2d) { - this.tones = new EmojiInfo[EmojiData.emojiColors.length - (is2d ? 1 : 0)]; - this.backgroundDrawable = Theme.filteredDrawable(R.drawable.stickers_back_all, ColorId.overlayFilling, themeProvider); - this.cornerDrawable = Theme.filteredDrawable(R.drawable.stickers_back_arrow, ColorId.overlayFilling, themeProvider); - } - - private View boundView; - private int offsetLeft; - - public void setAnchorView (View view, int offsetLeft) { - this.boundView = view; - this.offsetLeft = offsetLeft; - setPivotX(view.getMeasuredWidth() / 2 - offsetLeft); - setPivotY(Screen.dp(46f) + Screen.dp(3.5f) + Screen.dp(8f) / 2); - } - - public View getAnchorView () { - return boundView; - } - - private int toneIndex = -1; - - public void setEmoji (String emoji, String currentTone) { - int i = 0; - for (String tone : EmojiData.emojiColors) { - if (tone == null && currentTone == null) { - toneIndex = 0; - } else if (StringUtils.equalsOrBothEmpty(tone, currentTone)) { - toneIndex = i; - } - tones[i] = Emoji.instance().getEmojiInfo(EmojiData.instance().colorize(emoji, tone)); - i++; - } - } - - public boolean changeIndex (float x) { - int paddingHorizontal = Screen.dp(4.5f); - x -= paddingHorizontal; - int viewWidth = (Screen.dp(TONES_WIDTH) - paddingHorizontal * 2); - - int result; - if (x <= 0) { - result = 0; - } else if (x >= viewWidth) { - result = tones.length - 1; - } else { - result = Math.max(0, Math.min(tones.length - 1, (int) ((x / (float) viewWidth) * ((float) tones.length)))); - } - - if (result != -1 && result != toneIndex) { - toneIndex = result; - invalidate(); - return true; - } - return false; - } - - @Override - protected void onDraw (Canvas c) { - int paddingVertical = Screen.dp(4f); - int paddingHorizontal = Screen.dp(4.5f); - int itemWidth = (Screen.dp(TONES_WIDTH) - paddingHorizontal * 2) / tones.length; - int itemHeight = Screen.dp(46f); - - int x = paddingHorizontal; - int y = Screen.dp(2.5f); - - backgroundDrawable.setBounds(0, 0, getMeasuredWidth(), itemHeight + paddingVertical * 2); - backgroundDrawable.draw(c); - - int cornerWidth = Screen.dp(18f); - int cornerHeight = Screen.dp(8f); - int cornerX = 0; - int cornerY = itemHeight + Screen.dp(3.5f); - if (boundView != null) { - cornerX = boundView.getMeasuredWidth() / 2 - offsetLeft - cornerWidth / 2; - } - cornerDrawable.setBounds(cornerX, cornerY, cornerX + cornerWidth, cornerY + cornerHeight); - cornerDrawable.draw(c); - - int rectX = x + itemWidth * toneIndex; - int rectY = y + paddingVertical / 2; - RectF rectF = Paints.getRectF(); - rectF.set(rectX, rectY, rectX + itemWidth, rectY + itemHeight - paddingVertical); - c.drawRoundRect(rectF, Screen.dp(4f), Screen.dp(4f), Paints.fillingPaint(Theme.HALF_RIPPLE_COLOR)); - - for (EmojiInfo info : tones) { - int cx = x + itemWidth / 2; - int cy = y + itemHeight / 2; - int itemSize = Math.min(itemWidth, itemHeight) - Screen.dp(4f); - Rect rect = Paints.getRect(); - rect.left = cx - itemSize / 2; - rect.top = cy - itemSize / 2; - rect.right = rect.left + itemSize; - rect.bottom = rect.top + itemSize; - Emoji.instance().draw(c, info, rect); - x += itemWidth; - } + if (emojiTonePicker.getRowsCount() != 1 && emojiTonePicker.hasToneEmoji()) { + emojiTonePicker.changeIndex( + anchorView.getMeasuredWidth() / 2f - offsetLeft, + anchorView.getMeasuredHeight() / 2f - offsetTop); } + return true; } private void destroyWrap () { @@ -609,4 +485,29 @@ private void destroyWrap () { applyButton = null; } } + + public long getCurrentChatId () { + return delegate != null ? delegate.getCurrentChatId() : 0; + } + + public boolean isInSelfChat () { + long chatId = delegate != null ? delegate.getCurrentChatId() : 0; + return chatId != 0 && tdlib.isSelfChat(chatId); + } + + public TGStickerObj getSelectedCustomEmoji () { + return emojiTonePicker != null ? emojiTonePicker.getSelectedCustomEmoji() : null; + } + + private RunnableData onCustomEmojiSelectedListener; + + public void setOnCustomEmojiSelectedListener (RunnableData onCustomEmojiSelectedListener) { + this.onCustomEmojiSelectedListener = onCustomEmojiSelectedListener; + } + + public void onCustomEmojiSelected (TGStickerObj stickerObj) { + if (onCustomEmojiSelectedListener != null) { + onCustomEmojiSelectedListener.runWithData(stickerObj); + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/EmojiView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/EmojiView.java index 0ae030a753..fd47aae6db 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/EmojiView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/EmojiView.java @@ -21,14 +21,18 @@ import android.view.View; import android.view.ViewParent; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.component.sticker.TGStickerObj; import org.thunderdog.challegram.emoji.Emoji; import org.thunderdog.challegram.emoji.EmojiInfo; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.tool.EmojiData; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; -import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.unsorted.Settings; import me.vkryl.android.util.ClickHelper; @@ -36,10 +40,12 @@ public class EmojiView extends View implements ClickHelper.Delegate { private final ClickHelper helper; + private final Tdlib tdlib; private final EmojiToneHelper toneHelper; - public EmojiView (Context context, EmojiToneHelper toneHelper) { + public EmojiView (Context context, Tdlib tdlib, EmojiToneHelper toneHelper) { super(context); + this.tdlib = tdlib; this.toneHelper = toneHelper; this.helper = new ClickHelper( this); } @@ -161,19 +167,55 @@ public void onClickAt (View view, float x, float y) { @Override public boolean needLongPress (float x, float y) { - return colorState != EmojiData.STATE_NO_COLORS && toneHelper != null && toneHelper.canBeShown(); + if (toneHelper != null && toneHelper.canBeShown()) { + if (tdlib.hasPremium() || toneHelper.isInSelfChat()) { + preliminaryLoadCustomEmoji(emoji); + return true; + } + return colorState != EmojiData.STATE_NO_COLORS; + } + return false; + } + + private TdlibUi.EmojiStickers emojiStickers; + + private void preliminaryLoadCustomEmoji (String emoji) { + emojiStickers = tdlib.ui().getEmojiStickers(new TdApi.StickerTypeCustomEmoji(), emoji, false, 6, toneHelper.getCurrentChatId()); } @Override public boolean onLongPressRequestedAt (View view, float x, float y) { - if (colorState != EmojiData.STATE_NO_COLORS) { - UI.forceVibrate(view, false); - setInLongPress(true); - return toneHelper.openForEmoji(view, x, y, emoji, colorState, emojiTone, emojiOtherTones); + if (emojiStickers == null || !emojiStickers.query.equals(this.emoji)) { + if (colorState != EmojiData.STATE_NO_COLORS) { + onLongClick(view, x, y, null, null); + } + return false; } + emojiStickers.getStickers(new TdlibUi.EmojiStickers.Callback() { + @Override + public void onStickersLoaded (TdlibUi.EmojiStickers context, @NonNull TdApi.Sticker[] installedStickers, @Nullable TdApi.Sticker[] recommendedStickers, boolean expectMoreStickers) { + if (emojiStickers == context) { + int totalCount = installedStickers.length + (recommendedStickers != null ? recommendedStickers.length : 0); + if (totalCount > 0 || colorState != EmojiData.STATE_NO_COLORS) { + onLongClick(view, x, y, installedStickers, recommendedStickers); + } + } + } + + @Override + public void onRecommendedStickersLoaded (TdlibUi.EmojiStickers context, @NonNull TdApi.Sticker[] recommendedStickers) { + // TODO show recommended stickers + } + }, 300); return false; } + private void onLongClick (View view, float x, float y, @Nullable TdApi.Sticker[] installedStickers, @Nullable TdApi.Sticker[] recommendedStickers) { + helper.onLongPress(view, x, y); + setInLongPress(true); + toneHelper.openForEmoji(view, x, y, emoji, colorState, emojiTone, emojiOtherTones, installedStickers, recommendedStickers); + } + @Override public void onLongPressMove (View view, MotionEvent e, float x, float y, float startX, float startY) { toneHelper.processMovement(view, e, x, y); @@ -181,6 +223,7 @@ public void onLongPressMove (View view, MotionEvent e, float x, float y, float s @Override public void onLongPressCancelled (View view, float x, float y) { + emojiStickers = null; setInLongPress(false); toneHelper.hide(view); } @@ -190,15 +233,19 @@ public void onLongPressFinish (View view, float x, float y) { if (view != this) throw new AssertionError(); completeToneSelection(); + emojiStickers = null; } public void completeToneSelection () { String emoji = toneHelper.getSelectedEmoji(); String selectedTone = toneHelper.getSelectedTone(); String[] selectedOtherTones = toneHelper.getSelectedOtherTones(); + TGStickerObj selectedCustomEmoji = toneHelper.getSelectedCustomEmoji(); boolean needApplyToAll = toneHelper.needApplyToAll(); - if (needApplyToAll) { + if (selectedCustomEmoji != null) { + toneHelper.onCustomEmojiSelected(selectedCustomEmoji); + } else if (needApplyToAll) { if (toneHelper.needForgetApplyToAll()) { Settings.instance().markTutorialAsComplete(Settings.TUTORIAL_EMOJI_TONE_ALL); } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/InlineResultsAdapter.java b/app/src/main/java/org/thunderdog/challegram/component/chat/InlineResultsAdapter.java index 2e4cf8629e..ce0a0a76e9 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/InlineResultsAdapter.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/InlineResultsAdapter.java @@ -57,6 +57,7 @@ public class InlineResultsAdapter extends RecyclerView.Adapter> items; private final ThemeListenerList themeProvider; + private StickerSmallView.StickerMovementCallback stickerMovementCallback; private Tdlib tdlib; @@ -71,6 +72,12 @@ public InlineResultsAdapter (Context context, InlineResultsWrap parent, ThemeLis this.items = new ArrayList<>(); this.parent = parent; this.themeProvider = themeProvider; + this.stickerMovementCallback = parent; + } + + public void setStickerMovementCallback (StickerSmallView.StickerMovementCallback stickerMovementCallback) { + this.stickerMovementCallback = stickerMovementCallback; + notifyDataSetChanged(); } public void setTdlib (Tdlib tdlib) { @@ -107,7 +114,7 @@ public void addItems (ArrayList> items) { @Override public ViewHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { - return ViewHolder.create(context, tdlib, viewType, useDarkMode, this.parent, this.parent, this.parent, this.parent, this.parent, themeProvider); + return ViewHolder.create(context, tdlib, viewType, useDarkMode, this.parent, this.parent, this.parent, this.stickerMovementCallback, this.parent, themeProvider); } @Override @@ -166,6 +173,7 @@ public void onBindViewHolder (ViewHolder holder, int position) { case ViewHolder.TYPE_STICKER: { InlineResult result = items.get(position - 1); ((StickerSmallView) holder.itemView).setSticker(((InlineResultSticker) result).getSticker()); + ((StickerSmallView) holder.itemView).setStickerMovementCallback(stickerMovementCallback); holder.itemView.setTag(result); break; } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/InlineResultsWrap.java b/app/src/main/java/org/thunderdog/challegram/component/chat/InlineResultsWrap.java index 017502199c..c622036f88 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/InlineResultsWrap.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/InlineResultsWrap.java @@ -220,8 +220,18 @@ public void getItemOffsets (Rect outRect, View view, RecyclerView parent, Recycl } }); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + if (scrollCallback != null) { + scrollCallback.onScrollStateChanged(recyclerView, newState); + } + } + @Override public void onScrolled (RecyclerView recyclerView, int dx, int dy) { + if (scrollCallback != null) { + scrollCallback.onScrolled(recyclerView, dx, dy); + } checkLoadMore(); if (lickView != null) { lickView.invalidate(); @@ -251,6 +261,10 @@ public boolean areItemsVisible () { return itemsVisible; } + public RecyclerView getRecyclerView () { + return recyclerView; + } + public void setUseDarkMode (boolean useDarkMode) { adapter.setUseDarkMode(useDarkMode); } @@ -429,6 +443,7 @@ public interface LoadMoreCallback { } private LoadMoreCallback callback; + private RecyclerView.OnScrollListener scrollCallback; private TdlibDelegate delegate; public TdlibDelegate getTdlibDelegate () { @@ -436,8 +451,14 @@ public TdlibDelegate getTdlibDelegate () { } public void showItems (@NonNull TdlibDelegate delegate, @Nullable ArrayList> items, boolean needBackground, @Nullable LoadMoreCallback callback, boolean areHidden) { + showItems(delegate, items, needBackground, callback, null, this, areHidden); + } + + public void showItems (@NonNull TdlibDelegate delegate, @Nullable ArrayList> items, boolean needBackground, @Nullable LoadMoreCallback callback, @Nullable RecyclerView.OnScrollListener scrollCallback, @Nullable StickerSmallView.StickerMovementCallback stickerMovementCallback, boolean areHidden) { this.delegate = delegate; this.adapter.setTdlib(delegate.tdlib()); + this.adapter.setStickerMovementCallback(stickerMovementCallback != null ? stickerMovementCallback : this); + this.scrollCallback = scrollCallback; if (items != null && !items.isEmpty()) { setBackgroundFactor(needBackground ? 1f : 0f, this.visibleFactor != 0f); setItems(items); @@ -450,6 +471,12 @@ public void showItems (@NonNull TdlibDelegate delegate, @Nullable ArrayList> items, @Nullable LoadMoreCallback callback) { + addItems(delegate, items, callback, null, this); + } + + public void addItems (TdlibDelegate delegate, @Nullable ArrayList> items, @Nullable LoadMoreCallback callback, @Nullable RecyclerView.OnScrollListener scrollCallback, @Nullable StickerSmallView.StickerMovementCallback stickerMovementCallback) { + this.adapter.setStickerMovementCallback(stickerMovementCallback != null ? stickerMovementCallback : this); + this.scrollCallback = scrollCallback; if (items != null && !items.isEmpty()) { addItems(delegate, items); } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/InputView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/InputView.java index 038eb06304..8395ae7da9 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/InputView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/InputView.java @@ -14,12 +14,12 @@ */ package org.thunderdog.challegram.component.chat; -import android.app.AlertDialog; import android.content.ClipDescription; import android.content.Context; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.text.Editable; @@ -33,12 +33,10 @@ import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; -import android.text.style.CharacterStyle; import android.text.style.StyleSpan; import android.text.style.URLSpan; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ContextMenu; import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; @@ -51,6 +49,7 @@ import android.view.inputmethod.InputConnection; import android.widget.LinearLayout; +import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; @@ -83,7 +82,9 @@ import org.thunderdog.challegram.emoji.EmojiInfo; import org.thunderdog.challegram.emoji.EmojiSpan; import org.thunderdog.challegram.emoji.EmojiUpdater; +import org.thunderdog.challegram.emoji.PreserveCustomEmojiFilter; import org.thunderdog.challegram.filegen.PhotoGenerationInfo; +import org.thunderdog.challegram.helper.FoundUrls; import org.thunderdog.challegram.helper.InlineSearchContext; import org.thunderdog.challegram.loader.ComplexReceiver; import org.thunderdog.challegram.navigation.LocaleChanger; @@ -92,9 +93,12 @@ import org.thunderdog.challegram.receiver.RefreshRateLimiter; import org.thunderdog.challegram.telegram.RightId; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Fonts; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; @@ -133,13 +137,14 @@ public class InputView extends NoClipEditText implements InlineSearchContext.Cal private Text placeholderTitle; private Text placeholderSubTitle; + private Drawable placeholderIcon; private CharSequence placeholderTitleText; private CharSequence placeholderSubtitleText; - private BoolAnimator showPlaceholder = new BoolAnimator(0, (a, b, c, d) -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); - private BoolAnimator hasSubPlaceholder = new BoolAnimator(0, (a, b, c, d) -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); - private ReplaceAnimator subtitleReplaceAnimator = new ReplaceAnimator<>(a -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); + private final BoolAnimator showPlaceholder = new BoolAnimator(0, (a, b, c, d) -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); + private final BoolAnimator hasSubPlaceholder = new BoolAnimator(0, (a, b, c, d) -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); + private final ReplaceAnimator subtitleReplaceAnimator = new ReplaceAnimator<>(a -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); // TODO: get rid of chat-related logic inside of InputView private @Nullable MessagesController controller; @@ -326,6 +331,11 @@ public void onDestroyActionMode (ActionMode mode) { } } }); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { + ((BaseActivity) getContext()).updateEmojiSuggestionsPosition(false); + }); + } showPlaceholder.setValue(true, false); } @@ -425,9 +435,9 @@ public boolean canClearTextFormat () { TextSelection selection = getTextSelection(); if (selection != null && !selection.isEmpty()) { Editable editable = getText(); - CharacterStyle[] spans = editable.getSpans(selection.start, selection.end, CharacterStyle.class); + Object[] spans = editable.getSpans(selection.start, selection.end, Object.class); if (spans != null) { - for (CharacterStyle span : spans) { + for (Object span : spans) { if (span instanceof NoCopySpan || span instanceof EmojiSpan || isComposingSpan(editable, span) || !TD.canConvertToEntityType(span)) { continue; } @@ -444,10 +454,10 @@ private void clearSpans (int start, int end) { private void clearSpans (int start, int end, @Nullable TdApi.TextEntityType typeForRemove) { Editable editable = getText(); - CharacterStyle[] spans = editable.getSpans(start, end, CharacterStyle.class); + Object[] spans = editable.getSpans(start, end, Object.class); boolean updated = false; if (spans != null) { - for (CharacterStyle existingSpan : spans) { + for (Object existingSpan : spans) { if (existingSpan instanceof NoCopySpan || existingSpan instanceof EmojiSpan || isComposingSpan(editable, existingSpan) || !TD.canConvertToEntityType(existingSpan)) { continue; } @@ -456,7 +466,7 @@ private void clearSpans (int start, int end, @Nullable TdApi.TextEntityType type boolean needContinue = true; TdApi.TextEntityType[] textEntityTypes = TD.toEntityType(existingSpan); if (textEntityTypes != null) { - for (TdApi.TextEntityType textEntityType: textEntityTypes) { + for (TdApi.TextEntityType textEntityType : textEntityTypes) { if (textEntityType.getConstructor() == typeForRemove.getConstructor()) { needContinue = false; break; @@ -503,17 +513,6 @@ private void clearSpans (int start, int end, @Nullable TdApi.TextEntityType type } } - private static boolean canBeNested (TdApi.TextEntityType type) { - switch (type.getConstructor()) { - case TdApi.TextEntityTypePre.CONSTRUCTOR: - case TdApi.TextEntityTypePreCode.CONSTRUCTOR: - case TdApi.TextEntityTypeCode.CONSTRUCTOR: { - return false; - } - } - return true; - } - private static boolean isComposingSpan (Spanned spanned, Object span) { return BitwiseUtils.hasFlag(spanned.getSpanFlags(span), Spanned.SPAN_COMPOSING); } @@ -522,12 +521,12 @@ private boolean setSpanImpl (int start, int end, TdApi.TextEntityType newType) { if (end - start <= 0 || !TD.canConvertToSpan(newType)) { return false; } - CharacterStyle newSpan = TD.toSpan(newType); + Object newSpan = TD.toSpan(newType); Editable editable = getText(); - CharacterStyle[] existingSpansArray = editable.getSpans(start, end, CharacterStyle.class); - List existingSpans = null; + Object[] existingSpansArray = editable.getSpans(start, end, Object.class); + List existingSpans = null; if (existingSpansArray != null && existingSpansArray.length > 0) { - for (CharacterStyle existingSpan : existingSpansArray) { + for (Object existingSpan : existingSpansArray) { if (existingSpan instanceof NoCopySpan || isComposingSpan(editable, existingSpan) || !TD.canConvertToEntityType(existingSpan)) { continue; } @@ -591,8 +590,8 @@ private boolean setSpanImpl (int start, int end, TdApi.TextEntityType newType) { editable.setSpan(newSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return true; } - boolean canBeNested = canBeNested(newType); - for (CharacterStyle existingSpan : existingSpans) { + boolean canBeNested = Td.canBeNested(newType); + for (Object existingSpan : existingSpans) { int existingSpanStart = editable.getSpanStart(existingSpan); int existingSpanEnd = editable.getSpanEnd(existingSpan); TdApi.TextEntityType[] existingTypes = TD.toEntityType(existingSpan); @@ -603,7 +602,7 @@ private boolean setSpanImpl (int start, int end, TdApi.TextEntityType newType) { if (!((EmojiSpan) existingSpan).isCustomEmoji()) { throw new IllegalStateException(); // Unreachable } - if (!canBeNested || newType.getConstructor() == TdApi.TextEntityTypeTextUrl.CONSTRUCTOR) { + if (!canBeNested || Td.isTextUrl(newType)) { editable.removeSpan(existingSpan); if (existingSpan instanceof Destroyable) { ((Destroyable) existingSpan).performDestroy(); @@ -614,7 +613,7 @@ private boolean setSpanImpl (int start, int end, TdApi.TextEntityType newType) { } boolean moveExistingEntity = !canBeNested; for (TdApi.TextEntityType existingType : existingTypes) { - if (!canBeNested(existingType) || (existingType.getConstructor() == TdApi.TextEntityTypeTextUrl.CONSTRUCTOR && newType.getConstructor() == TdApi.TextEntityTypeTextUrl.CONSTRUCTOR)) { + if (!Td.canBeNested(existingType) || (Td.isTextUrl(existingType) && Td.isTextUrl(newType))) { moveExistingEntity = true; } } @@ -814,7 +813,7 @@ private void processTextChange (CharSequence s, boolean byUserAction) { final boolean hasText = s.length() > 0; setAllowsAnyGravity(hasText); - showPlaceholder.setValue(!hasText, !hasText && UI.inUiThread()); + showPlaceholder.setValue(!hasText, !hasText && needAnimateChanges()); } @Override @@ -840,7 +839,7 @@ public void setInputPlaceholder (@StringRes int resId, Object... args) { return; } this.rawPlaceholder = placeholder; - setInputPlaceholder(placeholder, null); + setInputPlaceholder(placeholder, null, 0); /*if (controller == null) { // setHint(placeholder); } else { @@ -850,9 +849,10 @@ public void setInputPlaceholder (@StringRes int resId, Object... args) { }*/ } - public void setInputPlaceholder (CharSequence placeholder, CharSequence placeholderSubtitle) { + public void setInputPlaceholder (CharSequence placeholder, CharSequence placeholderSubtitle, @DrawableRes int iconId) { this.placeholderTitleText = placeholder; this.placeholderSubtitleText = placeholderSubtitle; + this.placeholderIcon = iconId != 0 ? Drawables.get(getResources(), iconId) : null; if (controller != null) { this.lastPlaceholderAvailWidth = 0; checkPlaceholderWidth(); @@ -860,21 +860,27 @@ public void setInputPlaceholder (CharSequence placeholder, CharSequence placehol invalidate(); } + private boolean needAnimateChanges () { + return UI.inUiThread() && boundController.getParentOrSelf().needsTempUpdates() && boundController.getParentOrSelf().isFocused(); + } + public void checkPlaceholderWidth () { - if ((lastPlaceholderRes != 0 || !StringUtils.isEmpty(placeholderTitleText)) && controller != null) { - int availWidth = Math.max(0, getMeasuredWidth() - controller.getHorizontalInputPadding() - getPaddingLeft()); + if ((lastPlaceholderRes != 0 || !StringUtils.isEmpty(placeholderTitleText) || placeholderIcon != null) && controller != null) { + int availWidth = Math.max(0, getMeasuredWidth() - controller.getHorizontalInputPadding() - getPaddingLeft() - Screen.dp(placeholderIcon != null ? 20 : 0)); if (this.lastPlaceholderAvailWidth != availWidth) { this.lastPlaceholderAvailWidth = availWidth; - placeholderTitle = !StringUtils.isEmpty(placeholderTitleText)? new Text.Builder(tdlib, placeholderTitleText, null, availWidth, Paints.robotoStyleProvider(Screen.px(getTextSize())), TextColorSets.PLACEHOLDER, null) - .singleLine().clipTextArea().build(): null; + placeholderTitle = !StringUtils.isEmpty(placeholderTitleText) ? new Text.Builder(tdlib, placeholderTitleText, null, availWidth, Paints.robotoStyleProvider(Screen.px(getTextSize())), TextColorSets.PLACEHOLDER, null) + .singleLine().clipTextArea().build() : null; placeholderSubTitle = !StringUtils.isEmpty(placeholderSubtitleText) ? new Text.Builder(tdlib, placeholderSubtitleText, null, availWidth, Paints.robotoStyleProvider(Screen.px(getTextSize()) / 3f * 2f), TextColorSets.PLACEHOLDER, null) - .singleLine().clipTextArea().build(): null; + .singleLine().clipTextArea().build() : null; + + boolean needAnimateChanges = needAnimateChanges(); - subtitleReplaceAnimator.replace(placeholderSubTitle, UI.inUiThread()); + subtitleReplaceAnimator.replace(placeholderSubTitle, needAnimateChanges); - hasSubPlaceholder.setValue(placeholderSubTitle != null, UI.inUiThread()); + hasSubPlaceholder.setValue(placeholderSubTitle != null, needAnimateChanges); if (rawPlaceholderWidth <= availWidth) { //setHint(rawPlaceholder); @@ -914,20 +920,22 @@ public long provideInlineSearchChatId () { return controller != null ? controller.getChatId() : inputListener != null && inputListener.canSearchInline(this) ? inputListener.provideInlineSearchChatId(this) : 0; } - @Override - public TdApi.WebPage provideExistingWebPage (TdApi.FormattedText currentText) { - return controller != null ? controller.getEditingWebPage(currentText) : null; - } - private boolean isCaptionEditing () { return controller == null || controller.isEditingCaption(); } @Override - public boolean needsLinkPreview () { + public boolean enableLinkPreview () { return !isCaptionEditing() && tdlib.canAddWebPagePreviews(controller.getChat()); } + @Override + public void showLinkPreview (@Nullable FoundUrls foundUrls) { + if (controller != null) { + controller.showLinkPreview(foundUrls); + } + } + @Override public boolean needsInlineBots () { return !isCaptionEditing() && tdlib.canSendMessage(controller.getChat(), RightId.SEND_OTHER_MESSAGES); @@ -961,9 +969,13 @@ public void updateInlineMode (boolean isInInlineMode, boolean isInProgress) { } @Override - public void showInlineStickers (ArrayList stickers, boolean isMore) { + public void showInlineStickers (ArrayList stickers, String foundByEmoji, boolean isEmoji, boolean isMore) { if (controller != null) { - controller.showStickerSuggestions(stickers, isMore); + if (!isEmoji) { + controller.showStickerSuggestions(stickers, isMore); + } else { + controller.showEmojiSuggestions(stickers, foundByEmoji, isMore); + } } } @@ -993,7 +1005,7 @@ public boolean isDisplayingItems () { @Override public void hideInlineResults () { if (controller != null) { - controller.hideStickerSuggestions(); + controller.onHideEmojiAndStickerSuggestionsFinally(); } if (inputListener != null && inputListener.canSearchInline(this)) { inputListener.showInlineResults(this, null, false); @@ -1002,53 +1014,6 @@ public void hideInlineResults () { } } - @Override - public boolean showLinkPreview (@Nullable String link, @Nullable TdApi.WebPage webPage) { - if (controller == null) { - return false; - } - if (ignoreFirstLinkPreview) { - ignoreFirstLinkPreview = false; - controller.ignoreLinkPreview(link, webPage); - return false; - } else { - controller.showLinkPreview(link, webPage); - return true; - } - } - - private AlertDialog linkWarningDialog; - - @Override - public int showLinkPreviewWarning (final int contextId, @Nullable final String link) { - if (controller == null || !controller.isSecretChat()) { - return InlineSearchContext.WARNING_OK; - } - if (Settings.instance().needTutorial(Settings.TUTORIAL_SECRET_LINK_PREVIEWS)) { - if (linkWarningDialog == null || !linkWarningDialog.isShowing()) { - AlertDialog.Builder b = new AlertDialog.Builder(controller.context(), Theme.dialogTheme()); - b.setTitle(Lang.getString(R.string.AppName)); - b.setMessage(Lang.getString(R.string.SecretLinkPreviewAlert)); - b.setPositiveButton(Lang.getString(R.string.SecretLinkPreviewEnable), (dialog, which) -> { - linkWarningDialog = null; - Settings.instance().markTutorialAsComplete(Settings.TUTORIAL_SECRET_LINK_PREVIEWS); - Settings.instance().setUseSecretLinkPreviews(true); - inlineContext.forceCheck(); - }); - b.setNegativeButton(Lang.getString(R.string.SecretLinkPreviewDisable), (dialog, which) -> { - linkWarningDialog = null; - Settings.instance().markTutorialAsComplete(Settings.TUTORIAL_SECRET_LINK_PREVIEWS); - Settings.instance().setUseSecretLinkPreviews(false); - inlineContext.forceCheck(); - }); - b.setCancelable(false); - linkWarningDialog = controller.showAlert(b); - } - return InlineSearchContext.WARNING_CONFIRM; - } - return Settings.instance().needSecretLinkPreviews() ? InlineSearchContext.WARNING_OK : InlineSearchContext.WARNING_BLOCK; - } - public void setIsInEditMessageMode (boolean isInEditMessageMode, String futureText) { this.inlineContext.setDisallowInlineResults(isInEditMessageMode, getText().toString().equals(futureText)); } @@ -1088,7 +1053,7 @@ public int provideCurrentStringResource () { public void onEmojiSelected (String emoji) { TextSelection selection = getTextSelection(); - if (selection == null) + if (selection == null || !isEnabled()) return; int after = selection.start + emoji.length(); SpannableString s = new SpannableString(emoji); @@ -1101,7 +1066,59 @@ public void onEmojiSelected (String emoji) { setSelection(after); } - private boolean textChangedSinceChatOpened, ignoreFirstLinkPreview; + public void onCustomEmojiSelected (TGStickerObj stickerObj) { + onCustomEmojiSelected(stickerObj, false); + } + + public void onCustomEmojiSelected (TdApi.Sticker sticker) { + onCustomEmojiSelected(sticker, false); + } + + public void onCustomEmojiSelected (TGStickerObj stickerObj, boolean needReplace) { + onCustomEmojiSelected(stickerObj.getSticker(), needReplace); + } + + public void onCustomEmojiSelected (TdApi.Sticker stickerObj, boolean needReplace) { + TextSelection selection = getTextSelection(); + if (selection == null || !isEnabled()) + return; + + final String emoji = TD.stickerEmoji(stickerObj); + final Editable editable = getText(); + final EmojiSpan oldEmojiSpan = needReplace ? Emoji.findPrecedingEmojiSpan(editable, selection.start) : null; + + final int start = oldEmojiSpan != null ? editable.getSpanStart(oldEmojiSpan) : selection.start; + final int end = oldEmojiSpan != null ? editable.getSpanEnd(oldEmojiSpan) : selection.end; + + if (oldEmojiSpan != null) { + editable.removeSpan(oldEmojiSpan); + if (oldEmojiSpan instanceof Destroyable) { + ((Destroyable) oldEmojiSpan).performDestroy(); + } + } + + if (oldEmojiSpan != null && needReplace && Config.KEEP_ORIGINAL_EMOJI_WHEN_INPUT_CUSTOM_EMOJI) { + editable.setSpan(Emoji.instance().newCustomSpan(emoji, null, this, tdlib, Td.customEmojiId(stickerObj)), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + setSelection(start + emoji.length()); + if (inlineContext != null) { + inlineContext.reset(); + } + return; + } + + SpannableString s = new SpannableString(emoji); + s.setSpan(Emoji.instance().newCustomSpan(emoji, null, this, tdlib, + Td.customEmojiId(stickerObj)), 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + if (needReplace || start != end) { + editable.replace(start, end, s); + } else { + editable.insert(start, s); + } + setSelection(start + s.length()); + } + + private boolean textChangedSinceChatOpened; public void setChat (TdApi.Chat chat, @Nullable ThreadInfo messageThread, @Nullable String customInputField, boolean isSilent) { textChangedSinceChatOpened = false; @@ -1132,10 +1149,15 @@ public void updateMessageHint (TdApi.Chat chat, @Nullable ThreadInfo messageThre return; } int resource; + int icon = 0; CharSequence subplaceholder = null; Object[] args = null; TdApi.ChatMemberStatus status = tdlib.chatStatus(chat.id); - if (tdlib.isChannel(chat.id)) { + + if (!tdlib.canSendBasicMessage(chat)) { + resource = R.string.MessageInputTextDisabled; + icon = R.drawable.baseline_block_18; + } else if (tdlib.isChannel(chat.id)) { resource = isSilent ? R.string.ChannelSilentBroadcast : R.string.ChannelBroadcast; } /*else if (tdlib.isMultiChat(chat) && Td.isAnonymous(status)) { resource = messageThread != null ? (messageThread.areComments() ? R.string.CommentAnonymously : R.string.MessageReplyAnonymously) : R.string.MessageAnonymously; @@ -1156,7 +1178,7 @@ public void updateMessageHint (TdApi.Chat chat, @Nullable ThreadInfo messageThre } else { text = customInputField; } - setInputPlaceholder(text, subplaceholder); + setInputPlaceholder(text, subplaceholder, icon); } public void setDraft (@Nullable TdApi.InputMessageContent draftContent) { @@ -1164,10 +1186,8 @@ public void setDraft (@Nullable TdApi.InputMessageContent draftContent) { if (draftContent != null && draftContent.getConstructor() == TdApi.InputMessageText.CONSTRUCTOR) { TdApi.InputMessageText textDraft = (TdApi.InputMessageText) draftContent; draft = TD.toCharSequence(textDraft.text); - ignoreFirstLinkPreview = textDraft.disableWebPagePreview; } else { draft = ""; - ignoreFirstLinkPreview = false; } String current = getInput().trim(); controller.setInputVisible(true, current.length() > 0); @@ -1246,6 +1266,7 @@ private void drawEmojiOverlay (Canvas c) { @Override protected void onDraw (Canvas c) { + final int x = getPaddingLeft() + Screen.dp(placeholderIcon != null ? 20 : 0); final float alpha = showPlaceholder.getFloatValue(); final int offset = (int) (hasSubPlaceholder.getFloatValue() * (getTextSize() / 18 * 8)); final int baseline = getBaseline(); @@ -1255,15 +1276,16 @@ protected void onDraw (Canvas c) { final int titleHeight = placeholderTitle.getHeight(); final int titleBaseline = (int)(titleHeight * 0.75f); final int y = baseline - titleBaseline - offset; - placeholderTitle.draw(c, getPaddingLeft(), y, null, alpha); - //c.drawRect(getPaddingLeft(), y, getPaddingLeft() + placeholderTitle.getWidth(), y + placeholderTitle.getHeight(), Paints.strokeSmallPaint(Color.GREEN)); - //c.drawRect(getPaddingLeft(), y, getPaddingLeft() + placeholderTitle.getWidth(), y + placeholderTitle.getLineHeight(), Paints.strokeSmallPaint(Color.GREEN)); + placeholderTitle.draw(c, x, y, null, alpha); } for (ListAnimator.Entry entry : subtitleReplaceAnimator) { final int offset2 = (int) ((!entry.isAffectingList() ? ((entry.getVisibility() - 1f) * (getTextSize() / 18f * 14f)): ((1f - entry.getVisibility()) * (getTextSize() / 18f * 14f)))); - entry.item.draw(c, getPaddingLeft(), baseline - offset / 2 + offset2, null, Math.min(alpha, entry.getVisibility())); + entry.item.draw(c, x, baseline - offset / 2 + offset2, null, Math.min(alpha, entry.getVisibility())); + } + if (placeholderIcon != null) { + Drawables.draw(c, placeholderIcon, getPaddingLeft(), (getMeasuredHeight() - placeholderIcon.getMinimumHeight()) / 2f, PorterDuffPaint.get(ColorId.iconLight) /*Paints.getPorterDuffPaint(ColorId.textPlaceholder)*/); } } @@ -1273,11 +1295,9 @@ protected void onDraw (Canvas c) { String text = getText().toString(); if (text.equalsIgnoreCase(prefix)) { checkPrefix(text); - c.drawText(displaySuffix, getPaddingLeft() + prefixWidth, getBaseline(), paint); + c.drawText(displaySuffix, x + prefixWidth, getBaseline(), paint); } } - // c.drawRect(0, baseline, getMeasuredWidth(), baseline, Paints.strokeSmallPaint(Color.RED)); - // c.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), Paints.strokeBigPaint(Color.RED)); } // Inline query @@ -1390,9 +1410,15 @@ protected InputConnection createInputConnection (EditorInfo editorInfo) { return null; final InputConnectionCompat.OnCommitContentListener callback = (inputContentInfo, flags, bundle) -> { - if (controller == null || !controller.hasWritePermission()) + if (controller == null) return false; + final long chatId = controller.getChatId(); + final TdApi.Chat chat = tdlib.chat(chatId); + if (chat == null) { + return false; + } + ClipDescription description = inputContentInfo.getDescription(); @MediaType int mediaType; if (description.hasMimeType("image/webp")) { @@ -1419,9 +1445,8 @@ protected InputConnection createInputConnection (EditorInfo editorInfo) { } Uri uri = inputContentInfo.getContentUri(); long timestamp = System.currentTimeMillis(); - long chatId = controller.getChatId(); long messageThreadId = controller.getMessageThreadId(); - long replyToMessageId = controller.obtainReplyId(); + TdApi.InputMessageReplyTo replyTo = controller.obtainReplyTo(); boolean silent = controller.obtainSilentMode(); boolean needMenu = controller.areScheduledOnly(); @@ -1461,22 +1486,26 @@ protected InputConnection createInputConnection (EditorInfo editorInfo) { content = tdlib.filegen().createThumbnail(new TdApi.InputMessageSticker(generated, null, imageWidth, imageHeight, null), isSecretChat); } else { TdApi.InputFileGenerated generated = PhotoGenerationInfo.newFile(path, 0, timestamp, false, 0); - content = tdlib.filegen().createThumbnail(new TdApi.InputMessagePhoto(generated, null, null, imageWidth, imageHeight, null, 0, false), isSecretChat); + content = tdlib.filegen().createThumbnail(new TdApi.InputMessagePhoto(generated, null, null, imageWidth, imageHeight, null, null, false), isSecretChat); } - if (needMenu) { - tdlib.ui().post(() -> { + + UI.post(() -> { + if (controller.showRestriction(this, tdlib.getRestrictionText(chat, content))) { + return; + } + if (needMenu) { tdlib.ui().showScheduleOptions(controller, chatId, false, (sendOptions, disableMarkdown) -> - tdlib.sendMessage(chatId, messageThreadId, replyToMessageId, + tdlib.sendMessage(chatId, messageThreadId, replyTo, Td.newSendOptions(sendOptions, silent), content, null ), null, null); - }); - } else { - tdlib.sendMessage(chatId, messageThreadId, replyToMessageId, Td.newSendOptions(silent), content); - } + } else { + tdlib.sendMessage(chatId, messageThreadId, replyTo, Td.newSendOptions(silent), content); + } + }); }); // read and display inputContentInfo asynchronously. // call inputContentInfo.releasePermission() as needed. @@ -1505,6 +1534,7 @@ public void onFinalNewLineBeingRemoved (FinalNewLineFilter filter) { public void setMaxCodePointCount (int maxCodePointCount) { if (maxCodePointCount > 0) { setFilters(new InputFilter[] { + new PreserveCustomEmojiFilter(), new ExternalEmojiFilter(), new CodePointCountFilter(maxCodePointCount), new EmojiFilter(this), @@ -1513,6 +1543,7 @@ public void setMaxCodePointCount (int maxCodePointCount) { }); } else { setFilters(new InputFilter[] { + new PreserveCustomEmojiFilter(), new ExternalEmojiFilter(), new EmojiFilter(this), new CharacterStyleFilter(true), @@ -1540,12 +1571,16 @@ public final void setNoPersonalizedLearning (boolean noPersonalizedLearning) { public final TdApi.FormattedText getOutputText (boolean applyMarkdown) { SpannableStringBuilder text = new SpannableStringBuilder(getText()); BaseInputConnection.removeComposingSpans(text); - TdApi.FormattedText result = new TdApi.FormattedText(text.toString(), TD.toEntities(text, false)); + TdApi.FormattedText formattedText = new TdApi.FormattedText(text.toString(), TD.toEntities(text, false)); if (applyMarkdown) { //noinspection UnsafeOptInUsageError - Td.parseMarkdown(result); + Td.parseMarkdown(formattedText); } - return result; + return formattedText; + } + + public final boolean hasOnlyPremiumFeatures () { + return TD.hasCustomEmoji(getOutputText(false)); } // Android-related workarounds @@ -1594,26 +1629,14 @@ public boolean onTextContextMenuItem (@IdRes int id) { } public void paste (TdApi.FormattedText pasteText, boolean needSelectPastedText) { - TextSelection selection = getTextSelection(); - if (selection == null) return; - - final int start = selection.start; - paste(selection, pasteText.text, needSelectPastedText); - if (pasteText.entities != null && pasteText.entities.length > 0) { - for (TdApi.TextEntity entity: pasteText.entities) { - setSpan(start + entity.offset, start + entity.offset + entity.length, entity.type); - } - if (pasteText.text != null) { - setSelection(start, pasteText.text.length()); - } - } + paste(TD.toCharSequence(pasteText), needSelectPastedText); } public void paste (CharSequence pasteText, boolean needSelectPastedText) { paste(getTextSelection(), pasteText, needSelectPastedText); } - public void paste (TextSelection selection, CharSequence pasteText, boolean needSelectPastedText) { + private void paste (TextSelection selection, CharSequence pasteText, boolean needSelectPastedText) { if (selection == null) return; final int start = selection.start; final int end = selection.end; @@ -1708,4 +1731,36 @@ public void setVisibility (int visibility) { doBugfix(); } } + + private final int[] + cords1 = new int[2], + cords2 = new int[2], + cords3 = new int[2]; + + public void getSymbolUnderCursorPosition (int[] coordinates) { + TextSelection selection = getTextSelection(); + if (selection == null) { + coordinates[0] = coordinates[1] = 0; + return; + } + + Views.getCharacterCoordinates(this, selection.start, cords1); + cords2[0] = cords1[0]; + cords2[1] = cords1[1]; + int[] cords2 = this.cords2; + + for (int a = selection.start - 1; a >= 0; a--) { + Views.getCharacterCoordinates(this, a, cords3); + if (cords3[1] != cords1[1]) { + cords2[0] /= 2; + break; + } + if (cords3[0] == cords1[0]) continue; + cords2 = cords3; + break; + } + + coordinates[0] = (cords1[0] + cords2[0]) / 2; + coordinates[1] = cords1[1]; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/JoinRequestsView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/JoinRequestsView.java index 48d7c53956..08f7fb133e 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/JoinRequestsView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/JoinRequestsView.java @@ -26,10 +26,8 @@ import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; import org.thunderdog.challegram.core.Lang; -import org.thunderdog.challegram.data.AvatarPlaceholder; +import org.thunderdog.challegram.loader.AvatarReceiver; import org.thunderdog.challegram.loader.ComplexReceiver; -import org.thunderdog.challegram.loader.ImageFile; -import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.Theme; @@ -143,9 +141,7 @@ private void setRequestInfo (long[] userIds, boolean animated) { for (long userId : userIds) { UserEntry ue = new UserEntry(tdlib, userId); entries.add(ue); - if (ue.avatarFile != null) { - megaReceiver.getImageReceiver(userId).requestFile(ue.avatarFile); - } + megaReceiver.getAvatarReceiver(userId).requestUser(tdlib, ue.userId, AvatarReceiver.Options.NONE); } if (this.joinRequestEntries == null) this.joinRequestEntries = new ListAnimator<>(new SingleViewProvider(this)); @@ -203,17 +199,10 @@ public void onClickAt (View view, float x, float y) { private static class UserEntry { private final long userId; - private final ImageFile avatarFile; - private final AvatarPlaceholder avatarPlaceholder; private final Paint clearPaint = new Paint(Paint.ANTI_ALIAS_FLAG); public UserEntry (Tdlib tdlib, long userId) { this.userId = userId; - this.avatarFile = tdlib.cache().userAvatar(userId); - if (this.avatarFile != null) { - this.avatarFile.setSize(Screen.dp(AVATAR_RADIUS) * 2); - } - this.avatarPlaceholder = tdlib.cache().userPlaceholder(userId, false, AVATAR_RADIUS, null); clearPaint.setColor(0); clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); @@ -241,7 +230,7 @@ public void draw (Canvas c, ComplexReceiver complexReceiver, float cx, float cy, if (alpha == 0f) return; - ImageReceiver receiver = avatarFile != null ? complexReceiver.getImageReceiver(userId) : null; + AvatarReceiver receiver = complexReceiver.getAvatarReceiver(userId); int radius = Screen.dp(AVATAR_RADIUS); //c.drawRect(cx - radius, cy - radius, cx + radius, cy + radius, Paints.fillingPaint(Theme.getColor(ColorId.textNegative))); @@ -256,21 +245,17 @@ public void draw (Canvas c, ComplexReceiver complexReceiver, float cx, float cy, restoreToCount = -1; } - c.drawCircle(cx, cy, radius + Screen.dp(AVATAR_OUTLINE) * alpha * .5f, clearPaint); - - if (receiver != null) { - if (alpha != 1f) - receiver.setPaintAlpha(receiver.getPaintAlpha() * alpha); - receiver.setBounds((int) (cx - radius), (int) (cy - radius), (int) (cx + radius), (int) (cy + radius)); - if (receiver.needPlaceholder()) - receiver.drawPlaceholderRounded(c, radius, ColorUtils.alphaColor(alpha, Theme.placeholderColor())); - receiver.setRadius(radius); - receiver.draw(c); - if (alpha != 1f) - receiver.restorePaintAlpha(); - } else if (avatarPlaceholder != null) { - avatarPlaceholder.draw(c, cx, cy, alpha); - } + if (alpha != 1f) + receiver.setPaintAlpha(receiver.getPaintAlpha() * alpha); + receiver.setBounds((int) (cx - radius), (int) (cy - radius), (int) (cx + radius), (int) (cy + radius)); + + receiver.drawPlaceholderRounded(c, radius, Screen.dp(AVATAR_OUTLINE) * alpha * .5f, clearPaint); + + if (receiver.needPlaceholder()) + receiver.drawPlaceholderRounded(c, radius, ColorUtils.alphaColor(alpha, Theme.placeholderColor())); + receiver.draw(c); + if (alpha != 1f) + receiver.restorePaintAlpha(); if (needRestore) { Views.restore(c, restoreToCount); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MediaPreview.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MediaPreview.java index 53e94dc4b7..5cf768383e 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MediaPreview.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MediaPreview.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.data.ContentPreview; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.loader.ComplexReceiver; import org.thunderdog.challegram.telegram.Tdlib; @@ -133,7 +134,33 @@ public static MediaPreview valueOf (Tdlib tdlib, File file, String mimeType, int } } - public static MediaPreview valueOf (Tdlib tdlib, TdApi.Message message, @Nullable TD.ContentPreview preview, int size, int cornerRadius) { + public static boolean hasMedia (TdApi.WebPage webPage) { + if (webPage.video != null) { + TdApi.Video video = webPage.video; + // video preview + return video.thumbnail != null || video.minithumbnail != null; + } else if (webPage.sticker != null) { + // sticker preview + return true; + } else if (webPage.animation != null) { + // gif preview + return webPage.animation.thumbnail != null || webPage.animation.minithumbnail != null; + } else if (webPage.videoNote != null) { + return true; + } else if (webPage.voiceNote != null) { + // TODO voice note preview? + } else if (webPage.document != null) { + TdApi.Document document = webPage.document; + // doc preview + return document.minithumbnail != null || document.thumbnail != null; + } else if (webPage.photo != null) { + TdApi.PhotoSize thumbnail = Td.findSmallest(webPage.photo); + return thumbnail != null || webPage.photo.minithumbnail != null; + } + return false; + } + + public static MediaPreview valueOf (Tdlib tdlib, TdApi.Message message, @Nullable ContentPreview preview, int size, int cornerRadius) { Tdlib.Album album = preview != null ? preview.getAlbum() : null; if (album != null) { // TODO album preview? @@ -143,6 +170,7 @@ public static MediaPreview valueOf (Tdlib tdlib, TdApi.Message message, @Nullabl } } + //noinspection SwitchIntDdef switch (message.content.getConstructor()) { case TdApi.MessageText.CONSTRUCTOR: { TdApi.WebPage webPage = ((TdApi.MessageText) message.content).webPage; @@ -242,6 +270,28 @@ public static MediaPreview valueOf (Tdlib tdlib, TdApi.Message message, @Nullabl TdApi.VoiceNote voiceNote = ((TdApi.MessageVoiceNote) message.content).voiceNote; break; } + case TdApi.MessageGiftedPremium.CONSTRUCTOR: { + TdApi.Sticker sticker = ((TdApi.MessageGiftedPremium) message.content).sticker; + if (sticker != null) + return new MediaPreviewSimple(tdlib, size, cornerRadius, sticker); + break; + } + case TdApi.MessagePremiumGiftCode.CONSTRUCTOR: { + TdApi.Sticker sticker = ((TdApi.MessagePremiumGiftCode) message.content).sticker; + if (sticker != null) + return new MediaPreviewSimple(tdlib, size, cornerRadius, sticker); + break; + } + case TdApi.MessagePremiumGiveaway.CONSTRUCTOR: { + TdApi.Sticker sticker = ((TdApi.MessagePremiumGiveaway) message.content).sticker; + if (sticker != null) + return new MediaPreviewSimple(tdlib, size, cornerRadius, sticker); + break; + } + default: { + Td.assertMessageContent_d40af239(); + break; + } } return null; } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MediaPreviewSimple.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MediaPreviewSimple.java index e74543f60f..7501d30ee0 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MediaPreviewSimple.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MediaPreviewSimple.java @@ -156,7 +156,7 @@ public MediaPreviewSimple (Tdlib tdlib, int size, int cornerRadius, TdApi.Locati previewReceiver.drawPlaceholder(c); if (needPinIcon) { Drawable drawable = view.getSparseDrawable(R.drawable.baseline_location_on_24, 0); - Drawables.draw(c, drawable, receiver.centerX() - drawable.getMinimumWidth() / 2, receiver.centerY() - drawable.getMinimumHeight() / 2, Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, drawable, receiver.centerX() - drawable.getMinimumWidth() / 2, receiver.centerY() - drawable.getMinimumHeight() / 2, Paints.whitePorterDuffPaint()); } } previewReceiver.draw(c); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagePreviewView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagePreviewView.java index 7e57b9b89c..e85cea4ed0 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagePreviewView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagePreviewView.java @@ -14,15 +14,21 @@ import android.content.Context; import android.graphics.Canvas; +import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.view.MotionEvent; +import android.view.View; import android.view.ViewGroup; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; -import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.ContentPreview; +import org.thunderdog.challegram.helper.LinkPreview; import org.thunderdog.challegram.loader.ComplexReceiver; import org.thunderdog.challegram.receiver.RefreshRateLimiter; import org.thunderdog.challegram.support.RippleSupport; @@ -30,12 +36,18 @@ import org.thunderdog.challegram.telegram.MessageListener; import org.thunderdog.challegram.telegram.TGLegacyManager; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibCache; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; +import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.ui.ListItem; import org.thunderdog.challegram.ui.SettingHolder; import org.thunderdog.challegram.util.text.Text; @@ -43,26 +55,46 @@ import org.thunderdog.challegram.widget.AttachDelegate; import org.thunderdog.challegram.widget.BaseView; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.BoolAnimator; import me.vkryl.android.animator.ListAnimator; import me.vkryl.android.animator.ReplaceAnimator; +import me.vkryl.android.util.ClickHelper; import me.vkryl.android.util.SingleViewProvider; import me.vkryl.android.util.ViewProvider; import me.vkryl.core.ArrayUtils; +import me.vkryl.core.BitwiseUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.lambda.Destroyable; +import me.vkryl.core.lambda.RunnableData; import me.vkryl.td.MessageId; import me.vkryl.td.Td; -public class MessagePreviewView extends BaseView implements AttachDelegate, Destroyable, ChatListener, MessageListener, TdlibCache.UserDataChangeListener, TGLegacyManager.EmojiLoadListener { +public class MessagePreviewView extends BaseView implements AttachDelegate, Destroyable, ChatListener, MessageListener, TdlibCache.UserDataChangeListener, TGLegacyManager.EmojiLoadListener, TdlibUi.MessageProvider, RunnableData { private static class TextEntry extends ListAnimator.MeasurableEntry implements Destroyable { - public Drawable drawable; + public final Drawable drawable; public ComplexReceiver receiver; + public TdlibAccentColor accentColor; - public TextEntry (Text text, Drawable drawable, ComplexReceiver receiver) { + public TextEntry (Text text, Drawable drawable, ComplexReceiver receiver, TdlibAccentColor accentColor) { super(text); this.drawable = drawable; this.receiver = receiver; + this.accentColor = accentColor; + } + + public @PorterDuffColorId int getNameColorId (@PorterDuffColorId int fallbackColorId) { + if (accentColor != null) { + long complexColor = accentColor.getNameComplexColor(); + if (Theme.isColorId(complexColor)) { + return Theme.extractColorValue(complexColor); + } + } + return fallbackColorId; } @Override @@ -100,53 +132,210 @@ public MessagePreviewView (Context context, Tdlib tdlib) { private final ViewProvider viewProvider = new SingleViewProvider(this); - private TdApi.Message message; - private String forcedTitle; - private boolean ignoreAlbumRefreshers, useAvatarFallback; + private static class DisplayData implements Destroyable { + public final Tdlib tdlib; + public final TdApi.Message message; + public final @Nullable TdApi.InputTextQuote quote; + public final @Options int options; + public @Nullable TdApi.SearchMessagesFilter filter; + public @Nullable String forcedTitle; + public boolean messageDeleted; + public @Nullable LinkPreview linkPreview; + public @Nullable View.OnClickListener onMediaClickListener; + + public DisplayData (Tdlib tdlib, TdApi.Message message, @Nullable TdApi.InputTextQuote quote, int options) { + this.tdlib = tdlib; + this.message = message; + this.quote = quote; + this.options = options; + } - private TD.ContentPreview contentPreview; + public boolean setForcedTitle (String forcedTitle) { + if (!StringUtils.equalsOrBothEmpty(this.forcedTitle, forcedTitle)) { + this.forcedTitle = forcedTitle; + return true; + } + return false; + } - public void setMessage (@Nullable TdApi.Message message, @Nullable TdApi.SearchMessagesFilter filter, @Nullable String forcedTitle, boolean ignoreAlbumRefreshers) { - this.ignoreAlbumRefreshers = ignoreAlbumRefreshers; - if (this.message == message) { - setForcedTitle(forcedTitle); - return; + public void setOnMediaClickListener (View.OnClickListener onClickListener) { + this.onMediaClickListener = onClickListener; + } + + public void setPreviewFilter (@Nullable TdApi.SearchMessagesFilter filter) { + this.filter = filter; + } + + public void setLinkPreview (@Nullable LinkPreview linkPreview) { + this.linkPreview = linkPreview; + } + + public boolean isLinkPreviewShowSmallMedia () { + return linkPreview != null && linkPreview.hasMedia() && !linkPreview.getOutputShowLargeMedia(); + } + + public boolean equalsTo (TdApi.Message message, @Nullable TdApi.InputTextQuote quote, @Options int options, @Nullable LinkPreview linkPreview) { + return this.message == message && Td.equalsTo(this.quote, quote) && this.options == options && this.linkPreview == linkPreview; + } + + public TdlibAccentColor accentColor () { + if (StringUtils.isEmpty(forcedTitle)) { + // TODO handle fake messages + return tdlib.messageAccentColor(message); + } + return null; + } + + public boolean needBoldTitle () { + if (linkPreview != null && linkPreview.isNotFound()) { + return false; + } + return !StringUtils.isEmpty(forcedTitle) || Td.getMessageAuthorId(message, true) != 0; } - if (this.message != null) { - unsubscribeFromUpdates(this.message); + + public boolean relatedToChat (long chatId) { + return this.message.senderId.getConstructor() == TdApi.MessageSenderChat.CONSTRUCTOR && + ((TdApi.MessageSenderChat) this.message.senderId).chatId == chatId; + } + + public boolean relatedToUser (long userId) { + return this.message.senderId.getConstructor() == TdApi.MessageSenderUser.CONSTRUCTOR && + ((TdApi.MessageSenderUser) this.message.senderId).userId == userId; } - this.message = message; - this.forcedTitle = forcedTitle; + + public boolean updateMessageContent (long chatId, long messageId, TdApi.MessageContent newContent) { + if (this.message.chatId == chatId && this.message.id == messageId) { + this.message.content = newContent; + return true; + } + return false; + } + + @Override + public void performDestroy () { + setLinkPreview(null); + } + } + + private @Nullable DisplayData data; + private boolean useAvatarFallback; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + Options.NONE, + Options.IGNORE_ALBUM_REFRESHERS, + Options.DISABLE_MESSAGE_PREVIEW, + Options.NO_UPDATES, + Options.HANDLE_MEDIA_CLICKS + }, flag = true) + public @interface Options { + int + NONE = 0, + IGNORE_ALBUM_REFRESHERS = 1, + DISABLE_MESSAGE_PREVIEW = 1 << 1, + NO_UPDATES = 1 << 2, + HANDLE_MEDIA_CLICKS = 1 << 3; + } + + private ContentPreview contentPreview; + + public void setMessage (@Nullable TdApi.Message message, @Nullable TdApi.SearchMessagesFilter filter, @Nullable String forcedTitle, @Options int options) { + setMessage(message, null, filter, forcedTitle, options); + } + + public void setMessage (@Nullable TdApi.Message message, @Nullable TdApi.InputTextQuote quote, @Nullable TdApi.SearchMessagesFilter filter, @Nullable String forcedTitle, @Options int options) { if (message != null) { - subscribeToUpdates(message); + DisplayData displayData = new DisplayData(tdlib, message, quote, options); + displayData.setPreviewFilter(filter); + displayData.setForcedTitle(forcedTitle); + setDisplayData(displayData); + } else { + setDisplayData(null); + } + } + + public interface LinkPreviewMediaClickListener { + void onClick (MessagePreviewView view, @NonNull LinkPreview linkPreview); + } + + public void setLinkPreview (@Nullable LinkPreview linkPreview, @Nullable LinkPreviewMediaClickListener onMediaClick) { + if (linkPreview != null) { + DisplayData displayData = new DisplayData(tdlib, linkPreview.getFakeMessage(), null, Options.DISABLE_MESSAGE_PREVIEW | Options.NO_UPDATES | (onMediaClick != null ? Options.HANDLE_MEDIA_CLICKS : 0)); + displayData.setForcedTitle(linkPreview.getForcedTitle()); + if (onMediaClick != null) { + displayData.setOnMediaClickListener(v -> { + onMediaClick.onClick(this, linkPreview); + }); + } + displayData.setLinkPreview(linkPreview); + setDisplayData(displayData); + } else { + setDisplayData(null); + } + } + + @Override + public void runWithData (LinkPreview arg) { + if (this.data != null && this.data.linkPreview == arg) { + data.setForcedTitle(arg.getForcedTitle()); + updateTitleText(); buildPreview(); - setPreviewChatId(null, message.chatId, null, new MessageId(message.chatId, message.id), filter); + } + } + + private void setDisplayData (DisplayData data) { + if (this.data == null && data == null) { + return; + } + if (this.data != null && data != null && this.data.equalsTo(data.message, data.quote, data.options, data.linkPreview)) { + if (data.setForcedTitle(data.forcedTitle)) { + updateTitleText(); + } + return; + } + if (this.data != null) { + if (!BitwiseUtils.hasFlag(this.data.options, Options.NO_UPDATES)) { + unsubscribeFromUpdates(this.data.message); + } + if (this.data.linkPreview != null) { + this.data.linkPreview.removeReference(this); + } + this.data.performDestroy(); + } + this.data = data; + if (data != null) { + if (!BitwiseUtils.hasFlag(this.data.options, Options.NO_UPDATES)) { + subscribeToUpdates(data.message); + } + if (data.linkPreview != null && data.linkPreview.isLoading()) { + data.linkPreview.addReference(this); + } + buildPreview(); + if (!BitwiseUtils.hasFlag(data.options, Options.DISABLE_MESSAGE_PREVIEW)) { + setPreviewChatId(null, data.message.chatId, null, new MessageId(data.message), data.filter); + } else { + clearPreviewChat(); + } } else { - clearPreviewChat(); this.contentPreview = null; this.mediaPreview.replace(null, false); this.mediaPreview.measure(false); + clearPreviewChat(); } invalidate(); } - public void setForcedTitle (@Nullable String forcedTitle) { - if (!StringUtils.equalsOrBothEmpty(this.forcedTitle, forcedTitle)) { - this.forcedTitle = forcedTitle; - updateTitleText(); - } - } - public void setUseAvatarFallback (boolean useAvatarFallback) { this.useAvatarFallback = useAvatarFallback; } - private final ReplaceAnimator titleText = new ReplaceAnimator<>(ignored -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); + private final ReplaceAnimator titleText = new ReplaceAnimator<>(ignored -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); private final ReplaceAnimator contentText = new ReplaceAnimator<>(ignored -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); private final ReplaceAnimator mediaPreview = new ReplaceAnimator<>(ignored -> { buildText(true); invalidate(); }, AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); + private final BoolAnimator showSmallMedia = new BoolAnimator(this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); private static final float LINE_PADDING = 6f; private static final float PADDING_SIZE = 8f; @@ -167,8 +356,15 @@ public void setLinePadding (float customLinePadding) { } private void buildPreview () { - this.contentPreview = TD.getChatListPreview(tdlib, message.chatId, message); - if (contentPreview.hasRefresher() && !(ignoreAlbumRefreshers && contentPreview.isMediaGroup())) { + if (data == null) { + throw new IllegalStateException(); + } + if (!Td.isEmpty(data.quote)) { + this.contentPreview = new ContentPreview(data.quote.text, false); + } else { + this.contentPreview = ContentPreview.getChatListPreview(tdlib, data.message.chatId, data.messageDeleted ? null : data.message, true); + } + if (contentPreview.hasRefresher() && !(BitwiseUtils.hasFlag(data.options, Options.IGNORE_ALBUM_REFRESHERS) && contentPreview.isMediaGroup())) { contentPreview.refreshContent((chatId, messageId, newPreview, oldPreview) -> { tdlib.runOnUiThread(() -> { if (this.contentPreview == oldPreview) { @@ -187,7 +383,11 @@ private void buildPreview () { private int lastTextWidth; private int getTextHorizontalOffset () { - return (int) (mediaPreview.getMetadata().getTotalWidth() + mediaPreview.getMetadata().getVisibility() * Screen.dp(PADDING_SIZE)) + Screen.dp(getLinePadding()); + return getMediaWidth() + Screen.dp(getLinePadding()); + } + + private int getMediaWidth () { + return (int) (mediaPreview.getMetadata().getTotalWidth() + mediaPreview.getMetadata().getTotalVisibility() * Screen.dp(PADDING_SIZE)); } private int contentInset; @@ -222,8 +422,8 @@ private void buildText (boolean isLayout) { this.lastTextWidth = textWidth; if (textWidth > 0) { if (isLayout && !titleText.isEmpty()) { - for (ListAnimator.Entry entry : titleText) { - entry.item.changeMaxWidth(textWidth); + for (ListAnimator.Entry entry : titleText) { + entry.item.content.changeMaxWidth(textWidth); } } else { buildTitleText(textWidth, false); @@ -251,15 +451,17 @@ private void buildText (boolean isLayout) { private void buildMediaPreview (boolean animated) { MediaPreview preview; + boolean showSmallMedia = false; - if (message != null) { - preview = MediaPreview.valueOf(tdlib, message, contentPreview, Screen.dp(IMAGE_HEIGHT), Screen.dp(3f)); + if (data != null) { + preview = MediaPreview.valueOf(tdlib, data.message, contentPreview, Screen.dp(IMAGE_HEIGHT), Screen.dp(3f)); if (preview == null && useAvatarFallback) { - TdApi.Chat chat = tdlib.chat(message.chatId); + TdApi.Chat chat = tdlib.chat(data.message.chatId); if (chat != null && chat.photo != null) { preview = MediaPreview.valueOf(tdlib, chat.photo, Screen.dp(IMAGE_HEIGHT), Screen.dp(3f)); } } + showSmallMedia = data.isLinkPreviewShowSmallMedia(); } else { preview = null; } @@ -270,9 +472,15 @@ private void buildMediaPreview (boolean animated) { ComplexReceiver receiver = newComplexReceiver(false); preview.requestFiles(receiver, false); this.mediaPreview.replace(new MediaEntry(preview, receiver), animated); + this.showSmallMedia.setValue(showSmallMedia, animated); } } + public void updateShowSmallMedia (boolean animated) { + boolean showSmallMedia = data != null && data.isLinkPreviewShowSmallMedia(); + this.showSmallMedia.setValue(showSmallMedia, animated); + } + private void updateTitleText () { if (lastTextWidth > 0) { buildTitleText(lastTextWidth, true); @@ -282,15 +490,39 @@ private void updateTitleText () { @Nullable private String getTitle () { - return !StringUtils.isEmpty(forcedTitle) ? forcedTitle : message != null ? tdlib.senderName(message, true, false) : null; + if (this.data != null) { + if (!StringUtils.isEmpty(this.data.forcedTitle)) { + return this.data.forcedTitle; + } + return tdlib.senderName(this.data.message, true, false); + } + return null; } - private boolean needBoldTitle () { - return !StringUtils.isEmpty(forcedTitle) || (message != null && Td.getMessageAuthorId(message, true) != 0); - } + private static final float CORNER_PADDING = 3f; private void buildTitleText (int availWidth, boolean animated) { + if (data == null) { + throw new IllegalStateException(); + } String title = getTitle(); + TdlibAccentColor accentColor = data.accentColor(); + + Drawable drawable; + if (!Td.isEmpty(data.quote)) { + @ColorId int colorId = ColorId.messageAuthor; + if (accentColor != null) { + long complexColor = accentColor.getNameComplexColor(); + if (Theme.isColorId(complexColor)) { + colorId = Theme.extractColorValue(complexColor); + } + } + drawable = getSparseDrawable(R.drawable.baseline_format_quote_close_16, colorId); + availWidth -= drawable.getMinimumWidth() + Screen.dp(CORNER_PADDING); + } else { + drawable = null; + } + Text newText; if (!StringUtils.isEmpty(title)) { newText = new Text.Builder(tdlib, @@ -298,17 +530,19 @@ private void buildTitleText (int availWidth, boolean animated) { null, availWidth, Paints.robotoStyleProvider(TEXT_SIZE), - TextColorSets.Regular.MESSAGE_AUTHOR, + accentColor != null ? + accentColor::getNameColor : + TextColorSets.Regular.MESSAGE_AUTHOR, null ).viewProvider(viewProvider) .singleLine() .allClickable() - .allBold(needBoldTitle()) + .allBold(data.needBoldTitle()) .build(); } else { newText = null; } - this.titleText.replace(newText, animated); + this.titleText.replace(newText != null || drawable != null ? new TextEntry(newText, drawable, null, accentColor) : null, animated); } private void updateContentText () { @@ -354,7 +588,7 @@ private void buildContentText (int availWidth, boolean animated) { newText = null; receiver = null; } - this.contentText.replace(newText != null || iconRes != 0 ? new TextEntry(newText, getSparseDrawable(iconRes, ColorId.icon), receiver) : null, animated); + this.contentText.replace(newText != null || iconRes != 0 ? new TextEntry(newText, getSparseDrawable(iconRes, ColorId.icon), receiver, null) : null, animated); } @Override @@ -365,17 +599,77 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { // Drawing + public boolean getMediaPosition (RectF rectF) { + float reverseMediaFactor = showSmallMedia.getFloatValue(); + int textX = Screen.dp(getLinePadding()) + Screen.dp(PADDING_SIZE) + getTextHorizontalOffset() - (int) (getMediaWidth() * reverseMediaFactor); + ListAnimator.Entry entry = mediaPreview.singleton(); + if (entry == null) { + return false; + } + float scale = 1f, cx, cy; + if (reverseMediaFactor <= .5f) { + cx = textX - entry.item.content.getWidth() / 2f - Screen.dp(PADDING_SIZE); + cy = (SettingHolder.measureHeightForType(ListItem.TYPE_MESSAGE_PREVIEW) - Screen.dp(IMAGE_HEIGHT)) / 2f + entry.item.content.getHeight() / 2f; + } else { + scale = MINIMIZED_MEDIA_SCALE; + float x = getMeasuredWidth() - contentInset - getPaddingRight() - entry.item.content.getWidth() + (int) ((1f - reverseMediaFactor) * getMediaWidth()); + float y = (SettingHolder.measureHeightForType(ListItem.TYPE_MESSAGE_PREVIEW) - Screen.dp(IMAGE_HEIGHT)) / 2f; + cx = x + entry.item.content.getWidth() / 2f; + cy = y + entry.item.content.getHeight() / 2f; + } + + rectF.left = cx - entry.item.content.getWidth() * scale / 2f; + rectF.right = cx + entry.item.content.getWidth() * scale / 2f; + rectF.top = cy - entry.item.content.getHeight() * scale / 2f; + rectF.bottom = cy + entry.item.content.getHeight() * scale / 2f; + return true; + } + + private boolean isInsideMedia (float touchX, float touchY) { + RectF rectF = Paints.getRectF(); + if (getMediaPosition(rectF)) { + return (touchX >= rectF.left && touchX <= rectF.right && touchY >= rectF.top && touchY <= rectF.bottom); + } else { + return false; + } + } + @Override protected void onDraw (Canvas c) { - if (this.message == null) + if (this.data == null) return; - int textX = Screen.dp(getLinePadding()) + Screen.dp(PADDING_SIZE) + getTextHorizontalOffset(); + float reverseMediaFactor = showSmallMedia.getFloatValue(); + int textX = Screen.dp(getLinePadding()) + Screen.dp(PADDING_SIZE) + getTextHorizontalOffset() - (int) (getMediaWidth() * reverseMediaFactor); for (ListAnimator.Entry entry : mediaPreview) { - entry.item.content.draw(this, c, entry.item.receiver, textX - entry.item.content.getWidth() - Screen.dp(PADDING_SIZE), (SettingHolder.measureHeightForType(ListItem.TYPE_MESSAGE_PREVIEW) - Screen.dp(IMAGE_HEIGHT)) / 2f, entry.getVisibility()); + if (reverseMediaFactor <= .5f) { + float alpha = 1f - reverseMediaFactor / .5f; + entry.item.content.draw(this, c, entry.item.receiver, textX - entry.item.content.getWidth() - Screen.dp(PADDING_SIZE), (SettingHolder.measureHeightForType(ListItem.TYPE_MESSAGE_PREVIEW) - Screen.dp(IMAGE_HEIGHT)) / 2f, entry.getVisibility() * alpha); + } else { + float alpha = (reverseMediaFactor - .5f) / .5f; + int restoreToCount = Views.save(c); + float x = getMeasuredWidth() - contentInset - getPaddingRight() - entry.item.content.getWidth() + (int) ((1f - reverseMediaFactor) * getMediaWidth()); + float y = (SettingHolder.measureHeightForType(ListItem.TYPE_MESSAGE_PREVIEW) - Screen.dp(IMAGE_HEIGHT)) / 2f; + float cx = x + entry.item.content.getWidth() / 2f; + float cy = y + entry.item.content.getHeight() / 2f; + c.scale(MINIMIZED_MEDIA_SCALE, MINIMIZED_MEDIA_SCALE, cx, cy); + entry.item.content.draw(this, c, entry.item.receiver, x, y, entry.getVisibility() * alpha); + Views.restore(c, restoreToCount); + } } int contextTextY = Screen.dp(7f) + Screen.dp(TEXT_SIZE) + Screen.dp(5f); - for (ListAnimator.Entry entry : titleText) { - entry.item.draw(c, textX, Screen.dp(7f), null, entry.getVisibility()); + for (ListAnimator.Entry entry : titleText) { + int titleY = Screen.dp(7f); + if (entry.item.content != null) { + entry.item.content.draw(c, textX, titleY, null, entry.getVisibility()); + } + if (entry.item.drawable != null) { + float textCenterY = titleY + (entry.item.content != null ? entry.item.content.getLineHeight(false) : Screen.dp(TEXT_SIZE)) / 2f; + float iconX = textX + Math.max( + (entry.item.content != null ? entry.item.content.getWidth() + Screen.dp(CORNER_PADDING) : 0), + contentText.getMetadata().getTotalWidth() - entry.item.drawable.getMinimumWidth() + ); + Drawables.draw(c, entry.item.drawable, iconX, textCenterY - entry.item.drawable.getMinimumHeight() / 2f, PorterDuffPaint.get(entry.item.getNameColorId(ColorId.messageAuthor), entry.getVisibility())); + } } for (ListAnimator.Entry entry : contentText) { if (entry.item.drawable != null) { @@ -387,8 +681,29 @@ protected void onDraw (Canvas c) { } } + private static final float MINIMIZED_MEDIA_SCALE = .8f; + // Touch events + private final ClickHelper clickHelper = new ClickHelper(new ClickHelper.Delegate() { + @Override + public boolean needClickAt (View view, float x, float y) { + if (data != null && BitwiseUtils.hasFlag(data.options, Options.HANDLE_MEDIA_CLICKS)) { + return isInsideMedia(x, y); + } + return false; + } + + @Override + public void onClickAt (View view, float x, float y) { + if (data != null && BitwiseUtils.hasFlag(data.options, Options.HANDLE_MEDIA_CLICKS) && isInsideMedia(x, y)) { + if (data.onMediaClickListener != null) { + data.onMediaClickListener.onClick(MessagePreviewView.this); + } + } + } + }); + @Override public boolean onTouchEvent (MotionEvent e) { for (ListAnimator.Entry entry : contentText) { @@ -396,7 +711,7 @@ public boolean onTouchEvent (MotionEvent e) { return true; } } - return super.onTouchEvent(e); + return clickHelper.onTouchEvent(this, e) || super.onTouchEvent(e); } // Listeners @@ -429,10 +744,18 @@ public void unsubscribeFromUpdates (TdApi.Message message) { tdlib.listeners().unsubscribeFromMessageUpdates(message.chatId, this); } + private void runOnUiThreadOptional (RunnableData runnable) { + tdlib.runOnUiThread(() -> { + if (this.data != null) { + runnable.runWithData(this.data); + } + }); + } + @Override public void onChatTitleChanged (long chatId, String title) { - tdlib.runOnUiThread(() -> { - if (this.message != null && this.message.senderId.getConstructor() == TdApi.MessageSenderChat.CONSTRUCTOR && ((TdApi.MessageSenderChat) this.message.senderId).chatId == chatId) { + runOnUiThreadOptional(data -> { + if (data.relatedToChat(chatId)) { updateTitleText(); } }); @@ -440,8 +763,8 @@ public void onChatTitleChanged (long chatId, String title) { @Override public void onUserUpdated (TdApi.User user) { - tdlib.runOnUiThread(() -> { - if (this.message != null && this.message.senderId.getConstructor() == TdApi.MessageSenderUser.CONSTRUCTOR && ((TdApi.MessageSenderUser) this.message.senderId).userId == user.id) { + runOnUiThreadOptional(data -> { + if (data.relatedToUser(user.id)) { updateTitleText(); } }); @@ -449,9 +772,8 @@ public void onUserUpdated (TdApi.User user) { @Override public void onMessageContentChanged (long chatId, long messageId, TdApi.MessageContent newContent) { - tdlib.runOnUiThread(() -> { - if (this.message != null && this.message.chatId == chatId && this.message.id == messageId) { - this.message.content = newContent; + runOnUiThreadOptional(data -> { + if (data.updateMessageContent(chatId, messageId, newContent)) { buildPreview(); } }); @@ -459,19 +781,27 @@ public void onMessageContentChanged (long chatId, long messageId, TdApi.MessageC @Override public void onMessagesDeleted (long chatId, long[] messageIds) { - tdlib.runOnUiThread(() -> { - if (this.message != null && this.message.chatId == chatId) { + runOnUiThreadOptional(data -> { + if (data.message.chatId == chatId) { + boolean needUpdate = false; + if (ArrayUtils.contains(messageIds, data.message.id)) { + data.messageDeleted = true; + needUpdate = true; + } if (this.contentPreview != null) { Tdlib.Album album = this.contentPreview.getAlbum(); if (album != null) { for (TdApi.Message albumMessage : album.messages) { if (ArrayUtils.contains(messageIds, albumMessage.id)) { - buildPreview(); // one of album's message was deleted + needUpdate = true; // one of album's message was deleted, force reload album. break; } } } } + if (needUpdate) { + buildPreview(); + } } }); } @@ -506,9 +836,13 @@ public void detach () { } } + public void clear () { + setDisplayData(null); + } + @Override public void performDestroy () { - setMessage(null, null, null, false); + clear(); TGLegacyManager.instance().removeEmojiListener(this); } @@ -516,4 +850,29 @@ public void performDestroy () { public void onEmojiUpdated (boolean isPackSwitch) { invalidate(); } + + @Override + public boolean isMediaGroup () { + Tdlib.Album album = contentPreview != null ? contentPreview.getAlbum() : null; + return album != null; + } + + @Override + public List getVisibleMediaGroup () { + Tdlib.Album album = contentPreview != null ? contentPreview.getAlbum() : null; + return album != null ? album.messages : null; + } + + @Override + public TdApi.Message getVisibleMessage () { + if (data != null && data.message.chatId != 0) { + return data.message; + } + return null; + } + + @Override + public int getVisibleMessageFlags () { + return TdlibMessageViewer.Flags.NO_SENSITIVE_SCREENSHOT_NOTIFICATION; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageQuickActionSwipeHelper.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageQuickActionSwipeHelper.java index 6004588dcd..c7d7d0a281 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageQuickActionSwipeHelper.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageQuickActionSwipeHelper.java @@ -1,7 +1,5 @@ package org.thunderdog.challegram.component.chat; -import android.util.Log; - import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.tool.Screen; @@ -37,7 +35,7 @@ public void translate (float dx, float dy, boolean bySwipe) { final boolean lockedVerticalSwipe = isLockedVerticalSwipe(); final boolean isLeft = dx > 0; final float ddx = dx - currentDx; - final float ddy = lockedVerticalSwipe ? 0f: dy - currentDy; + final float ddy = lockedVerticalSwipe ? 0f : dy - currentDy; currentDx = dx; actualDx += ddx; currentDy = dy; actualDy += ddy; actualDy = clampActualDy(isLeft, actualDy); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageSenderButton.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageSenderButton.java index 476732c648..db5e87b8d3 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageSenderButton.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageSenderButton.java @@ -183,6 +183,7 @@ public void checkPosition () { public void setSendFactor (float factor) { sendFactor = factor; checkPositionAndSize(); + checkAlpha(); } public void checkPositionAndSize () { @@ -216,7 +217,7 @@ private void update (TdApi.MessageSender sender, boolean isPersonal, boolean isA currentButtonView = oldButtonView; oldButtonView = swap; - currentButtonView.setDrawMode(tdlib, sender, sender != null ? MODE_CHAT_BUTTON: isAnonymous ? MODE_ANONYMOUS_BUTTON: MODE_PERSON_BUTTON); + currentButtonView.setDrawMode(tdlib, sender, sender != null ? MODE_CHAT_BUTTON : isAnonymous ? MODE_ANONYMOUS_BUTTON : MODE_PERSON_BUTTON); replaceAnimator.replace(currentButtonView, animated); invalidate(); @@ -246,13 +247,24 @@ public void onItemChanged (ReplaceAnimator animator) { } } + private boolean inSlowMode; + + public void setInSlowMode (boolean inSlowMode) { + this.inSlowMode = inSlowMode; + checkAlpha(); + } + + private void checkAlpha () { + setAlpha(alphaAnimator.getFloatValue() * (inSlowMode ? (1f - sendFactor) : 1f)); + } + @Override public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { if (id == QUICK_ANIMATOR) { currentButtonView.setQuickSelectFactor(factor); oldButtonView.setQuickSelectFactor(factor); } else if (id == VISIBLE_ANIMATOR) { - setAlpha(factor); + checkAlpha(); } invalidate(); } @@ -305,7 +317,7 @@ public ButtonView (@NonNull Context context) { public void setDrawMode (Tdlib tdlib, TdApi.MessageSender sender, int mode) { this.avatarView.setMessageSender(tdlib, sender); - this.avatarView.setVisibility(mode == MODE_CHAT_BUTTON ? View.VISIBLE: View.GONE); + this.avatarView.setVisibility(mode == MODE_CHAT_BUTTON ? View.VISIBLE : View.GONE); this.sender = sender; this.mode = mode; diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java index 50cc200a77..3d608124e0 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java @@ -33,6 +33,7 @@ import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.ContentPreview; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.data.TGMessageBotInfo; @@ -100,10 +101,11 @@ public class MessageView extends SparseDrawableView implements Destroyable, Draw private final AvatarReceiver avatarReceiver; private final GifReceiver gifReceiver; private final ComplexReceiver avatarsReceiver; + private final ComplexReceiver reactionAvatarsReceiver; private final ComplexReceiver emojiStatusReceiver; private final ComplexReceiver reactionsComplexReceiver, textMediaReceiver, replyTextMediaReceiver; private final DoubleImageReceiver replyReceiver; - private final RefreshRateLimiter refreshRateLimiter; + private final RefreshRateLimiter refreshRateLimiter, highRefreshRateLimiter; private ComplexReceiver footerTextMediaReceiver; private ImageReceiver contentReceiver; @@ -112,14 +114,23 @@ public class MessageView extends SparseDrawableView implements Destroyable, Draw private MessageViewGroup parentMessageViewGroup; private MessagesManager manager; + public MessageView (Context context) { super(context); this.refreshRateLimiter = new RefreshRateLimiter(this, Config.MAX_ANIMATED_EMOJI_REFRESH_RATE); - avatarReceiver = new AvatarReceiver(this); - avatarsReceiver = new ComplexReceiver(this); - gifReceiver = new GifReceiver(this); // TODO use refreshRateLimiter? + this.highRefreshRateLimiter = new RefreshRateLimiter(this, 60.0f); + this.highRefreshRateLimiter.attachOtherRefreshLimiter(refreshRateLimiter); + + avatarReceiver = new AvatarReceiver(this) + .setUpdateListener(refreshRateLimiter.passThroughUpdateListener()); + avatarsReceiver = new ComplexReceiver(this) + .setUpdateListener(refreshRateLimiter.passThroughComplexUpdateListener()); + reactionAvatarsReceiver = new ComplexReceiver(this) + .setUpdateListener(refreshRateLimiter.passThroughComplexUpdateListener()); + gifReceiver = new GifReceiver(this) + .setUpdateListener(refreshRateLimiter.passThroughUpdateListener()); reactionsComplexReceiver = new ComplexReceiver() - .setUpdateListener(new RefreshRateLimiter(this, 60.0f)); // Limit by 60fps + .setUpdateListener(highRefreshRateLimiter); textMediaReceiver = new ComplexReceiver() .setUpdateListener(refreshRateLimiter); emojiStatusReceiver = new ComplexReceiver() @@ -127,7 +138,8 @@ public MessageView (Context context) { replyTextMediaReceiver = new ComplexReceiver() .setUpdateListener(refreshRateLimiter); //noinspection ContantConditions - replyReceiver = new DoubleImageReceiver(this, Config.USE_SCALED_ROUNDINGS ? Screen.dp(Theme.getImageRadius()) : 0); + replyReceiver = new DoubleImageReceiver(this, Config.USE_SCALED_ROUNDINGS ? Screen.dp(Theme.getImageRadius()) : 0) + .setUpdateListener(refreshRateLimiter); setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); if (Config.HARDWARE_MESSAGE_LAYER) { @@ -155,6 +167,7 @@ public void setCustomMeasureDisabled (boolean disabled) { public void performDestroy () { avatarReceiver.destroy(); avatarsReceiver.performDestroy(); + reactionAvatarsReceiver.performDestroy(); replyReceiver.destroy(); replyTextMediaReceiver.performDestroy(); gifReceiver.destroy(); @@ -184,7 +197,8 @@ public void setUseReceivers () { } public void setUseComplexReceiver () { - complexReceiver = new ComplexReceiver(this); + complexReceiver = new ComplexReceiver(this) + .setUpdateListener(refreshRateLimiter.passThroughComplexUpdateListener()); flags |= FLAG_USE_COMPLEX_RECEIVER; } @@ -286,6 +300,7 @@ public void setMessage (TGMessage message) { message.requestAvatar(avatarReceiver); message.requestReactions(reactionsComplexReceiver); message.requestCommentsResources(avatarsReceiver, false); + message.requestReactionsResources(reactionAvatarsReceiver, false); message.requestAllTextMedia(this); if ((flags & FLAG_USE_COMMON_RECEIVER) != 0) { @@ -408,6 +423,10 @@ public ComplexReceiver getAvatarsReceiver () { return avatarsReceiver; } + public ComplexReceiver getReactionAvatarsReceiver () { + return reactionAvatarsReceiver; + } + public ImageReceiver getContentReceiver () { return contentReceiver; } @@ -453,6 +472,7 @@ public void onAttachedToRecyclerView () { isAttached = true; avatarReceiver.attach(); avatarsReceiver.attach(); + reactionAvatarsReceiver.attach(); gifReceiver.attach(); reactionsComplexReceiver.attach(); textMediaReceiver.attach(); @@ -474,6 +494,7 @@ public void onDetachedFromRecyclerView () { isAttached = false; avatarReceiver.detach(); avatarsReceiver.detach(); + reactionAvatarsReceiver.detach(); gifReceiver.detach(); reactionsComplexReceiver.detach(); textMediaReceiver.detach(); @@ -587,7 +608,7 @@ public static Object fillMessageOptions (MessagesController m, TGMessage msg, @N // Promotion - if (msg.isSponsored()) { + if (msg.isSponsoredMessage()) { ids.append(R.id.btn_messageCopy); strings.append(R.string.Copy); icons.append(R.drawable.baseline_content_copy_24); @@ -640,6 +661,7 @@ public static Object fillMessageOptions (MessagesController m, TGMessage msg, @N icons.append(R.drawable.outline_forum_24); } + //noinspection SwitchIntDef switch (content.getConstructor()) { case TdApi.MessagePoll.CONSTRUCTOR: { TdApi.Poll poll = ((TdApi.MessagePoll) content).poll; @@ -742,14 +764,14 @@ public static Object fillMessageOptions (MessagesController m, TGMessage msg, @N if (msg.getMessage().content.getConstructor() == TdApi.MessageDice.CONSTRUCTOR && !msg.tdlib().hasRestriction(msg.getMessage().chatId, RightId.SEND_OTHER_MESSAGES)) { String emoji = ((TdApi.MessageDice) msg.getMessage().content).emoji; ids.append(R.id.btn_messageReplyWithDice); - if (TD.EMOJI_DART.textRepresentation.equals(emoji)) { + if (ContentPreview.EMOJI_DART.textRepresentation.equals(emoji)) { strings.append(R.string.SendDart); - } else if (TD.EMOJI_DICE.textRepresentation.equals(emoji)) { + } else if (ContentPreview.EMOJI_DICE.textRepresentation.equals(emoji)) { strings.append(R.string.SendDice); } else { strings.append(R.string.SendUnknownDice); } - icons.append(TD.EMOJI_DART.textRepresentation.equals(emoji) ? R.drawable.baseline_gps_fixed_24 : R.drawable.baseline_casino_24); + icons.append(ContentPreview.EMOJI_DART.textRepresentation.equals(emoji) ? R.drawable.baseline_gps_fixed_24 : R.drawable.baseline_casino_24); } ids.append(R.id.btn_messageReply); @@ -1008,7 +1030,7 @@ public static Object fillMessageOptions (MessagesController m, TGMessage msg, @N } } - if (msg.canBeReported() && !msg.isFakeMessage()) { + if (msg.canBeReported() && !msg.isFakeMessage() && !msg.isSponsoredMessage()) { if (isMore) { ids.append(R.id.btn_messageReport); strings.append(R.string.MessageReport); @@ -1146,6 +1168,9 @@ private void showEventLogOptions (MessagesController m, TGMessage msg) { TdApi.ChatMemberStatus myStatus = m.tdlib().chatStatus(m.getChatId()); RunnableData showOptions = (member) -> { + if (ids.isEmpty()) { + return; + } m.showOptions(null, ids.get(), strings.get(), colors.get(), icons.get(), (optionItemView, id) -> { int optionItemId = optionItemView.getId(); if (optionItemId == R.id.btn_restrictMember) { @@ -1414,7 +1439,7 @@ private void preventLongPress () { } private boolean startSwipeIfNeeded (float diffX) { - if (msg == null || msg.isNotSent() || !msg.canSwipe() || msg.isSponsored() || UI.getContext(getContext()).getRecordAudioVideoController().isOpen()) { + if (msg == null || msg.isNotSent() || !msg.canSwipe() || msg.isSponsoredMessage() || UI.getContext(getContext()).getRecordAudioVideoController().isOpen()) { return false; } MessagesController m = msg.messagesController(); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesAdapter.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesAdapter.java index 9414d4f8af..a6459ec658 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesAdapter.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesAdapter.java @@ -473,11 +473,11 @@ public boolean addMessage (TGMessage message, boolean top, boolean needScrollToB TGMessage bottomMessage = top ? null : getMessage(0); TGMessage topMessage = top ? getMessage(getMessageCount() - 1) : null; - boolean sponsoredFlag = bottomMessage != null && bottomMessage.isSponsored(); + boolean sponsoredFlag = bottomMessage != null && bottomMessage.isSponsoredMessage(); if (sponsoredFlag && items != null) { for (TGMessage msg : items) { - if (!msg.isSponsored()) { + if (!msg.isSponsoredMessage()) { bottomMessage = msg; break; } @@ -506,7 +506,7 @@ public boolean addMessage (TGMessage message, boolean top, boolean needScrollToB } } message.mergeWith(bottomMessage, !top || this.items == null || this.items.isEmpty()); - if (bottomMessage != null && getBottomMessage().isSponsored() && !message.isSponsored()) { + if (bottomMessage != null && getBottomMessage().isSponsoredMessage() && !message.isSponsoredMessage()) { bottomMessage.setNeedExtraPresponsoredPadding(false); bottomMessage.setNeedExtraPadding(false); message.setNeedExtraPresponsoredPadding(true); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesLoader.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesLoader.java index f7cc30021a..76e0f7ff0a 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesLoader.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesLoader.java @@ -33,13 +33,13 @@ import org.thunderdog.challegram.core.Background; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.ChatEventUtil; -import org.thunderdog.challegram.data.SponsoredMessageUtils; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.data.TdApiExt; import org.thunderdog.challegram.data.ThreadInfo; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibDelegate; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.unsorted.Settings; @@ -90,6 +90,7 @@ public class MessagesLoader implements Client.ResultHandler { private int knownTotalMessageCount = -1; private MessageId scrollMessageId; private int scrollHighlightMode; + private TdlibMessageViewer.Viewport viewport; public static final int SPECIAL_MODE_NONE = 0; public static final int SPECIAL_MODE_EVENT_LOG = 1; @@ -102,6 +103,7 @@ public class MessagesLoader implements Client.ResultHandler { private String searchQuery; private TdApi.MessageSender searchSender; private TdApi.SearchMessagesFilter searchFilter; + private TdApi.MessageSource messageSource; private @Nullable TdApi.Chat chat; private @Nullable ThreadInfo messageThread; @@ -133,8 +135,6 @@ public void processResult (TdApi.Object object) { if (object.getConstructor() == TdApi.SponsoredMessages.CONSTRUCTOR) { message = ((TdApi.SponsoredMessages) object); - } else if (tdlib.account().isDebug()) { - message = SponsoredMessageUtils.generateSponsoredMessages(tdlib); } else { message = null; } @@ -161,6 +161,40 @@ public void setChat (@Nullable TdApi.Chat chat, @Nullable ThreadInfo messageThre this.messageThread = messageThread; this.specialMode = mode; this.searchFilter = filter; + this.messageSource = newMessageSource(); + recycleMessageViewer(); + this.viewport = tdlib.messageViewer().createViewport(messageSource, manager.controller()); + } + + private void recycleMessageViewer () { + if (viewport != null) { + viewport.performDestroy(); + viewport = null; + } + } + + public TdlibMessageViewer.Viewport viewport () { + if (viewport == null) + throw new IllegalStateException(); + return viewport; + } + + public TdApi.MessageSource messageSource () { + return messageSource; + } + + private TdApi.MessageSource newMessageSource () { + if (manager.readMessagesDisabled()) { + return new TdApi.MessageSourceHistoryPreview(); + } else if (specialMode == MessagesLoader.SPECIAL_MODE_EVENT_LOG) { + return new TdApi.MessageSourceChatEventLog(); + } else if (specialMode == MessagesLoader.SPECIAL_MODE_SEARCH) { + return new TdApi.MessageSourceSearch(); + } else if (getMessageThreadId() != 0) { + return new TdApi.MessageSourceMessageThreadHistory(); + } else { + return new TdApi.MessageSourceChatHistory(); + } } public void setSearchParameters (String query, TdApi.MessageSender sender, TdApi.SearchMessagesFilter filter) { @@ -254,13 +288,6 @@ public void onResult (final TdApi.Object object) { nextSearchFromMessageId = 0; break; } - case TdApi.Error.CONSTRUCTOR: { - Log.w(Log.TAG_MESSAGES_LOADER, "Received error: %s", TD.toErrorString(object)); - messages = new TdApi.Message[0]; - knownTotalCount = -1; - nextSearchOffset = null; nextSearchFromMessageId = 0; - break; - } case TdApi.ChatEvents.CONSTRUCTOR: { if (Log.isEnabled(Log.TAG_MESSAGES_LOADER)) { Log.i(Log.TAG_MESSAGES_LOADER, "Received %d events in %dms", ((TdApi.ChatEvents) object).events.length, ms); @@ -270,17 +297,15 @@ public void onResult (final TdApi.Object object) { nextSearchOffset = null; nextSearchFromMessageId = 0; break; } + case TdApi.Error.CONSTRUCTOR: { + Log.w(Log.TAG_MESSAGES_LOADER, "Received error: %s", TD.toErrorString(object)); + messages = new TdApi.Message[0]; + knownTotalCount = -1; + nextSearchOffset = null; nextSearchFromMessageId = 0; + break; + } default: { - synchronized (lock) { - lastHandler = null; - } - Log.unexpectedTdlibResponse(object, - TdApi.GetChatHistory.class, - TdApi.Messages.class, TdApi.FoundMessages.class, TdApi.FoundChatMessages.class, - TdApi.ChatEvents.class, - TdApi.Error.class - ); - return; + throw new UnsupportedOperationException(object.toString()); } } @@ -470,12 +495,14 @@ public void onResult (final TdApi.Object object) { mergeMode = MERGE_MODE_TOP; mergeChunk = messages; Log.i(Log.TAG_MESSAGES_LOADER, "Loading more groupped messages on the top, count: %d, fromMessageId: %d", loadMoreTopCount, oldestMessage.id); + Log.ensureReturnType(TdApi.GetChatHistory.class, TdApi.Messages.class); tdlib.client().send(new TdApi.GetChatHistory(messages[0].chatId, oldestMessage.id, 0, loadMoreTopCount, true), this); return; } else if (loadMoreBottomCount > 0) { mergeMode = MERGE_MODE_BOTTOM; mergeChunk = messages; Log.i(Log.TAG_MESSAGES_LOADER, "Loading more groupped messages on the bottom, count: %d, fromMessageId: %d", loadMoreBottomCount + 1, newestMessage.id); + Log.ensureReturnType(TdApi.GetChatHistory.class, TdApi.Messages.class); tdlib.client().send(new TdApi.GetChatHistory(messages[0].chatId, newestMessage.id, -loadMoreBottomCount, loadMoreBottomCount + 1, true), this); return; } @@ -552,10 +579,10 @@ private static class PreviewMessage { public final int date; public final int after; public final boolean out; - public final int senderUserId; + public final long senderUserId; public final TdApi.MessageContent content; - public PreviewMessage (int date, int after, boolean out, int senderUserId, TdApi.MessageContent content) { + public PreviewMessage (int date, int after, boolean out, long senderUserId, TdApi.MessageContent content) { this.date = date; this.after = after; this.out = out; @@ -616,7 +643,7 @@ private static String parsePreviewString (String string, int lang) { private static TdApi.User parsePreviewUser (Tdlib tdlib, JSONArray data, int lang) throws JSONException { int dataArrayLength = data.length(); - int userId = data.getInt(0); + long userId = data.getLong(0); TdApi.User user = TD.newFakeUser(userId, parsePreviewString(data.getString(1), lang), dataArrayLength > 2 ? parsePreviewString(data.getString(2), lang) : null); String remoteId = dataArrayLength > 3 ? data.getString(3) : null; if (!StringUtils.isEmpty(remoteId) && !Strings.isValidLink(remoteId)) { @@ -642,6 +669,7 @@ private static boolean parsePreviewMessages (TdlibDelegate context, List= 0 && (minDistance == -1 || distance < minDistance)) { minDistance = distance; @@ -1517,7 +1554,7 @@ private void processMessages (final long currentContextId, TdApi.Message[] messa if (!items.isEmpty()) { for (TGMessage message : items) { - if (!message.isSponsored()) { + if (!message.isSponsoredMessage()) { suitableMessage = message; break; } @@ -1749,7 +1786,7 @@ private boolean isEndReached (MessageId messageId) { return true; } } - if (searchFilter != null && searchFilter.getConstructor() == TdApi.SearchMessagesFilterPinned.CONSTRUCTOR && manager.maxPinnedMessageId() != 0 && messageId.getMessageId() >= manager.maxPinnedMessageId()) { + if (searchFilter != null && Td.isPinnedFilter(searchFilter) && manager.maxPinnedMessageId() != 0 && messageId.getMessageId() >= manager.maxPinnedMessageId()) { return true; } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesManager.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesManager.java index 2f13cb2793..2b19fa90c3 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesManager.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesManager.java @@ -29,12 +29,10 @@ import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.BaseActivity; -import org.thunderdog.challegram.BuildConfig; import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; -import org.thunderdog.challegram.data.MessageListManager; import org.thunderdog.challegram.data.SponsoredMessageUtils; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; @@ -46,11 +44,14 @@ import org.thunderdog.challegram.player.TGPlayerController; import org.thunderdog.challegram.telegram.ListManager; import org.thunderdog.challegram.telegram.MessageEditListener; +import org.thunderdog.challegram.telegram.MessageListManager; import org.thunderdog.challegram.telegram.MessageListener; import org.thunderdog.challegram.telegram.MessageThreadListener; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibCache; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; import org.thunderdog.challegram.telegram.TdlibSettingsManager; +import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.PropertyId; import org.thunderdog.challegram.theme.Theme; @@ -72,15 +73,12 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import me.vkryl.core.ArrayUtils; import me.vkryl.core.ColorUtils; import me.vkryl.core.StringUtils; -import me.vkryl.core.collection.LongSet; -import me.vkryl.core.lambda.CancellableRunnable; import me.vkryl.core.lambda.RunnableData; import me.vkryl.td.ChatId; import me.vkryl.td.MessageId; @@ -129,7 +127,7 @@ public void onScrollStateChanged (RecyclerView recyclerView, int newState) { if (MessagesManager.this.isScrolling != isScrolling) { MessagesManager.this.isScrolling = isScrolling; if (!isScrolling) { - viewMessages(); + viewMessages(true); } } if (newState == RecyclerView.SCROLL_STATE_IDLE) { @@ -140,7 +138,7 @@ public void onScrollStateChanged (RecyclerView recyclerView, int newState) { @Override public void onScrolled (RecyclerView recyclerView, int dx, int dy) { - viewMessages(); + viewMessages(true); if (dy == 0) { saveScrollPosition(); ((MessagesRecyclerView) recyclerView).showDateForcely(); @@ -263,7 +261,7 @@ private void checkVisibleContentProtection (int first, int last) { View view = manager.findViewByPosition(i); if (view instanceof MessageProvider) { TGMessage message = ((MessageProvider) view).getMessage(); - if (message != null && !message.canBeSaved() && !message.isSponsored()) { + if (message != null && !message.canBeSaved() && !message.isSponsoredMessage()) { hasVisibleProtectedContent = true; break; } @@ -273,7 +271,10 @@ private void checkVisibleContentProtection (int first, int last) { setHasVisibleProtectedContent(hasVisibleProtectedContent); } - public void viewMessages () { + public void viewMessages (boolean byScroll) { + if (!byScroll && messageViewer != null) { + messageViewer.run(); + } if (manager != null) { int first = manager.findFirstVisibleItemPosition(); int last = manager.findLastVisibleItemPosition(); @@ -298,121 +299,101 @@ public void viewMessages () { } public void onViewportMeasure () { - viewMessages(); + viewMessages(false); saveScrollPosition(); } - private long lastCheckedTopId; - private long lastCheckedBottomId; - private long lastViewedMention; - private int lastCheckedCount; + private long lastViewedMentionMessageId; private boolean viewDisplayedMessages (int first, int last) { - if (first == -1 || last == -1 || !allowReadMessages()) { - return false; - } - - TGMessage topEdge = adapter.getMessage(first); - TGMessage bottomEdge = adapter.getMessage(last); - - if (topEdge == null || bottomEdge == null) { - return false; - } - - long topId = topEdge.getSmallestId(); - long bottomId = bottomEdge.getBiggestId(); - final int count = last - first + 1; - - if (lastCheckedTopId == topId && lastCheckedBottomId == bottomId && lastCheckedCount == count) { + if (first == -1 || last == -1) { return false; } - boolean success = true; - - LongSet list = null; - LongSparseArray refreshMap = null; - int maxDate = 0; boolean headerVisible = false; boolean hasProtectedContent = false; for (int viewIndex = first; viewIndex <= last; viewIndex++) { View view = manager.findViewByPosition(viewIndex); - if (!(view instanceof MessageProvider)) { - success = false; - } TGMessage msg = view instanceof MessageProvider ? ((MessageProvider) view).getMessage() : null; if (msg != null && msg.getChatId() == loader.getChatId()) { - if (!msg.canBeSaved() && !msg.isSponsored()) { + if (!msg.canBeSaved() && !msg.isSponsoredMessage()) { hasProtectedContent = true; } if (isHeaderMessage(msg)) { headerVisible = true; } - maxDate = Math.max(msg.getDate(), maxDate); - if (!inSpecialMode()) { - if (msg.markAsViewed() || msg.containsUnreadReactions()) { - long id = msg.getBiggestId(); - if (msg.containsUnreadMention() && id > lastViewedMention) { - lastViewedMention = id; - } - if (msg.containsUnreadReactions() && id > lastViewedReaction) { - lastViewedReaction = id; - } - } - } - if (list == null) { - list = new LongSet(last - first); - } else { - list.ensureCapacity(last - first); - } - msg.getIds(list); - if (msg.needRefreshViewCount()) { - if (refreshMap == null) { - refreshMap = new LongSparseArray<>(); - } - LongSet refreshList = refreshMap.get(msg.getChatId()); - if (refreshList == null) { - refreshList = new LongSet(last - first); - refreshMap.put(msg.getChatId(), refreshList); - } else { - refreshList.ensureCapacity(last - first); - } - msg.getIds(refreshList); - } } } - LongSparseArray viewedMap; - if (refreshMap != null) { - viewedMap = new LongSparseArray<>(refreshMap.size()); - for (int i = 0; i < refreshMap.size(); i++) { - viewedMap.append(refreshMap.keyAt(i), refreshMap.valueAt(i).toArray()); - } - } else { - viewedMap = null; + setHasVisibleProtectedContent(hasProtectedContent); + setHeaderVisible(headerVisible); + + return true; + } + + public interface MessageProvider extends TdlibUi.MessageProvider { + TGMessage getMessage (); + + + // MessageProvider + + @Override + default boolean isSponsoredMessage () { + TGMessage msg = getMessage(); + return msg != null && !msg.isFakeMessage() && msg.isSponsoredMessage(); } - if (success) { - lastCheckedTopId = topId; - lastCheckedBottomId = bottomId; - lastCheckedCount = count; - } else { - lastCheckedTopId = lastCheckedBottomId = lastCheckedCount = 0; + @Override + default TdApi.SponsoredMessage getVisibleSponsoredMessage () { + TGMessage msg = getMessage(); + return msg != null && !msg.isFakeMessage() ? msg.getSponsoredMessage() : null; } - setHasVisibleProtectedContent(hasProtectedContent); - setRefreshMessages(loader.getChatId(), loader.getMessageThreadId(), viewedMap, maxDate); - setHeaderVisible(headerVisible); + @Override + default boolean isMediaGroup () { + TGMessage msg = getMessage(); + return msg != null && !msg.isFakeMessage() && msg.getCombinedMessageCount() > 1; + } - if (list != null) { - viewMessagesInternal(loader.getChatId(), loader.getMessageThreadId(), list); + @Override + default List getVisibleMediaGroup () { + TGMessage msg = getMessage(); + if (msg != null && !msg.isFakeMessage()) { + return Arrays.asList(msg.getAllMessages()); + } + return null; } - return true; - } + @Override + default TdApi.Message getVisibleMessage () { + TGMessage msg = getMessage(); + return msg != null && !msg.isFakeMessage() ? msg.getMessage() : null; + } - public interface MessageProvider { - TGMessage getMessage (); + @Override + default long getVisibleChatId () { + TGMessage msg = getMessage(); + return msg != null && !msg.isFakeMessage() ? msg.getChatId() : 0; + } + + @Override + default int getVisibleMessageFlags () { + TGMessage msg = getMessage(); + if (msg != null && !msg.isFakeMessage()) { + int flags; + if (msg.isHot()) { + flags = TdlibMessageViewer.Flags.NO_SCREENSHOT_NOTIFICATION; + } else { + flags = TdlibMessageViewer.Flags.NO_SENSITIVE_SCREENSHOT_NOTIFICATION; + } + if (msg.needRefreshViewCount()) { + flags |= TdlibMessageViewer.Flags.REFRESH_INTERACTION_INFO; + } + return flags; + } + return 0; + } } private boolean lastScrollToBottomVisible; @@ -506,7 +487,10 @@ public int getPinnedMessageCount () { } private void checkPinnedMessages () { - setPinnedMessagesAvailable(pinnedMessages != null && pinnedMessages.isAvailable() && !tdlib.settings().isMessageDismissed(loader.getChatId(), pinnedMessages.getMaxMessageId())); + long chatId = loader.getChatId(); + setPinnedMessagesAvailable(pinnedMessages != null && pinnedMessages.isAvailable() && + !(tdlib.settings().isMessageDismissed(chatId, pinnedMessages.getMaxMessageId()) || tdlib.chatRestricted(chatId)) + ); } @Override @@ -534,14 +518,13 @@ public boolean canRestorePinnedMessage () { public void destroy (ViewController context) { resetScroll(); - cancelRefresh(); returnToMessageIds = null; highlightMode = 0; tdlib.settings().removePinnedMessageDismissListener(this); highlightMessageId = null; hasScrolled = false; - lastViewedMention = 0; - lastViewedReaction = 0; + lastViewedMentionMessageId = 0; + lastViewedReactionMessageId = 0; chatAdmins = null; if (pinnedMessages != null) { pinnedMessages.performDestroy(); @@ -562,9 +545,9 @@ public void destroy (ViewController context) { tdlib.closeChat(chatId, context, true); } loader.reuse(); + messageViewer = null; adapter.clear(true); clearHeaderMessage(); - lastCheckedBottomId = lastCheckedTopId = 0; awaitingForPinnedMessages = false; wasScrollByUser = false; } @@ -597,7 +580,7 @@ public void openSearch (TdApi.Chat chat, String query, TdApi.MessageSender sende loader.setChat(chat, null, MessagesLoader.SPECIAL_MODE_SEARCH, filter); loader.setSearchParameters(query, sender, filter); adapter.setChatType(chat.type); - if (filter != null && filter.getConstructor() == TdApi.SearchMessagesFilterPinned.CONSTRUCTOR) { + if (filter != null && Td.isPinnedFilter(filter)) { initPinned(chat.id, 1, 1); } if (highlightMessageId != null) { @@ -661,7 +644,8 @@ public TdApi.ChatEventLogFilters getEventLogFilters () { @Override public void onMaxMessageIdChanged (ListManager list, long maxMessageId) { if (maxMessageId != 0) { - setPinnedMessagesAvailable(!tdlib.settings().isMessageDismissed(loader.getChatId(), maxMessageId)); + long chatId = loader.getChatId(); + setPinnedMessagesAvailable(!(tdlib.settings().isMessageDismissed(chatId, maxMessageId) || tdlib.chatRestricted(chatId))); } else if (!list.isAvailable()) { setPinnedMessagesAvailable(false); } @@ -670,7 +654,8 @@ public void onMaxMessageIdChanged (ListManager list, long maxMess private final MessageListManager.ChangeListener pinnedMessageListener = new MessageListManager.ChangeListener() { @Override public void onAvailabilityChanged (ListManager list, boolean isAvailable) { - if (!isAvailable || !tdlib.settings().hasDismissedMessages(loader.getChatId())) { + long chatId = loader.getChatId(); + if (!isAvailable || !(tdlib.settings().hasDismissedMessages(chatId) || tdlib.chatRestricted(chatId))) { // Either list became unavailable, // or it has no dismissed pinned messages setPinnedMessagesAvailable(isAvailable); @@ -732,6 +717,48 @@ public void openChat (TdApi.Chat chat, @Nullable ThreadInfo messageThread, TdApi loadFromStart(); } } + messageViewer = tdlib.ui().attachViewportToRecyclerView(loader.viewport(), controller.getMessagesView(), new TdlibUi.MessageViewCallback() { + @Override + public void onSponsoredMessageViewed (TdlibMessageViewer.Viewport viewport, View view, TdApi.SponsoredMessage sponsoredMessage, long flags, long viewId, boolean allowRequest) { + MessageProvider provider = (MessageProvider) view; + provider.getMessage().markAsViewed(); + } + + @Override + public boolean isMessageContentVisible (TdlibMessageViewer.Viewport viewport, View view) { + // TODO return false when "FOLLOW" bar overlaps the message entirely + return true; + } + + @Override + public boolean onMessageViewed (TdlibMessageViewer.Viewport viewport, View view, TdApi.Message message, long flags, long viewId, boolean allowRequest) { + if (inSpecialMode() || !allowRequest) + return false; + MessageProvider provider = (MessageProvider) view; + TGMessage msg = provider.getMessage(); + if (msg.markAsViewed() || msg.containsUnreadReactions()) { + long messageId = msg.getBiggestId(); + if (msg.containsUnreadMention() && messageId > lastViewedMentionMessageId) { + lastViewedMentionMessageId = messageId; + } + if (msg.containsUnreadReactions() && messageId > lastViewedReactionMessageId) { + lastViewedReactionMessageId = messageId; + } + return true; + } + return false; + } + + @Override + public boolean needForceRead (TdlibMessageViewer.Viewport viewport) { + return canRead(); + } + + @Override + public boolean allowViewRequest (TdlibMessageViewer.Viewport viewport) { + return isFocused; + } + }); subscribeForUpdates(); this.useReactionBubblesValue = checkReactionBubbles(); this.usedTranslateStyleMode = checkTranslateStyleMode(); @@ -884,7 +911,7 @@ private void scrollToBottom (boolean smooth) { } if (!Config.SMOOTH_SCROLL_TO_BOTTOM_ENABLED || !smooth) { - if (adapter.getBottomMessage() != null && adapter.getBottomMessage().isSponsored()) { + if (adapter.getBottomMessage() != null && adapter.getBottomMessage().isSponsoredMessage()) { controller.setScrollToBottomVisible(false, false, false); if (controller.canWriteMessages()) { manager.scrollToPosition(1); @@ -1050,7 +1077,7 @@ private void loadFromStart (MessageId fromId, TGMessage startMessage) { adapter.reset(startMessage); manager.scrollToPositionWithOffset(0, 0); loader.loadFromStart(fromId); - viewMessages(); + viewMessages(false); } private void loadFromMessage (MessageId messageId, int highlightMode, boolean force) { @@ -1058,7 +1085,7 @@ private void loadFromMessage (MessageId messageId, int highlightMode, boolean fo adapter.reset(null); } loader.loadFromMessage(messageId, highlightMode, force); - viewMessages(); + viewMessages(false); } private void loadPreviewMessages () { @@ -1201,7 +1228,7 @@ public void displayMessages (ArrayList items, int mode, int scrollPos if (!willRepeat) { onChatAwaitFinish(); } - viewMessages(); + viewMessages(false); break; } case MessagesLoader.MODE_MORE_TOP: { @@ -1343,7 +1370,7 @@ private void requestSponsoredMessage () { if (lastMessage == null) return; controller.sponsoredMessageLoaded = true; boolean isFirstItemVisible = manager.findFirstCompletelyVisibleItemPosition() == 0; - adapter.addMessage(SponsoredMessageUtils.sponsoredToTgx(this, loader.getChatId(), lastMessage.getDate(), sponsoredMessages.messages[0]), false, false); + adapter.addMessage(SponsoredMessageUtils.sponsoredToTgx(this, loader.getChatId(), sponsoredMessages.messages[0]), false, false); if (isFirstItemVisible && !isScrolling && !controller.canWriteMessages()) { manager.scrollToPositionWithOffset(1, Screen.dp(48f)); } @@ -1525,7 +1552,7 @@ public void modifyRecycler (Context context, RecyclerView recyclerView, LinearLa this.manager = manager; this.adapter = new MessagesAdapter(context, this, this.controller); - recyclerView.clearOnScrollListeners(); + recyclerView.removeOnScrollListener(listener); recyclerView.addOnScrollListener(listener); recyclerView.setAdapter(adapter); } @@ -1568,7 +1595,7 @@ private void updateNewMessage (TGMessage message) { message.onDestroy(); return; } - boolean scrollToBottom = (message.isSending() || (atBottom && (!message.isOld() || message.isChatMember()))) && !message.isSponsored(); + boolean scrollToBottom = (message.isSending() || (atBottom && (!message.isOld() || message.isChatMember()))) && !message.isSponsoredMessage(); // message.mergeWith(bottomMessage, true); if (scrollToBottom) { boolean hasScrolled = adapter.addMessage(message, false, scrollToBottom); @@ -1586,7 +1613,7 @@ private void updateNewMessage (TGMessage message) { boolean bottomFullyVisible = manager.findFirstCompletelyVisibleItemPosition() == 0; if (!adapter.addMessage(message, false, scrollToBottom)) { - if (message.isSponsored() && bottomFullyVisible) { + if (message.isSponsoredMessage() && bottomFullyVisible) { if (controller.canWriteMessages()) { manager.scrollToPosition(1); } else { @@ -1678,7 +1705,7 @@ private void updateMessageSendSucceeded (TdApi.Message message, long oldMessageI if (messageThread != null) { messageThread.updateReadInbox(message); } - viewMessages(); + viewMessages(false); } } @@ -1741,8 +1768,8 @@ private void replaceMessage (TGMessage msg, int index, long messageId, TdApi.Mes } } - private void replaceMessageContent (TGMessage msg, int index, long messageId, TdApi.MessageContent content) { - switch (msg.setMessageContent(messageId, content)) { + private void replaceMessageContent (TGMessage msg, int index, long chatId, long messageId, TdApi.MessageContent content) { + switch (msg.replaceMessageContent(chatId, messageId, content)) { case TGMessage.MESSAGE_INVALIDATED: { invalidateViewAt(index); break; @@ -1768,15 +1795,10 @@ private void updateMessageContent (long chatId, long messageId, TdApi.MessageCon controller.onMessageChanged(chatId, messageId, content); ArrayList items = adapter.getItems(); if (!adapter.isEmpty() && items != null) { - int i = 0; + int index = 0; for (TGMessage item : items) { - TdApi.Message msg = item.getMessage(); - if (item.isDescendantOrSelf(messageId)) { - replaceMessageContent(item, i, messageId, content); - } else if (msg.replyToMessageId == messageId) { - item.replaceReplyContent(messageId, content); - } - i++; + replaceMessageContent(item, index, chatId, messageId, content); + index++; } } ThreadInfo messageThread = loader.getMessageThread(); @@ -1792,10 +1814,7 @@ public void updateMessageTranslation (long chatId, long messageId, TdApi.Formatt ArrayList items = adapter.getItems(); if (!adapter.isEmpty() && items != null) { for (TGMessage item : items) { - TdApi.Message msg = item.getMessage(); - if (msg.replyToMessageId == messageId) { - item.replaceReplyTranslation(messageId, translatedText); - } + item.replaceReplyTranslation(chatId, messageId, translatedText); } } } @@ -1862,9 +1881,7 @@ public void updateMessageUnreadReactions (long messageId, @Nullable TdApi.Unread int index = adapter.indexOfMessageContainer(messageId); if (index != -1 && adapter.getItem(index).setMessageUnreadReactions(messageId, unreadReactions)) { invalidateViewAt(index); - - lastCheckedCount = 0; - viewMessages(); + viewMessages(false); } ThreadInfo messageThread = loader.getMessageThread(); if (messageThread != null) { @@ -1894,8 +1911,30 @@ public void updateMessageIsPinned (long messageId, boolean isPinned) { } } + private void handleMessageChange (TGMessage msg, int index, long messageId, @TGMessage.MessageChangeType int changeType) { + switch (changeType) { + case TGMessage.MESSAGE_INVALIDATED: { + invalidateViewAt(index); + break; + } + case TGMessage.MESSAGE_CHANGED: { + getAdapter().notifyItemChanged(index); + break; + } + case TGMessage.MESSAGE_NOT_CHANGED: { + // Nothing to do + break; + } + case TGMessage.MESSAGE_REPLACE_REQUIRED: { + TdApi.Message message = msg.getMessage(messageId); + replaceMessage(msg, index, messageId, message); + break; + } + } + } + public void updateMessagesDeleted (long chatId, long[] messageIds) { - controller.removeReply(messageIds); + controller.removeReply(chatId, messageIds); controller.onMessagesDeleted(chatId, messageIds); int removedCount = 0; @@ -1907,15 +1946,16 @@ public void updateMessagesDeleted (long chatId, long[] messageIds) { ThreadInfo messageThread = loader.getMessageThread(); long lastReadInboxMessageId = messageThread != null ? messageThread.getLastReadInboxMessageId() : chat.lastReadInboxMessageId; - int i = 0; - main: while (i < adapter.getMessageCount()) { - TGMessage item = adapter.getMessage(i); + int index = 0; + main: while (index < adapter.getMessageCount()) { + TGMessage item = adapter.getMessage(index); for (long messageId : messageIds) { switch (item.removeMessage(messageId)) { case TGMessage.REMOVE_NOTHING: { - if (item.getMessage().replyToMessageId == messageId) { - item.removeReply(messageId); + @TGMessage.MessageChangeType int changeType = item.removeMessagePreview(chatId, messageId); + if (changeType != TGMessage.MESSAGE_NOT_CHANGED) { + handleMessageChange(item, index, messageId, changeType); } break; } @@ -1934,7 +1974,7 @@ public void updateMessagesDeleted (long chatId, long[] messageIds) { } } case TGMessage.REMOVE_COMPLETELY: { - TGMessage removed = adapter.removeItem(i); + TGMessage removed = adapter.removeItem(index); if (controller.unselectMessage(messageId, removed)) { selectedCount--; unselectedSomeMessages = true; @@ -1951,7 +1991,7 @@ public void updateMessagesDeleted (long chatId, long[] messageIds) { } } - i++; + index++; } if (unselectedSomeMessages) { @@ -2009,7 +2049,7 @@ public MediaStack collectMedias (final long fromMessageId, @Nullable TdApi.Searc AtomicInteger addedAfter = new AtomicInteger(); RunnableData callback = message -> { - if (TD.isSecret(message)) + if (Td.isSecret(message.content)) return; boolean matchesFilter = filter == null || Td.matchesFilter(message, filter); MediaItem item = matchesFilter ? MediaItem.valueOf(controller.context(), tdlib, message) : null; @@ -2043,36 +2083,6 @@ public MediaStack collectMedias (final long fromMessageId, @Nullable TdApi.Searc // Reading messages - boolean viewMessagesInternal (final long chatId, final long messageThreadId, final LongSet viewed) { - if (allowReadMessages() && !viewed.isEmpty()) { - final long[] messageIds = viewed.toArray(); - if (Log.isEnabled(Log.TAG_MESSAGES_LOADER)) { - Log.i(Log.TAG_MESSAGES_LOADER, "Reading %d messages: %s", messageIds.length, Arrays.toString(messageIds)); - } - if (Log.isEnabled(Log.TAG_FCM)) { - Log.i(Log.TAG_FCM, "Reading %d messages from MessagesManager: %s", messageIds.length, Arrays.toString(messageIds)); - } - - TdApi.MessageSource source; - boolean forceRead = !inSpecialMode(); - if (controller.isInForceTouchMode() || (BuildConfig.DEBUG && Settings.instance().dontReadMessages())) { - source = new TdApi.MessageSourceHistoryPreview(); - forceRead = false; - } else if (isEventLog()) { - source = new TdApi.MessageSourceChatEventLog(); - } else if (isSearchPreview()) { - source = new TdApi.MessageSourceSearch(); - } else if (messageThreadId != 0) { - source = new TdApi.MessageSourceMessageThreadHistory(); - } else { - source = new TdApi.MessageSourceChatHistory(); - } - tdlib.client().send(new TdApi.ViewMessages(chatId, messageIds, source, forceRead), loader); - return true; - } - return false; - } - private boolean parentPaused, parentFocused, parentHidden; public void setParentHidden (boolean isHidden) { @@ -2124,9 +2134,6 @@ public void onPasscodeShowing (BaseActivity context, boolean isShowing) { private boolean isFocused; - private long viewedChatId, viewedMessageThreadId; - private LongSet viewedMessages; - private void setFocused (boolean isFocused) { if (this.isFocused != isFocused) { if (Log.isEnabled(Log.TAG_MESSAGES_LOADER)) { @@ -2141,72 +2148,6 @@ private void setFocused (boolean isFocused) { } } - private long refreshChatId; - private long refreshMessageThreadId; - private LongSparseArray refreshMessageIds; - private int refreshMaxDate; - private CancellableRunnable refreshViewsRunnable; - - private static long timeTillNextRefresh (long millis) { - long seconds = TimeUnit.MILLISECONDS.toSeconds(millis); - if (seconds < 15) { - return millis % 3000; // once per 3 seconds for the first 15 seconds - } - if (seconds < 60) { - return millis % 5000; // once per 5 seconds for 15-60 seconds - } - long minutes = TimeUnit.MILLISECONDS.toMinutes(millis); - if (minutes < 30) { - return millis % 15000; // once per 15 seconds for 1-30 minutes - } - if (minutes < 60) { - return millis % 30000; // once per 30 seconds for 30-60 minutes - } - return millis % 60000; // once per minute - } - - private void cancelRefresh () { - if (refreshViewsRunnable != null) { - refreshViewsRunnable.cancel(); - refreshViewsRunnable = null; - } - } - - private void scheduleRefresh () { - cancelRefresh(); - if (refreshChatId != 0 && refreshMessageIds != null && refreshMessageIds.size() > 0) { - long ms = refreshMaxDate != 0 ? timeTillNextRefresh(tdlib.currentTimeMillis() - TimeUnit.SECONDS.toMillis(refreshMaxDate)) : 60000; - refreshViewsRunnable = new CancellableRunnable() { - @Override - public void act () { - if (allowReadMessages()) { - ArrayList> functions = new ArrayList<>(); - for (int i = 0; i < refreshMessageIds.size(); i++) { - long chatId = refreshMessageIds.keyAt(i); - long[] messageIds = refreshMessageIds.valueAt(i); - functions.add(new TdApi.ViewMessages(chatId, messageIds, new TdApi.MessageSourceHistoryPreview(), false)); - } - tdlib.sendAll(functions.toArray(new TdApi.Function[0]), tdlib.okHandler(), () -> tdlib.ui().post(MessagesManager.this::scheduleRefresh)); - } else { - scheduleRefresh(); - } - } - }; - refreshViewsRunnable.removeOnCancel(tdlib.ui()); - tdlib.ui().postDelayed(refreshViewsRunnable, ms); - } - } - - private void setRefreshMessages (long chatId, long messageThreadId, LongSparseArray messageIds, int maxDate) { - if (this.refreshChatId != chatId || this.refreshMessageThreadId != messageThreadId || refreshMaxDate != maxDate || !ArrayUtils.contentEquals(refreshMessageIds, messageIds)) { - this.refreshChatId = chatId; - this.refreshMessageThreadId = messageThreadId; - this.refreshMessageIds = messageIds; - this.refreshMaxDate = maxDate; - scheduleRefresh(); - } - } - private boolean hasVisibleProtectedContent; public boolean hasVisibleProtectedContent () { @@ -2245,8 +2186,16 @@ private void onBlur () { saveScrollPosition(); } + public boolean readMessagesDisabled () { + return controller.isInForceTouchMode() || Settings.instance().dontReadMessages(); + } + + private boolean canRead () { + return !(inSpecialMode() || readMessagesDisabled()); + } + private void saveScrollPosition () { - if (controller.isInForceTouchMode() || inSpecialMode() || Settings.instance().dontReadMessages()) { + if (!canRead()) { return; } @@ -2266,7 +2215,7 @@ private void saveScrollPosition () { if (i != -1 && MessagesHolder.isMessageType(adapter.getItemViewType(i))) { TGMessage message = adapter.getMessage(i); - boolean isBottomSponsored = adapter.getBottomMessage() != null && adapter.getBottomMessage().isSponsored() && adapter.getMessageCount() > 1; + boolean isBottomSponsored = adapter.getBottomMessage() != null && adapter.getBottomMessage().isSponsoredMessage() && adapter.getMessageCount() > 1; ThreadInfo threadInfo = loader.getMessageThread(); if (message != null && message.getChatId() != 0) { @@ -2288,7 +2237,7 @@ private void saveScrollPosition () { scrollMessageId = scrollMessageChatId = 0; scrollMessageOtherIds = null; } else if (isBottomSponsored) { - if (message.isSponsored()) { + if (message.isSponsoredMessage()) { // the bottom VISIBLE message is sponsored - no need to save that data scrollMessageId = scrollMessageChatId = scrollOffsetInPixels = 0; scrollMessageOtherIds = null; @@ -2335,14 +2284,9 @@ public void onMissedMessagesHintReceived () { onCanLoadMoreBottomChanged(); } - private boolean allowReadMessages () { - return isFocused && !inSpecialMode(); // && !controller.isInForceTouchMode() && !inSpecialMode(); - } - private void onFocus () { - viewMessages(); + viewMessages(false); saveScrollPosition(); - scheduleRefresh(); } // Highlight message id @@ -2427,8 +2371,8 @@ public void scrollToNextMention () { return; } final long fromMessageId; - if (lastViewedMention != 0) { - fromMessageId = lastViewedMention; + if (lastViewedMentionMessageId != 0) { + fromMessageId = lastViewedMentionMessageId; } else { TGMessage message = adapter.getTopMessage(); if (message == null) { @@ -2483,7 +2427,8 @@ private void setMentionsImpl (final TdApi.Message[] messages, final long fromMes private ArrayList closestUnreadReactions; private CancellableResultHandler reactionsHandler; - private long lastViewedReaction = 0; + private long lastViewedReactionMessageId = 0; + private Runnable messageViewer; private void setUnreadReactions (final CancellableResultHandler handler, final TdApi.FoundChatMessages messages, final long fromMessageId) { tdlib.ui().post(() -> { @@ -2515,8 +2460,8 @@ public void scrollToNextUnreadReaction () { return; } final long fromMessageId; - if (lastViewedReaction != 0) { - fromMessageId = lastViewedReaction; + if (lastViewedReactionMessageId != 0) { + fromMessageId = lastViewedReactionMessageId; } else { TGMessage message = adapter.getTopMessage(); if (message == null) { @@ -3107,7 +3052,7 @@ public void onMessageSendSucceeded (final TdApi.Message message, final long oldM } @Override - public void onMessageSendFailed (final TdApi.Message message, final long oldMessageId, int errorCode, String errorMessage) { + public void onMessageSendFailed (final TdApi.Message message, final long oldMessageId, TdApi.Error error) { int sentMessageIndex = indexOfSentMessage(message.chatId, oldMessageId); if (sentMessageIndex != -1) { sentMessages.set(sentMessageIndex, message); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesSearchManager.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesSearchManager.java index 95977000a0..f46b8dfa0d 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesSearchManager.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesSearchManager.java @@ -19,7 +19,6 @@ import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; import org.jetbrains.annotations.Nullable; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.tool.UI; @@ -116,7 +115,7 @@ public void search (final long chatId, final long messageThreadId, final TdApi.M currentIsSecret = isSecret; currentFromSender = fromSender; foundTargetMessageId = foundMsgId; - currentDisplayedMessage = foundMsgId != null ? foundMsgId.getMessageId(): 0; + currentDisplayedMessage = foundMsgId != null ? foundMsgId.getMessageId() : 0; currentSearchFilter = filter; searchManagerMiddleware.setDelegate(newTotalCount -> { delegate.onSearchUpdateTotalCount(getMessageIndex(currentDisplayedMessage), currentTotalCount = newTotalCount, knownIndex(), knownTotalCount()); @@ -168,12 +167,7 @@ private void searchInternal (final int contextId, final long chatId, final long break; } default: { - if (isSecret) { - Log.unexpectedTdlibResponse(object, TdApi.SearchSecretMessages.class, TdApi.FoundMessages.class); - } else { - Log.unexpectedTdlibResponse(object, TdApi.SearchChatMessages.class, TdApi.Messages.class); - } - break; + throw new UnsupportedOperationException(object.toString()); } } }; @@ -182,7 +176,7 @@ private void searchInternal (final int contextId, final long chatId, final long TdApi.SearchSecretMessages query = new TdApi.SearchSecretMessages(chatId, input, nextSearchOffset, SEARCH_LOAD_LIMIT, filter); searchManagerMiddleware.search(query, fromSender, handler); } else { - final int offset = direction == SEARCH_DIRECTION_TOP ? 0 : ( direction == SEARCH_DIRECTION_BOTTOM ? -19: -10); + final int offset = direction == SEARCH_DIRECTION_TOP ? 0 : ( direction == SEARCH_DIRECTION_BOTTOM ? -19 : -10); TdApi.SearchChatMessages function = new TdApi.SearchChatMessages(chatId, input, fromSender, fromMessageId, offset, SEARCH_LOAD_LIMIT, filter, messageThreadId); searchManagerMiddleware.search(function, handler); } @@ -245,7 +239,7 @@ private void parseMessages (final int contextId, final boolean isMore, final int if (isMore) { TdApi.Message currentMessage = currentSearchResultsArr.get(currentDisplayedMessage); if (messages == null || messages.length == 0) { - flags &= ~(direction == SEARCH_DIRECTION_TOP ? FLAG_CAN_LOAD_MORE_TOP: FLAG_CAN_LOAD_MORE_BOTTOM); + flags &= ~(direction == SEARCH_DIRECTION_TOP ? FLAG_CAN_LOAD_MORE_TOP : FLAG_CAN_LOAD_MORE_BOTTOM); if (currentMessage != null) { delegate.showSearchResult(getMessageIndex(currentMessage.id), currentTotalCount, knownIndex(), knownTotalCount(), new MessageId(currentMessage.chatId, currentMessage.id)); } @@ -254,7 +248,7 @@ private void parseMessages (final int contextId, final boolean isMore, final int addAllMessages(messages, direction); TdApi.Message message = getNextMessage(direction == SEARCH_DIRECTION_TOP); if (message == null) { - flags &= ~(direction == SEARCH_DIRECTION_TOP ? FLAG_CAN_LOAD_MORE_TOP: FLAG_CAN_LOAD_MORE_BOTTOM); + flags &= ~(direction == SEARCH_DIRECTION_TOP ? FLAG_CAN_LOAD_MORE_TOP : FLAG_CAN_LOAD_MORE_BOTTOM); if (currentMessage != null) { delegate.showSearchResult(getMessageIndex(currentMessage.id), currentTotalCount, knownIndex(), knownTotalCount(), new MessageId(currentMessage.chatId, currentMessage.id)); } @@ -319,11 +313,11 @@ public void moveToNext (boolean next) { if (0 <= nextIndex && nextIndex < currentSearchResultsArr.size()) { TdApi.Message message = getMessageByIndex(nextIndex); delegate.showSearchResult(getMessageIndex(currentDisplayedMessage = message.id), currentTotalCount, knownIndex(), knownTotalCount(), new MessageId(message.chatId, message.id)); - } else if (next ? canLoadTop(): canLoadBottom()) { + } else if (next ? canLoadTop() : canLoadBottom()) { flags |= FLAG_LOADING; delegate.onAwaitNext(next); searchInternal(contextId, currentChatId, currentMessageThreadId, currentFromSender, currentSearchFilter, currentIsSecret, currentInput, - currentDisplayedMessage, currentSecretOffset, next ? SEARCH_DIRECTION_TOP: SEARCH_DIRECTION_BOTTOM); + currentDisplayedMessage, currentSecretOffset, next ? SEARCH_DIRECTION_TOP : SEARCH_DIRECTION_BOTTOM); } } @@ -374,7 +368,7 @@ private void addAllMessages (TdApi.Message[] messages, int direction) { } else { currentSearchResults.addAll(0, Arrays.asList(messages)); } - for (TdApi.Message message: messages) { + for (TdApi.Message message : messages) { currentSearchResultsArr.append(message.id, message); } } @@ -385,7 +379,7 @@ private TdApi.Message getMessageByIndex (int index) { private int getMessageIndex (long msgId) { int index = currentSearchResultsArr.indexOfKey(msgId); - return (index < 0) ? -1: (currentSearchResultsArr.size() - 1 - index); + return (index < 0) ? -1 : (currentSearchResultsArr.size() - 1 - index); } private boolean knownIndex () { diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesSearchManagerMiddleware.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesSearchManagerMiddleware.java index 6281b6c864..829c3e3501 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesSearchManagerMiddleware.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesSearchManagerMiddleware.java @@ -17,8 +17,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.Objects; +import me.vkryl.core.ObjectUtils; import me.vkryl.core.StringUtils; import me.vkryl.td.Td; @@ -281,7 +281,7 @@ private TdApi.Message[] filterSearchRequestResult (TdApi.Message[] messages, Sen int discardedCount = 0; ArrayList filteredArr = new ArrayList<>(); - for (TdApi.Message message: messages) { + for (TdApi.Message message : messages) { final boolean isFiltered = manager.filter(message); if (isFiltered) { filteredArr.add(message); @@ -300,7 +300,7 @@ private TdApi.Message[] filterSearchRequestResult (TdApi.Message[] messages, Sen } private boolean isWasDiscardedBefore (long id) { - for (SendSearchRequestFilterChunkInfo part: filteredChunksInfo) { + for (SendSearchRequestFilterChunkInfo part : filteredChunksInfo) { if (part.isChunkPart(id)) return true; } @@ -348,13 +348,13 @@ public void search (TdApi.SearchSecretMessages query, @Nullable TdApi.MessageSen final String contextId = makeContextId(query, sender); checkContextId(contextId); - TdApi.FoundMessages cached = secretMessagesCache.get(query.offset != null ? query.offset: ""); + TdApi.FoundMessages cached = secretMessagesCache.get(query.offset != null ? query.offset : ""); if (cached != null) { resultHandler.onResult(cached); return; } - if (!Objects.equals(lastSecretNextOffset, query.offset)) { + if (!ObjectUtils.equals(lastSecretNextOffset, query.offset)) { TdApi.Error error = new TdApi.Error(400, "INCORRECT OFFSET"); UI.showError(error); resultHandler.onResult(error); @@ -461,7 +461,7 @@ public void dismiss () { } private boolean checkContextId (String contextId) { - if (!Objects.equals(contextId, currentContextId)) { + if (!ObjectUtils.equals(contextId, currentContextId)) { currentContextId = contextId; Log.i("SEARCH_MIDDLEWARE", "RESET"); reset(); @@ -487,14 +487,14 @@ private void reset () { private static TdApi.Function safeSearchSecretQuery (TdApi.SearchSecretMessages query) { final TdApi.SearchMessagesFilter safeFilter = safeFilter(query.filter); - final boolean hasMediaFilter = query.filter != null && safeFilter != null && safeFilter.getConstructor() != TdApi.SearchMessagesFilterEmpty.CONSTRUCTOR; + final boolean hasMediaFilter = query.filter != null && safeFilter != null && !Td.isEmptyFilter(safeFilter); final boolean queryIsEmpty = StringUtils.isEmpty(query.query); if (queryIsEmpty) { if (hasMediaFilter) { - return new TdApi.SearchChatMessages(query.chatId, query.query, null, !StringUtils.isEmpty(query.offset) ? Long.parseLong(query.offset): 0, 0, query.limit, safeFilter, 0); + return new TdApi.SearchChatMessages(query.chatId, query.query, null, !StringUtils.isEmpty(query.offset) ? Long.parseLong(query.offset) : 0, 0, query.limit, safeFilter, 0); } else { - return new TdApi.GetChatHistory(query.chatId, !StringUtils.isEmpty(query.offset) ? Long.parseLong(query.offset): 0, 0, query.limit, false); + return new TdApi.GetChatHistory(query.chatId, !StringUtils.isEmpty(query.offset) ? Long.parseLong(query.offset) : 0, 0, query.limit, false); } } @@ -506,7 +506,7 @@ private static TdApi.SearchSecretMessages cloneSearchSecretQuery (TdApi.SearchSe } private static TdApi.SearchChatMessages safeSearchChatQuery (TdApi.SearchChatMessages query, boolean withoutSenderId, boolean withoutFilter) { - return new TdApi.SearchChatMessages(query.chatId, query.query, withoutSenderId ? null : query.senderId, query.fromMessageId, query.offset, query.limit, withoutFilter ? null: safeFilter(query.filter), query.messageThreadId); + return new TdApi.SearchChatMessages(query.chatId, query.query, withoutSenderId ? null : query.senderId, query.fromMessageId, query.offset, query.limit, withoutFilter ? null : safeFilter(query.filter), query.messageThreadId); } private static TdApi.SearchChatMessages cloneSearchChatQuery (TdApi.SearchChatMessages query, long newFromMessageId, int newOffset) { diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/PinnedMessagesBar.java b/app/src/main/java/org/thunderdog/challegram/component/chat/PinnedMessagesBar.java index b09e0d2077..50f99f885a 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/PinnedMessagesBar.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/PinnedMessagesBar.java @@ -29,10 +29,14 @@ import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.attach.CustomItemAnimator; import org.thunderdog.challegram.config.Config; -import org.thunderdog.challegram.data.MessageListManager; +import org.thunderdog.challegram.helper.LinkPreview; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.ListManager; +import org.thunderdog.challegram.telegram.MessageListManager; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.DrawAlgorithms; @@ -40,11 +44,13 @@ import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.ui.ListItem; +import org.thunderdog.challegram.ui.MessagesController; import org.thunderdog.challegram.ui.SettingHolder; import org.thunderdog.challegram.ui.SettingsAdapter; import org.thunderdog.challegram.v.CustomRecyclerView; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import me.vkryl.android.AnimatorUtils; @@ -52,12 +58,14 @@ import me.vkryl.android.animator.FactorAnimator; import me.vkryl.core.ColorUtils; import me.vkryl.core.MathUtils; +import me.vkryl.core.StringUtils; import me.vkryl.core.lambda.Destroyable; import me.vkryl.td.MessageId; public class PinnedMessagesBar extends ViewGroup implements Destroyable, MessageListManager.ChangeListener, View.OnClickListener { private CustomRecyclerView recyclerView; private SettingsAdapter messagesAdapter; + private TdlibMessageViewer.Viewport messageViewport; private CollapseButton collapseButton; private TopBarView showAllButton; @@ -76,9 +84,15 @@ protected void onDraw (Canvas c) { private int lastKnownViewHeight; - public PinnedMessagesBar (@NonNull Context context) { + private final RecyclerView.ItemAnimator itemAnimator = new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); + + private final boolean reverseLayout; + + public PinnedMessagesBar (@NonNull Context context, boolean reverseLayout) { super(context); + this.reverseLayout = reverseLayout; + showAllButton = new TopBarView(context); showAllButton.setAlpha(0f); showAllButton.setCanDismiss(true); @@ -96,36 +110,48 @@ public PinnedMessagesBar (@NonNull Context context) { addView(showAllButton); recyclerView = (CustomRecyclerView) Views.inflate(context, R.layout.recycler_custom, null); - recyclerView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180l)); + recyclerView.setItemAnimator(itemAnimator); recyclerView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS : RecyclerView.OVER_SCROLL_NEVER); recyclerView.setVerticalScrollBarEnabled(false); Views.setScrollBarPosition(recyclerView); - recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)); + recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, reverseLayout)); recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { @Override public void onDrawOver (@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { - if (messagesAdapter.getItems().isEmpty()) + if (messagesAdapter.getItems().isEmpty()) { + setOverScrollDisabled(true); return; + } int viewportHeight = getRecyclerHeight(); int itemHeight = SettingHolder.measureHeightForType(ListItem.TYPE_MESSAGE_PREVIEW); int scrollItemCount = messageList != null ? messageList.getTotalCount() : messagesAdapter.getItems().size(); - if (scrollItemCount <= 0) + if (scrollItemCount <= 0) { + setOverScrollDisabled(true); return; + } - float alpha = 1f; - float focusPosition = getFocusPosition(); + final float alpha = 1f; // - final int color = ColorUtils.alphaColor(alpha, Theme.chatVerticalLineColor()); - final int backgroundColor = ColorUtils.alphaColor(.3f * alpha, color); + final int defaultFillColor = ColorUtils.alphaColor(alpha, Theme.chatVerticalLineColor()); + final int defaultBackgroundColor = ColorUtils.alphaColor(.3f * alpha, defaultFillColor); // final float visibleItemCount = Math.min(scrollItemCount, (float) viewportHeight / (float) itemHeight); + // + + float focusPosition = getFocusPosition(); + if (!reverseLayout) { + focusPosition = scrollItemCount - 1 - focusPosition; + } + + // + final int lineX = Screen.dp(10f); final float strokeWidth = Screen.dp(1.5f); final int outerSpacing = Screen.dp(6f); // spacing before the first and the last item @@ -146,6 +172,8 @@ public void onDrawOver (@NonNull Canvas c, @NonNull RecyclerView parent, @NonNul final int fromPosition = Math.max(0, (int) (focusPosition - Math.ceil(visibleLineCount * scrollFactor))); final int toPosition = Math.min(scrollItemCount, (int) Math.ceil(focusPosition) + visibleLineCount + 1); + setOverScrollDisabled(fromPosition == 0 && toPosition == scrollItemCount); + RectF rectF = Paints.getRectF(); float lineScrollY = Math.max(0, lineHeight * scrollItemCount + spacingBetweenItems * (scrollItemCount - 1) + outerSpacing * 2 - viewportHeight); @@ -156,20 +184,39 @@ public void onDrawOver (@NonNull Canvas c, @NonNull RecyclerView parent, @NonNul float lineEndY = lineBeginY - (float) Math.ceil((lineHeight + spacingBetweenItems) * position); float lineStartY = lineEndY - lineHeight; + int dataPosition; + if (reverseLayout) { + dataPosition = position; + } else { + dataPosition = scrollItemCount - 1 - position; + } + Entry entry = null; + if (messagesAdapter != null && dataPosition >= 0 && dataPosition < messagesAdapter.getItemCount()) { + entry = (Entry) messagesAdapter.getItems().get(dataPosition).getData(); + } + final int fillColor, backgroundColor; + if (entry != null && entry.accentColor != null) { + fillColor = ColorUtils.alphaColor(alpha, entry.accentColor.getVerticalLineColor()); + backgroundColor = ColorUtils.alphaColor(.3f * alpha, fillColor); + } else { + fillColor = defaultFillColor; + backgroundColor = defaultBackgroundColor; + } + boolean isFull = focusPosition == position || (position > (int) focusPosition && position < (int) (focusPosition + visibleItemCount)); rectF.set(lineStartX, lineStartY, lineEndX, lineEndY); - c.drawRoundRect(rectF, strokeWidth, strokeWidth, Paints.fillingPaint(isFull ? color : backgroundColor)); + c.drawRoundRect(rectF, strokeWidth, strokeWidth, Paints.fillingPaint(isFull ? fillColor : backgroundColor)); if (!isFull) { if (position == (int) focusPosition && focusPosition > position) { rectF.set(lineStartX, lineStartY, lineEndX, lineEndY + (lineStartY - lineEndY) * (focusPosition - (float) position)); - c.drawRoundRect(rectF, strokeWidth, strokeWidth, Paints.fillingPaint(color)); + c.drawRoundRect(rectF, strokeWidth, strokeWidth, Paints.fillingPaint(fillColor)); } else { float remain = focusPosition + visibleItemCount - position; if (remain > 0f && remain < 1f) { rectF.set(lineStartX, lineEndY + (lineStartY - lineEndY) * remain, lineEndX, lineEndY); - c.drawRoundRect(rectF, strokeWidth, strokeWidth, Paints.fillingPaint(color)); + c.drawRoundRect(rectF, strokeWidth, strokeWidth, Paints.fillingPaint(fillColor)); } } } @@ -201,6 +248,12 @@ public void onDrawOver (@NonNull Canvas c, @NonNull RecyclerView parent, @NonNul setWillNotDraw(false); } + private boolean ignoreAlbums; + + public void setIgnoreAlbums (boolean ignoreAlbums) { + this.ignoreAlbums = ignoreAlbums; + } + public void setCollapseButtonVisible (boolean isVisible) { collapseButton.setVisibility(isVisible ? View.VISIBLE : View.GONE); updateContentInsets(); @@ -209,10 +262,14 @@ public void setCollapseButtonVisible (boolean isVisible) { public void setAnimationsDisabled (boolean animationsDisabled) { if (this.animationsDisabled != animationsDisabled) { this.animationsDisabled = animationsDisabled; - recyclerView.setItemAnimator(animationsDisabled ? null : new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180l)); + recyclerView.setItemAnimator(animationsDisabled ? null : itemAnimator); } } + public boolean areAnimationsDisabled () { + return animationsDisabled; + } + @Override protected void onDraw (Canvas c) { c.drawRect(0, getRecyclerHeight(), getMeasuredWidth(), getMeasuredHeight(), Paints.fillingPaint(Theme.fillingColor())); @@ -231,7 +288,12 @@ private float getFocusPosition () { if (view instanceof MessagePreviewView) { int adapterPosition = recyclerView.getChildAdapterPosition(view); if (adapterPosition != RecyclerView.NO_POSITION) { - int scrollY = view.getBottom() - viewportHeight; + int scrollY; + if (reverseLayout) { + scrollY = view.getBottom() - viewportHeight; + } else { + scrollY = -view.getTop(); + } return adapterPosition + (float) scrollY / (float) itemHeight; } } @@ -254,6 +316,15 @@ private void updateContentInsets () { } } + private boolean overScrollDisabled; + + private void setOverScrollDisabled (boolean isDisabled) { + if (this.overScrollDisabled != isDisabled) { + this.overScrollDisabled = isDisabled; + recyclerView.setOverScrollMode(isDisabled ? RecyclerView.OVER_SCROLL_NEVER : RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS); + } + } + @CallSuper protected void onViewportChanged () { updateContentInsets(); @@ -288,15 +359,6 @@ private void checkHeight () { float expand = getExpandFactor(); if (expand == 1f || expand == 0f) { updateContentInsets(); - /*LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager(); - int first = manager.findFirstVisibleItemPosition(); - int last = manager.findLastCompletelyVisibleItemPosition(); - if (first > 0) { - messagesAdapter.notifyItemRangeChanged(0, first); - } - if (last < messagesAdapter.getItemCount()) { - messagesAdapter.notifyItemRangeChanged(last + 1, messagesAdapter.getItemCount() - (last + 1)); - }*/ } } } @@ -309,9 +371,15 @@ private float getExpandFactor () { protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { int newHeight = getTotalHeight(); super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY)); + int horizontalPadding = getPaddingLeft() + getPaddingRight(); + if (horizontalPadding != 0) { + int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec) - horizontalPadding; + widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); + } recyclerView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getRecyclerHeight(), MeasureSpec.EXACTLY)); - collapseButton.measure(MeasureSpec.makeMeasureSpec(Screen.dp(40f), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(MathUtils.fromTo(SettingHolder.measureHeightForType(ListItem.TYPE_MESSAGE_PREVIEW), Screen.dp(36f), getExpandFactor()), MeasureSpec.EXACTLY)); showAllButton.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Screen.dp(36f), MeasureSpec.EXACTLY)); + + collapseButton.measure(MeasureSpec.makeMeasureSpec(Screen.dp(40f), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(MathUtils.fromTo(SettingHolder.measureHeightForType(ListItem.TYPE_MESSAGE_PREVIEW), Screen.dp(36f), getExpandFactor()), MeasureSpec.EXACTLY)); if (newHeight != lastKnownViewHeight) { lastKnownViewHeight = newHeight; onViewportChanged(); @@ -320,6 +388,8 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { @Override protected void onLayout (boolean changed, int l, int t, int r, int b) { + l += getPaddingLeft(); + r -= getPaddingRight(); showAllButton.layout(l, b - Screen.dp(36f), r, b); recyclerView.layout(l, t, r, getRecyclerHeight()); collapseButton.layout(r - collapseButton.getMeasuredWidth(), b - collapseButton.getMeasuredHeight(), r, b); @@ -342,21 +412,47 @@ private void updateContentInset (MessagePreviewView view, int position, float fo } public void initialize (@NonNull ViewController viewController) { + messageViewport = viewController.tdlib().messageViewer().createViewport(new TdApi.MessageSourceSearch(), viewController); messagesAdapter = new SettingsAdapter(viewController, this, viewController) { + @Nullable + @Override + protected View createModifiedView (ViewGroup parent, int viewType, View view) { + if (viewType == ListItem.TYPE_MESSAGE_PREVIEW && messageListener != null) { + messageListener.onCreateMessagePreview(PinnedMessagesBar.this, (MessagePreviewView) view); + } + return null; + } + @Override protected void setMessagePreview (ListItem item, int position, MessagePreviewView previewView) { - TdApi.Message message = (TdApi.Message) item.getData(); - previewView.setMessage(message, new TdApi.SearchMessagesFilterPinned(), item.getStringValue(), false); - if (messageList == null) { - // override message preview - MessageId highlightMessageId; - //noinspection ConstantConditions - if (viewController.tdlib().isChannelAutoForward(message) && message.forwardInfo.fromChatId == contextChatId) { - highlightMessageId = new MessageId(message.forwardInfo.fromChatId, message.forwardInfo.fromMessageId); - } else { - highlightMessageId = new MessageId(message.chatId, message.id); + Entry data = (Entry) item.getData(); + if (data.isLinkPreview()) { + LinkPreview linkPreview = data.linkPreviewContext.getLinkPreview(data.linkPreviewUrl); + previewView.setLinkPreview(linkPreview, (v, currentLinkPreview) -> { + if (messageListener != null && messageListener.onToggleLargeMedia(PinnedMessagesBar.this, v, data.linkPreviewContext, currentLinkPreview)) { + v.updateShowSmallMedia(true); + } + }); + } else if (data.isMessage()) { + TdApi.Message message = data.message; + TdApi.InputTextQuote quote = data.quote; + previewView.setMessage(message, quote, new TdApi.SearchMessagesFilterPinned(), item.getStringValue(), ignoreAlbums ? MessagePreviewView.Options.IGNORE_ALBUM_REFRESHERS : MessagePreviewView.Options.NONE); + if (messageList == null) { + // override message preview + MessageId highlightMessageId; + //noinspection ConstantConditions + if (data.tdlib.isChannelAutoForward(message) && message.forwardInfo.fromChatId == contextChatId) { + highlightMessageId = new MessageId(message.forwardInfo.fromChatId, message.forwardInfo.fromMessageId); + } else { + highlightMessageId = new MessageId(message.chatId, message.id); + } + previewView.setPreviewChatId(null, highlightMessageId.getChatId(), null, highlightMessageId, null); } - previewView.setPreviewChatId(null, highlightMessageId.getChatId(), null, highlightMessageId, null); + if (messageListener != null) { + messageListener.onMessageDisplayed(PinnedMessagesBar.this, previewView, message); + } + } else { + throw new UnsupportedOperationException(); } updateContentInset(previewView, position); } @@ -380,12 +476,23 @@ public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newSta if (firstVisiblePosition != RecyclerView.NO_POSITION) { View view = manager.findViewByPosition(firstVisiblePosition); if (view != null) { - int offsetY = Math.max(0, view.getBottom() - recyclerView.getMeasuredHeight()); - if (offsetY != 0) { - if (offsetY > view.getMeasuredHeight() / 2) { - recyclerView.smoothScrollBy(0, offsetY - view.getMeasuredHeight()); - } else { - recyclerView.smoothScrollBy(0, offsetY); + if (reverseLayout) { + int offsetY = Math.max(0, view.getBottom() - recyclerView.getMeasuredHeight()); + if (offsetY != 0) { + if (offsetY > view.getMeasuredHeight() / 2) { + recyclerView.smoothScrollBy(0, offsetY - view.getMeasuredHeight()); + } else { + recyclerView.smoothScrollBy(0, offsetY); + } + } + } else { + int offsetY = Math.min(0, view.getTop()); + if (offsetY != 0) { + if (-offsetY > view.getMeasuredHeight() / 2) { + recyclerView.smoothScrollBy(0, view.getMeasuredHeight() + offsetY); + } else { + recyclerView.smoothScrollBy(0, offsetY); + } } } } @@ -396,14 +503,33 @@ public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newSta @Override public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager(); - if (messageList != null && dy != 0 && manager != null) { + if (manager != null) { int lastVisiblePosition = manager.findLastVisibleItemPosition(); int firstVisiblePosition = manager.findFirstVisibleItemPosition(); - if (lastVisiblePosition != RecyclerView.NO_POSITION && lastVisiblePosition + 15 >= messageList.getCount()) { - messageList.loadItems(false, null); - } else if (firstVisiblePosition != RecyclerView.NO_POSITION && firstVisiblePosition - 5 <= 0) { - messageList.loadItems(true, null); + if (messageList != null && dy != 0) { + if (lastVisiblePosition != RecyclerView.NO_POSITION && lastVisiblePosition + 15 >= messageList.getCount()) { + messageList.loadItems(false, null); + } else if (firstVisiblePosition != RecyclerView.NO_POSITION && firstVisiblePosition - 5 <= 0) { + messageList.loadItems(true, null); + } + } + + int focusIndex = firstVisiblePosition; + View view = manager.findViewByPosition(firstVisiblePosition); + if (view != null) { + if (reverseLayout) { + int offsetY = Math.max(0, view.getBottom() - recyclerView.getMeasuredHeight()); + if (offsetY > view.getMeasuredHeight() / 2) { + focusIndex++; + } + } else { + int offsetY = Math.min(0, view.getTop()); + if (-offsetY > view.getMeasuredHeight() / 2) { + focusIndex++; + } + } } + setFocusIndex(focusIndex); } float expand = getExpandFactor(); if (expand > 0f && expand < 1f) { @@ -411,16 +537,38 @@ public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { } } }); + viewController.tdlib().ui().attachViewportToRecyclerView(messageViewport, recyclerView); viewController.addThemeInvalidateListener(this); viewController.addThemeInvalidateListener(recyclerView); viewController.addThemeInvalidateListener(collapseButton); showAllButton.addThemeListeners(viewController); } + private int focusIndex = RecyclerView.NO_POSITION; + + private void setFocusIndex (int focusIndex) { + if (this.focusIndex != focusIndex) { + this.focusIndex = focusIndex; + ListItem item = messagesAdapter.getItem(focusIndex); + if (item != null) { + Entry entry = (Entry) item.getData(); + if (entry.isLinkPreview()) { + messageListener.onSelectLinkPreviewUrl(this, entry.linkPreviewContext, entry.linkPreviewUrl); + } + } + } + } + public interface MessageListener { - void onMessageClick (PinnedMessagesBar view, TdApi.Message message); - void onDismissRequest (PinnedMessagesBar view); - void onShowAllRequest (PinnedMessagesBar view); + void onMessageClick (PinnedMessagesBar view, TdApi.Message message, @Nullable TdApi.InputTextQuote quote); + default void onSelectLinkPreviewUrl (PinnedMessagesBar view, MessagesController.MessageInputContext messageContext, String url) { } + default boolean onToggleLargeMedia (PinnedMessagesBar view, MessagePreviewView previewView, MessagesController.MessageInputContext messageContext, LinkPreview linkPreview) { + return false; + } + default void onDismissRequest (PinnedMessagesBar view) { } + default void onShowAllRequest (PinnedMessagesBar view) { } + default void onCreateMessagePreview (PinnedMessagesBar view, MessagePreviewView previewView) { } + default void onMessageDisplayed (PinnedMessagesBar view, MessagePreviewView previewView, TdApi.Message message) { } } private MessageListener messageListener; @@ -433,9 +581,15 @@ public void setMessageListener (MessageListener messageListener) { public void onClick (View v) { if (v.getId() == R.id.message) { ListItem item = ((ListItem) v.getTag()); - TdApi.Message message = (TdApi.Message) item.getData(); + Entry entry = (Entry) item.getData(); if (messageListener != null) { - messageListener.onMessageClick(this, message); + if (entry.isLinkPreview()) { + messageListener.onSelectLinkPreviewUrl(this, entry.linkPreviewContext, entry.linkPreviewUrl); + } else if (entry.isMessage()) { + messageListener.onMessageClick(this, entry.message, entry.quote); + } else { + // TODO + } } } } @@ -468,22 +622,99 @@ private int getRecyclerHeight () { return itemHeight + Math.round((float) itemHeight * countAnimator.getFactor() * getExpandFactor()); } + public static class Entry implements Destroyable { + public final Tdlib tdlib; + + public final TdApi.Message message; + public final @Nullable TdApi.InputTextQuote quote; + public final TdlibAccentColor accentColor; + + public final MessagesController.MessageInputContext linkPreviewContext; + public final String linkPreviewUrl; + + public Entry (Tdlib tdlib, TdApi.Message message, @Nullable TdApi.InputTextQuote quote) { + this.tdlib = tdlib; + this.message = message; + this.linkPreviewContext = null; + this.linkPreviewUrl = null; + this.quote = quote; + this.accentColor = tdlib.messageAccentColor(message); + } + + public Entry (Tdlib tdlib, @NonNull MessagesController.MessageInputContext linkPreviewContext, @NonNull String linkPreviewUrl) { + this.tdlib = tdlib; + this.linkPreviewContext = linkPreviewContext; + this.linkPreviewUrl = linkPreviewUrl; + this.message = null; + this.accentColor = null; + this.quote = null; + } + + public boolean isLinkPreview () { + return linkPreviewUrl != null; + } + + public boolean isMessage () { + return message != null; + } + + public void subscribeToUpdates () { + // TODO subscribe to updates + } + + @Override + public void performDestroy () { + // TODO + } + + @Override + public boolean equals (@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Entry)) { + return false; + } + Entry b = (Entry) obj; + if (isMessage() != b.isMessage() || isLinkPreview() != b.isLinkPreview()) { + return false; + } + if (isLinkPreview()) { + return StringUtils.equalsTo(this.linkPreviewUrl, b.linkPreviewUrl) && (this.linkPreviewContext == b.linkPreviewContext); + } else if (isMessage()) { + return this.message.chatId == b.message.chatId && this.message.id == b.message.id; + } else { + throw new UnsupportedOperationException(); + } + } + } + private long contextChatId; - private @Nullable TdApi.Message message; + private @Nullable List staticList; private @Nullable MessageListManager messageList; public void collapse (boolean animated) { this.isExpanded.setValue(false, animated); } - public void setMessageList (@Nullable MessageListManager messageList) { - if (this.messageList == messageList) - return; + private void clearMessageList () { + if (this.staticList != null) { + for (Entry entry : staticList) { + entry.performDestroy(); + } + this.staticList = null; + } if (this.messageList != null) { this.messageList.removeChangeListener(this); this.messageList = null; } - this.message = null; + this.focusIndex = RecyclerView.NO_POSITION; + } + + public void setMessageList (@Nullable MessageListManager messageList) { + if (this.messageList == messageList) + return; + clearMessageList(); this.messageList = messageList; collapse(false); canExpand.setValue(messageList != null && messageList.getTotalCount() > 1, false); @@ -492,7 +723,7 @@ public void setMessageList (@Nullable MessageListManager messageList) { messageList.addChangeListener(this); List items = new ArrayList<>(messageList.getCount()); for (TdApi.Message message : messageList) { - items.add(itemOf(message)); + items.add(itemOf(messageList.tdlib(), message)); } messagesAdapter.setItems(items, false); messageList.loadInitialChunk(null); @@ -501,22 +732,56 @@ public void setMessageList (@Nullable MessageListManager messageList) { } } - public void setMessage (@Nullable TdApi.Message message) { - if (this.message == message) { + public void setMessage (@Nullable Tdlib tdlib, @Nullable TdApi.Message message) { + setMessage(tdlib, message, null); + } + + public void setStaticMessageList (@Nullable List entries, int scrollToPosition) { + if (entries == null || entries.isEmpty()) { + setMessageList(null); return; } - if (this.messageList != null) { - this.messageList.removeChangeListener(this); - this.messageList = null; + if (this.staticList != null && this.staticList.size() == entries.size()) { + boolean contentEquals = true; + for (int i = 0; i < entries.size(); i++) { + Entry oldEntry = this.staticList.get(i); + Entry newEntry = entries.get(i); + // TODO better check + if (!oldEntry.equals(newEntry)) { + contentEquals = false; + break; + } + } + if (contentEquals) { + if (scrollToPosition != RecyclerView.NO_POSITION) { + LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager(); + // TODO smooth? + manager.scrollToPositionWithOffset(scrollToPosition, 0); + } + return; + } } - this.message = message; + clearMessageList(); collapse(false); canExpand.setValue(false, false); countAnimator.forceFactor(0); - if (message != null) { - messagesAdapter.setItems(new ListItem[] {itemOf(message)}, false); + List items = new ArrayList<>(entries.size()); + for (Entry entry : entries) { + entry.subscribeToUpdates(); + items.add(itemOf(entry)); + } + messagesAdapter.setItems(items, false); + if (scrollToPosition != RecyclerView.NO_POSITION) { + LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager(); + manager.scrollToPositionWithOffset(scrollToPosition, 0); + } + } + + public void setMessage (@Nullable Tdlib tdlib, @Nullable TdApi.Message message, @Nullable TdApi.InputTextQuote quote) { + if (tdlib != null && message != null) { + setStaticMessageList(Collections.singletonList(new Entry(tdlib, message, quote)), RecyclerView.NO_POSITION); } else { - messagesAdapter.setItems(new ListItem[0], false); + setMessageList(null); } } @@ -534,13 +799,22 @@ public void setMaxFocusMessageId (long maxFocusMessageId) { } } - private static ListItem itemOf (TdApi.Message message) { - return new ListItem(ListItem.TYPE_MESSAGE_PREVIEW, R.id.message).setData(message); + private static ListItem itemOf (@NonNull Entry entry) { + return new ListItem(ListItem.TYPE_MESSAGE_PREVIEW, R.id.message).setData(entry); + } + + private static ListItem itemOf (Tdlib tdlib, TdApi.Message message) { + return new ListItem(ListItem.TYPE_MESSAGE_PREVIEW, R.id.message).setData(new Entry(tdlib, message, null)); } @Override public void performDestroy () { setMessageList(null); + messageViewport.clear(); + } + + public void completeDestroy () { + messageViewport.performDestroy(); } @Override @@ -548,7 +822,7 @@ public void onItemsAdded (ListManager list, List i ListItem[] messageList = new ListItem[items.size()]; int index = 0; for (TdApi.Message message : items) { - messageList[index] = itemOf(message); + messageList[index] = itemOf(list.tdlib(), message); index++; } if (needsClear || messagesAdapter.getItems().isEmpty()) { @@ -564,11 +838,11 @@ public void onItemAdded (ListManager list, TdApi.Message item, in if (needsClear || messagesAdapter.getItems().isEmpty()) { needsClear = false; messagesAdapter.setItems(new ListItem[] { - itemOf(item) + itemOf(list.tdlib(), item) }, false); } else { boolean scrollBy = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition() == 0; - messagesAdapter.addItem(toIndex, itemOf(item)); + messagesAdapter.addItem(toIndex, itemOf(list.tdlib(), item)); if (scrollBy) { ((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(0, 0); } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/RaiseHelper.java b/app/src/main/java/org/thunderdog/challegram/component/chat/RaiseHelper.java index 45f38b7646..517ba86c4f 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/RaiseHelper.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/RaiseHelper.java @@ -398,7 +398,7 @@ private void processRaiseMode () { if (inRaise) { BaseActivity context = UI.getUiContext(); inRaise = !TdlibManager.instance().calls().hasActiveCall(); - if (inRaise && !this.inRaiseMode && (context != null && context.getActivityState() == UI.STATE_RESUMED && !context.isActivityBusyWithSomething())) { + if (inRaise && !this.inRaiseMode && (context != null && context.getActivityState() == UI.State.RESUMED && !context.isActivityBusyWithSomething())) { ViewController c = context.navigation().getCurrentStackItem(); inRaise = c instanceof MessagesController && ((MessagesController) c).isEmojiInputEmpty(); } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/ReplyBarView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/ReplyBarView.java new file mode 100644 index 0000000000..587a64d447 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/ReplyBarView.java @@ -0,0 +1,289 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 21/02/2016 at 21:08 + */ +package org.thunderdog.challegram.component.chat; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.helper.FoundUrls; +import org.thunderdog.challegram.helper.LinkPreview; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.ui.MessagesController; +import org.thunderdog.challegram.widget.LinkPreviewToggleView; + +import java.util.ArrayList; +import java.util.List; + +import me.vkryl.android.ViewUtils; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.lambda.Destroyable; +import me.vkryl.core.lambda.RunnableData; + +public class ReplyBarView extends FrameLayoutFix implements View.OnClickListener, Destroyable { + protected final Tdlib tdlib; + private Callback callback; + + public ReplyBarView (Context context, Tdlib tdlib) { + super(context); + this.tdlib = tdlib; + } + + @Override + public void onClick (View v) { + if (callback != null) { + final int id = v.getId(); + if (id == R.id.btn_close) { + callback.onDismissReplyBar(this); + } + } + } + + ImageView closeView; + LinkPreviewToggleView linkPreviewToggleView; + + public void checkRtl () { + if (Views.setGravity(closeView, Lang.gravity())) { + Views.updateLayoutParams(closeView); + } + if (Views.setGravity(closeView, Lang.reverseGravity())) { + Views.updateLayoutParams(closeView); + } + invalidate(); + } + + private PinnedMessagesBar pinnedMessagesBar; + + public void initWithCallback (Callback callbacK, ViewController themeProvider) { + this.callback = callbacK; + themeProvider.addThemeInvalidateListener(this); + + FrameLayoutFix.LayoutParams params; + + pinnedMessagesBar = new PinnedMessagesBar(getContext(), false); + pinnedMessagesBar.setPadding(Screen.dp(49.5f), 0, 0, 0); + pinnedMessagesBar.setCollapseButtonVisible(false); + pinnedMessagesBar.setIgnoreAlbums(true); + pinnedMessagesBar.setMessageListener(new PinnedMessagesBar.MessageListener() { + @Override + public void onCreateMessagePreview (PinnedMessagesBar view, MessagePreviewView previewView) { + ViewUtils.setBackground(previewView, null); + // previewView.setLinePadding(4f); + } + + @Override + public void onMessageDisplayed (PinnedMessagesBar view, MessagePreviewView previewView, TdApi.Message message) { + previewView.clearPreviewChat(); + } + + @Override + public void onSelectLinkPreviewUrl (PinnedMessagesBar view, MessagesController.MessageInputContext messageContext, String url) { + if (callback != null) { + callback.onSelectLinkPreviewUrl(ReplyBarView.this, messageContext, url); + } + updateLinkPreviewSettings(true); + } + + @Override + public boolean onToggleLargeMedia (PinnedMessagesBar view, MessagePreviewView previewView, MessagesController.MessageInputContext messageContext, LinkPreview linkPreview) { + if (callback != null && callback.onRequestToggleLargeMedia(ReplyBarView.this, previewView, messageContext, linkPreview)) { + updateLinkPreviewSettings(true); + return true; + } + return false; + } + + @Override + public void onMessageClick (PinnedMessagesBar view, TdApi.Message message, @Nullable TdApi.InputTextQuote quote) { + if (callback != null) { + callback.onMessageHighlightRequested(ReplyBarView.this, message, quote); + } + } + }); + pinnedMessagesBar.setAnimationsDisabled(animationsDisabled); + pinnedMessagesBar.initialize(themeProvider); + addView(pinnedMessagesBar); + + params = FrameLayoutFix.newParams(Screen.dp(56f), ViewGroup.LayoutParams.MATCH_PARENT); + params.gravity = Lang.gravity(); + closeView = newButton(R.id.btn_close, R.drawable.baseline_close_24, themeProvider); + closeView.setLayoutParams(params); + addView(closeView); + + params = FrameLayoutFix.newParams(Screen.dp(56f), ViewGroup.LayoutParams.MATCH_PARENT); + params.gravity = Lang.reverseGravity(); + linkPreviewToggleView = new LinkPreviewToggleView(getContext()); + linkPreviewToggleView.setLayoutParams(params); + linkPreviewToggleView.setVisibility(View.GONE); + linkPreviewToggleView.addThemeListeners(themeProvider); + linkPreviewToggleView.setOnClickListener(v -> { + if (callback != null && inputContext != null && callback.onRequestToggleShowAbove(ReplyBarView.this, v, inputContext)) { + updateLinkPreviewSettings(true); + } + }); + Views.setClickable(linkPreviewToggleView); + linkPreviewToggleView.setBackgroundResource(R.drawable.bg_btn_header); + addView(linkPreviewToggleView); + } + + private boolean animationsDisabled; + + public void setAnimationsDisabled (boolean animationsDisabled) { + if (this.animationsDisabled != animationsDisabled) { + this.animationsDisabled = animationsDisabled; + if (pinnedMessagesBar != null) { + pinnedMessagesBar.setAnimationsDisabled(animationsDisabled); + } + } + } + + public boolean areAnimationsDisabled () { + return animationsDisabled; + } + + private ImageView newButton (@IdRes int idRes, @DrawableRes int iconRes, ViewController themeProvider) { + ImageView btn = new ImageView(getContext()); + btn.setId(idRes); + btn.setImageResource(iconRes); + btn.setColorFilter(Theme.iconColor()); + themeProvider.addThemeFilterListener(btn, ColorId.icon); + btn.setScaleType(ImageView.ScaleType.CENTER); + btn.setOnClickListener(this); + Views.setClickable(btn); + btn.setBackgroundResource(R.drawable.bg_btn_header); + return btn; + } + + private void updateLinkPreviewSettings (boolean animated) { + LinkPreview linkPreview = inputContext != null ? inputContext.getSelectedLinkPreview() : null; + setPendingLinkPreview(linkPreview != null && linkPreview.isLoading() ? linkPreview : null); + if (linkPreview != null) { + TdApi.LinkPreviewOptions options = inputContext.takeOutputLinkPreviewOptions(false); + @LinkPreviewToggleView.MediaVisibility int mediaState; + if (!linkPreview.hasMedia()) { + mediaState = LinkPreviewToggleView.MediaVisibility.NONE; + } else { + mediaState = linkPreview.getOutputShowLargeMedia() ? LinkPreviewToggleView.MediaVisibility.LARGE : LinkPreviewToggleView.MediaVisibility.SMALL; + } + linkPreviewToggleView.setMediaVisibility(mediaState, animated); + linkPreviewToggleView.setShowAboveText(options.showAboveText, animated); + } + } + + private void setLinkPreviewToggleVisible (boolean isVisible) { + if (isVisible != (linkPreviewToggleView.getVisibility() == View.VISIBLE)) { + linkPreviewToggleView.setVisibility(isVisible ? View.VISIBLE : View.GONE); + pinnedMessagesBar.setPadding(Screen.dp(49.5f), 0, isVisible ? Screen.dp(49.5f) : 0, 0); + } + } + + private MessagesController.MessageInputContext inputContext; + private LinkPreview pendingLinkPreview; + + private final RunnableData onLinkPreviewLoadListener = linkPreview -> { + if (linkPreview == pendingLinkPreview) { + updateLinkPreviewSettings(true); + } + }; + + private void setPendingLinkPreview (LinkPreview linkPreview) { + if (this.pendingLinkPreview != linkPreview) { + if (this.pendingLinkPreview != null) { + this.pendingLinkPreview.removeLoadCallback(onLinkPreviewLoadListener); + } + this.pendingLinkPreview = linkPreview; + if (linkPreview != null) { + linkPreview.addLoadCallback(onLinkPreviewLoadListener); + } + } + } + + private void setMessageInputContext (MessagesController.MessageInputContext context) { + if (this.inputContext != context) { + if (this.inputContext != null) { + setPendingLinkPreview(null); + } + this.inputContext = context; + if (context != null) { + updateLinkPreviewSettings(false); + } else { + setPendingLinkPreview(null); + } + } + } + + public void showWebPage (@NonNull MessagesController.MessageInputContext context, int selectedUrlIndex) { + FoundUrls foundUrls = context.getFoundUrls(); + List entryList = new ArrayList<>(); + for (String url : foundUrls.urls) { + entryList.add(new PinnedMessagesBar.Entry(tdlib, context, url)); + } + pinnedMessagesBar.setStaticMessageList(entryList, selectedUrlIndex); + setLinkPreviewToggleVisible(true); + setMessageInputContext(context); + } + + public void setReplyTo (TdApi.Message msg, @Nullable TdApi.InputTextQuote quote) { + pinnedMessagesBar.setMessage(tdlib, msg, quote); + setLinkPreviewToggleVisible(false); + setMessageInputContext(null); + } + + public void setEditingMessage (TdApi.Message msg) { + pinnedMessagesBar.setMessage(tdlib, msg); + setLinkPreviewToggleVisible(false); + setMessageInputContext(null); + } + + public void reset () { + performDestroy(); + } + + @Override + public void performDestroy () { + pinnedMessagesBar.performDestroy(); + setMessageInputContext(null); + } + + public void completeDestroy () { + pinnedMessagesBar.completeDestroy(); + } + + public LinkPreviewToggleView getLinkPreviewToggleView () { + return linkPreviewToggleView; + } + + public interface Callback { + void onDismissReplyBar (ReplyBarView view); + void onMessageHighlightRequested (ReplyBarView view, TdApi.Message message, @Nullable TdApi.InputTextQuote quote); + void onSelectLinkPreviewUrl (ReplyBarView view, MessagesController.MessageInputContext messageContext, String url); + boolean onRequestToggleShowAbove (ReplyBarView view, View buttonView, MessagesController.MessageInputContext messageContext); + boolean onRequestToggleLargeMedia (ReplyBarView view, View buttonView, MessagesController.MessageInputContext messageContext, LinkPreview linkPreview); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/ReplyComponent.java b/app/src/main/java/org/thunderdog/challegram/component/chat/ReplyComponent.java index 9b98946624..870664bf89 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/ReplyComponent.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/ReplyComponent.java @@ -17,24 +17,28 @@ import android.graphics.Canvas; import android.graphics.Path; import android.graphics.RectF; +import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Looper; import android.view.MotionEvent; import android.view.View; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Background; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.ContentPreview; import org.thunderdog.challegram.data.MediaWrapper; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; +import org.thunderdog.challegram.data.TGWebPage; +import org.thunderdog.challegram.helper.InlineSearchContext; import org.thunderdog.challegram.loader.ComplexReceiver; import org.thunderdog.challegram.loader.DoubleImageReceiver; import org.thunderdog.challegram.loader.ImageFile; @@ -42,12 +46,14 @@ import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.Tdlib; -import org.thunderdog.challegram.telegram.TdlibSender; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.DrawAlgorithms; +import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.util.text.Text; import org.thunderdog.challegram.util.text.TextColorSet; @@ -66,39 +72,26 @@ public class ReplyComponent implements Client.ResultHandler, Destroyable { private static final int FLAG_ALLOW_TOUCH_EVENTS = 1 << 1; private static final int FLAG_FORCE_TITLE = 1 << 3; private static final int FLAG_LOADING = 1 << 4; - private static final int FLAG_PINNED_MESSAGE = 1 << 5; - private static final int FLAG_USE_COLORIZE = 1 << 7; + private static final int FLAG_USE_COLORIZE = 1 << 5; private final Tdlib tdlib; - private final @Nullable TGMessage parent; + private final @NonNull TGMessage parent; private TdApi.MessageSender sender; private String senderName; - private @ColorId int nameColorId; + private TdlibAccentColor accentColor; private int maxWidth; private int flags; private String title; - private TD.ContentPreview content; - private ImageFile miniThumbnail; - private ImageFile preview; - private Path contour; - private boolean hasSpoiler; - private boolean previewCircle; + private ContentPreview content; + private MediaPreview mediaPreview; private Text trimmedTitle, trimmedContent; private ViewProvider viewProvider; - public ReplyComponent (@NonNull Tdlib tdlib) { - if (lineWidth == 0) { - initSizes(); - } - this.tdlib = tdlib; - this.parent = null; - } - public ReplyComponent (@NonNull TGMessage message) { if (lineWidth == 0) { initSizes(); @@ -125,6 +118,10 @@ public String getAuthor () { return sender != null ? tdlib.senderName(sender) : senderName; } + public TdApi.MessageSender getSender () { + return sender; + } + public void layout (int maxWidth) { if (this.maxWidth != maxWidth && maxWidth > 0) { this.maxWidth = maxWidth; @@ -132,16 +129,6 @@ public void layout (int maxWidth) { } } - public void postLayout (final int maxWidth) { - Background.instance().post(() -> { - if (ReplyComponent.this.maxWidth != maxWidth) { - ReplyComponent.this.maxWidth = maxWidth; - buildLayout(); - invalidate(false); - } - }); - } - public void setCurrentView (@Nullable View view) { setViewProvider(new SingleViewProvider(view)); } @@ -160,7 +147,7 @@ public void invalidate (boolean requestImage) { } viewProvider.invalidate(); } - + private boolean isMessageComponent () { return parent != null; } @@ -180,8 +167,17 @@ private void buildLayout () { } } - private int getContentWidth () { - return !hasPreview() ? maxWidth - paddingLeft - lineWidth - paddingLeft : maxWidth - paddingLeft - paddingLeft - lineWidth - lineWidth - (isMessageComponent() ? mHeight : height); + private static final float CORNER_PADDING = 1f; + + private int getContentWidth (boolean isTitle) { + int maxWidth = this.maxWidth - paddingLeft - lineWidth - paddingLeft; + if (hasPreview()) { + maxWidth -= lineWidth + (isMessageComponent() ? mHeight : height); + } + if (isTitle && cornerDrawable != null) { + maxWidth -= cornerDrawable.getMinimumWidth() + Screen.dp(CORNER_PADDING); + } + return maxWidth; } public int width () { @@ -189,7 +185,14 @@ public int width () { } public int width (boolean isWhite) { - return (!hasPreview() ? Math.max(getTextWidth(), getTitleWidth()) + paddingLeft + paddingLeft + lineWidth : Math.max(getTextWidth(), getTitleWidth()) + paddingLeft + lineWidth + lineWidth + paddingLeft + (isMessageComponent() ? mHeight : height)) + (isWhite ? Screen.dp(3f) : 0); + int contentWidth = Math.max(getTextWidth(), getTitleWidth() + (cornerDrawable != null ? cornerDrawable.getMinimumWidth() + Screen.dp(CORNER_PADDING) : 0)) + paddingLeft + paddingLeft + lineWidth; + if (hasPreview()) { + contentWidth += lineWidth + (isMessageComponent() ? mHeight : height); + } + if (isWhite) { + contentWidth += Screen.dp(3f); + } + return contentWidth; } private int getTextWidth () { @@ -209,19 +212,19 @@ private static TextStyleProvider getTextStyleProvider () { } private void buildTitle () { - int width = getContentWidth(); - String titleText = title != null ? title : Lang.getString(isChannel() ? R.string.LoadingChannel : R.string.LoadingUser); + int width = getContentWidth(true); + String titleText = computeTitleText(Lang.getString(isChannel() ? R.string.LoadingChannel : R.string.LoadingUser)); trimmedTitle = new Text.Builder(titleText, width, getTitleStyleProvider(isMessageComponent()), getTitleColorSet()) .singleLine() .clipTextArea() - .allBold() + .allBold(sender != null || StringUtils.isEmpty(senderName)) .allClickable() .viewProvider(viewProvider) .build(); } private void buildContent () { - int width = getContentWidth(); + int width = getContentWidth(false); //noinspection UnsafeOptInUsageError Text trimmedContent = new Text.Builder(content != null ? content.buildText(true) : Lang.getString(R.string.LoadingMessage), width, isMessageComponent() ? TGMessage.getTextStyleProvider() : getTextStyleProvider(), getContentColorSet()) @@ -234,7 +237,7 @@ private void buildContent () { if (isMessageComponent()) { parent.invalidateReplyTextMediaReceiver(text, specificMedia); } else { - viewProvider.performWithViews((view) -> { + /*viewProvider.performWithViews((view) -> { if (view instanceof ReplyView) { ComplexReceiver receiver = ((ReplyView) view).getTextMediaReceiver(); if (!text.invalidateMediaContent(receiver, specificMedia)) { @@ -243,7 +246,7 @@ private void buildContent () { } else { throw new UnsupportedOperationException(); } - }); + });*/ } } ) @@ -273,8 +276,10 @@ public void requestTextContent (ComplexReceiver textMediaReceiver) { public void requestPreview (DoubleImageReceiver receiver, ComplexReceiver textMediaReceiver) { int imageHeight = isMessageComponent() ? mHeight : height; - receiver.setRadius(previewCircle ? imageHeight / 2f : 0); - receiver.requestFile(miniThumbnail, preview); + if (mediaPreview != null) { + receiver.setRadius(mediaPreview.previewCircle ? imageHeight / 2f : 0); + receiver.requestFile(mediaPreview.miniThumbnail, mediaPreview.preview); + } requestTextContent(textMediaReceiver); } @@ -306,7 +311,7 @@ public boolean isInside (float x, float y, boolean needWhite) { } private boolean hasPreview () { - return miniThumbnail != null || preview != null; + return mediaPreview != null && (mediaPreview.miniThumbnail != null || mediaPreview.preview != null); } private int getTextLeft () { @@ -317,15 +322,34 @@ private TextColorSet getTitleColorSet () { if (parent != null) { if (parent.separateReplyFromContent()) return parent::getBubbleMediaReplyTextColor; - if (nameColorId != 0) - return () -> Theme.getColor(nameColorId); if (isPsa()) return parent.getChatAuthorPsaColorSet(); + if (useAccentColor()) + return accentColorSet(); return parent.getChatAuthorColorSet(); } + if (useAccentColor()) + return accentColorSet(); return TextColorSets.Regular.NORMAL; } + private TextColorSet accentColorSet () { + if (accentColor == null) { + throw new IllegalStateException(); + } + return new TextColorSet() { + @Override + public int defaultTextColor () { + return accentColor.getNameColor(); + } + + @Override + public long mediaTextComplexColor () { + return accentColor.getNameComplexColor(); + } + }; + } + private TextColorSet getContentColorSet () { if (parent != null) { return parent.separateReplyFromContent() ? new TextColorSet() { @@ -366,6 +390,14 @@ public int clickableTextColor (boolean isPressed) { }; } + public int getLastX () { + return lastX; + } + + public int getLastY () { + return lastY; + } + public void draw (Canvas c, int startX, int startY, int endX, int width, Receiver receiver, ComplexReceiver textMediaReceiver, boolean rtl) { boolean isWhite = parent != null && parent.separateReplyFromContent(); @@ -373,6 +405,16 @@ public void draw (Canvas c, int startX, int startY, int endX, int width, Receive lastY = startY; final boolean isOutBubble = isOutBubble(); + final @ColorInt int lineColor; + if (isMessageComponent()) { + lineColor = isWhite ? parent.getBubbleMediaReplyTextColor() : isOutBubble ? Theme.getColor(ColorId.bubbleOut_chatVerticalLine) : useAccentColor() ? accentColor.getVerticalLineColor() : Theme.getColor(ColorId.messageVerticalLine); + } else { + lineColor = useAccentColor() ? accentColor.getVerticalLineColor() : Theme.getColor(ColorId.messageVerticalLine); + } + + float cornerPositionX, cornerPositionY; + cornerPositionX = startX + width; + cornerPositionY = startY; RectF rectF = Paints.getRectF(); if (isWhite) { @@ -386,6 +428,11 @@ public void draw (Canvas c, int startX, int startY, int endX, int width, Receive c.drawRoundRect(rectF, mergeRadius, mergeRadius, Paints.fillingPaint(parent.getBubbleMediaReplyBackgroundColor())); } + if (cornerDrawable != null) { + // TODO: optimize + Drawables.draw(c, cornerDrawable, cornerPositionX - cornerDrawable.getMinimumWidth(), cornerPositionY, Paints.getPorterDuffPaint(lineColor)); + } + if (hasPreview()) { int imageX = lineWidth + lineWidth; if (rtl) { @@ -405,14 +452,14 @@ public void draw (Canvas c, int startX, int startY, int endX, int width, Receive } else { restoreToCount = Integer.MIN_VALUE; } - if (receiver.needPlaceholder()) { - receiver.drawPlaceholderContour(c, contour); + if (receiver.needPlaceholder() && mediaPreview != null) { + receiver.drawPlaceholderContour(c, mediaPreview.contour); } receiver.draw(c); - if (Config.DEBUG_STICKER_OUTLINES) { - receiver.drawPlaceholderContour(c, contour); + if (Config.DEBUG_STICKER_OUTLINES && mediaPreview != null) { + receiver.drawPlaceholderContour(c, mediaPreview.contour); } - if (hasSpoiler) { + if (mediaPreview != null && mediaPreview.hasSpoiler) { float radius = Theme.getBubbleMergeRadius(); DrawAlgorithms.drawRoundRect(c, radius, receiver.getLeft(), receiver.getTop(), receiver.getRight(), receiver.getBottom(), Paints.fillingPaint(Theme.getColor(ColorId.spoilerMediaOverlay))); DrawAlgorithms.drawParticles(c, radius, receiver.getLeft(), receiver.getTop(), receiver.getRight(), receiver.getBottom(), 1f); @@ -436,7 +483,7 @@ public void draw (Canvas c, int startX, int startY, int endX, int width, Receive } else { rectF.set(startX, startY, startX + lineWidth, startY + mHeight); } - c.drawRoundRect(rectF, lineWidth / 2f, lineWidth / 2f, Paints.fillingPaint(isWhite ? parent.getBubbleMediaReplyTextColor() : isOutBubble ? Theme.getColor(ColorId.bubbleOut_chatVerticalLine) : nameColorId != 0 ? Theme.getColor(nameColorId) : Theme.getColor(ColorId.messageVerticalLine))); + c.drawRoundRect(rectF, lineWidth / 2f, lineWidth / 2f, Paints.fillingPaint(lineColor)); return; } @@ -452,7 +499,7 @@ public void draw (Canvas c, int startX, int startY, int endX, int width, Receive } Paints.getRectF().set(startX, startY, startX + lineWidth, startY + height); - c.drawRoundRect(Paints.getRectF(), lineWidth / 2f, lineWidth / 2f, Paints.fillingPaint(Theme.getColor(ColorId.messageVerticalLine))); + c.drawRoundRect(Paints.getRectF(), lineWidth / 2f, lineWidth / 2f, Paints.fillingPaint(lineColor)); } // Data workers @@ -467,22 +514,16 @@ private CharSequence getTitle (@Nullable CharSequence forcedTitle, @Nullable TdA } public void set (@Nullable CharSequence forcedTitle, @NonNull TdApi.Message msg) { + this.accentColor = tdlib.senderAccentColor(msg.senderId); setTitleImpl(getTitle(forcedTitle, msg)); setMessage(msg, false, false); } - public void set (@Nullable CharSequence forcedTitle, TdApi.Message msg, boolean isPinnedMessage) { - setTitleImpl(getTitle(forcedTitle, msg)); - this.flags = BitwiseUtils.setFlag(flags, FLAG_FORCE_TITLE, !StringUtils.isEmpty(forcedTitle)); - this.flags = BitwiseUtils.setFlag(flags, FLAG_PINNED_MESSAGE, isPinnedMessage); - setMessage(msg, false, false); - } - public boolean onTouchEvent (View view, MotionEvent e) { return (flags & FLAG_ALLOW_TOUCH_EVENTS) != 0 && trimmedContent != null && trimmedContent.onTouchEvent(view, e); } - public void set (String title, TD.ContentPreview content, TdApi.Minithumbnail miniThumbnail, TdApi.File file) { + public void set (String title, ContentPreview content, TdApi.Minithumbnail miniThumbnail, TdApi.File file) { ImageFile image; if (file == null || TD.isFileEmpty(file)) { image = null; @@ -503,50 +544,100 @@ public void set (String title, TD.ContentPreview content, TdApi.Minithumbnail mi preview = null; } - setContent(title, content, false, null, preview, image, false, false); + setContent(title, content, new MediaPreview(preview, image, null, false, false), false); } - private @Nullable TdApi.Function retryFunction; + private @Nullable TdApi.Function retryFunction; + private boolean ignoreFailures; + private @Nullable TdApi.TextQuote quote; + private Drawable cornerDrawable; public void load () { if (parent == null) return; TdApi.Message message = parent.getMessage(); - if (message.chatId == message.replyInChatId && message.replyToMessageId != 0) { - TdApi.Message foundMessage = parent.manager().getAdapter().tryFindMessage(message.replyInChatId, message.replyToMessageId); - if (foundMessage != null) { - setMessage(foundMessage, false, true); - return; - } + if (message.replyTo == null) { + return; } - flags |= FLAG_LOADING; - final TdApi.Function function; - if (message.forwardInfo != null && message.forwardInfo.fromChatId != 0 && message.forwardInfo.fromMessageId != 0 && !parent.isRepliesChat()) { - function = new TdApi.GetRepliedMessage(message.forwardInfo.fromChatId, message.forwardInfo.fromMessageId); - } else { - function = new TdApi.GetRepliedMessage(message.chatId, message.id); + final TdApi.Function function, retryFunction; + switch (message.replyTo.getConstructor()) { + case TdApi.MessageReplyToMessage.CONSTRUCTOR: { + TdApi.MessageReplyToMessage replyToMessage = (TdApi.MessageReplyToMessage) message.replyTo; + if (!Td.isEmpty(replyToMessage.quote) && replyToMessage.quote.isManual) { + cornerDrawable = Drawables.load(R.drawable.baseline_format_quote_close_18); + } else { + cornerDrawable = null; + } + if (replyToMessage.origin != null) { + handleOrigin(replyToMessage.origin); + } + if (!Td.isEmpty(replyToMessage.quote) || replyToMessage.content != null) { + this.quote = replyToMessage.quote; + TdApi.Message fakeMessage = TD.newFakeMessage(replyToMessage.chatId, sender, replyToMessage.content == null ? new TdApi.MessageText(replyToMessage.quote.text, null, null) : replyToMessage.content); + fakeMessage.id = replyToMessage.messageId; + + ContentPreview contentPreview = ContentPreview.getChatListPreview(tdlib, fakeMessage.chatId, fakeMessage, true); + if (!Td.isEmpty(replyToMessage.quote)) { + contentPreview = new ContentPreview(contentPreview, replyToMessage.quote.text); + } + MediaPreview mediaPreview = replyToMessage.content != null ? newMediaPreview(replyToMessage.chatId, replyToMessage.content) : null; + this.content = new ContentPreview(translatedText, contentPreview); + setTitleImpl(computeTitleText(null)); + this.mediaPreview = mediaPreview; + buildLayout(); + invalidate(mediaPreview != null); + this.ignoreFailures = true; + } else if (message.chatId == replyToMessage.chatId) { + TdApi.Message foundMessage = parent.manager().getAdapter().tryFindMessage(replyToMessage.chatId, replyToMessage.messageId); + if (foundMessage != null) { + setMessage(foundMessage, false, true); + return; + } + } + if (replyToMessage.origin == null) { + if (message.forwardInfo != null && message.forwardInfo.fromChatId != 0 && message.forwardInfo.fromMessageId != 0 && !parent.isRepliesChat()) { + function = new TdApi.GetRepliedMessage(message.forwardInfo.fromChatId, message.forwardInfo.fromMessageId); + } else { + function = new TdApi.GetRepliedMessage(message.chatId, message.id); + } + if (replyToMessage.chatId != 0 && replyToMessage.messageId != 0) { + retryFunction = new TdApi.GetMessage(replyToMessage.chatId, replyToMessage.messageId); + } else { + retryFunction = null; + } + } else { + function = null; + retryFunction = null; + } + break; + } + case TdApi.MessageReplyToStory.CONSTRUCTOR: { + TdApi.MessageReplyToStory replyToStory = (TdApi.MessageReplyToStory) message.replyTo; + function = new TdApi.GetStory(replyToStory.storySenderChatId, replyToStory.storyId, true); + retryFunction = new TdApi.GetStory(replyToStory.storySenderChatId, replyToStory.storyId, false); + break; + } + default: { + Td.assertMessageReplyTo_699c5345(); + throw Td.unsupported(message.replyTo); + } } - if (message.replyInChatId != 0 && message.replyToMessageId != 0) { - retryFunction = new TdApi.GetMessage(message.replyInChatId, message.replyToMessageId); - } else { - retryFunction = null; + if (function != null) { + flags |= FLAG_LOADING; + this.retryFunction = retryFunction; + tdlib.client().send(function, this); } - tdlib.send(function, this); } private void parseContent (final TdApi.Message msg, final boolean forceRequestImage) { Background.instance().post(() -> setMessage(msg, forceRequestImage, false)); } - public void setChannelTitle (final String title) { - Background.instance().post(() -> { - setTitleImpl(title); - buildTitle(); - invalidate(false); - }); + private void parseContent (final TdApi.Story story, final boolean forceRequestImage) { + Background.instance().post(() -> setStory(story, forceRequestImage, false)); } - public void setTitle (final String title) { + public void setChannelTitle (final String title) { Background.instance().post(() -> { setTitleImpl(title); buildTitle(); @@ -554,27 +645,35 @@ public void setTitle (final String title) { }); } - private void setContent (final String title, final TD.ContentPreview content, boolean hasSpoiler, final Path contour, final ImageFile miniThumbnail, final ImageFile preview, final boolean previewCircle, final boolean forceRequest) { + private void setContent (final String title, final ContentPreview content, MediaPreview mediaPreview, final boolean forceRequest) { Background.instance().post(() -> { ReplyComponent.this.currentMessage = null; - ReplyComponent.this.content = new TD.ContentPreview(translatedText, content); + ReplyComponent.this.content = new ContentPreview(translatedText, content); setTitleImpl(title); - ReplyComponent.this.contour = contour; - ReplyComponent.this.miniThumbnail = miniThumbnail; - ReplyComponent.this.preview = preview; - ReplyComponent.this.hasSpoiler = hasSpoiler; - ReplyComponent.this.previewCircle = previewCircle; + ReplyComponent.this.mediaPreview = mediaPreview; buildLayout(); - invalidate(!isMessageComponent() || preview != null || miniThumbnail != null || forceRequest); + invalidate(!isMessageComponent() || mediaPreview != null || forceRequest); }); } private TdApi.Message currentMessage; + private TdApi.Error currentError; public boolean hasValidMessage () { return currentMessage != null; } + public TdApi.Error getError () { + return currentError; + } + + public CharSequence toErrorText () { + if (currentError != null && currentError.code == 404) { + return Lang.getString(R.string.ReplyMessageDeletedHint); + } + return TD.toErrorString(currentError); + } + public boolean replaceMessageContent (long messageId, TdApi.MessageContent content) { if (currentMessage != null && currentMessage.id == messageId) { currentMessage.content = content; @@ -597,7 +696,7 @@ public boolean replaceMessageTranslation (long messageId, TdApi.FormattedText tr public boolean deleteMessageContent (long messageId) { if (currentMessage != null && currentMessage.id == messageId) { - setContent(Lang.getString(R.string.Error), new TD.ContentPreview(null, R.string.DeletedMessage), false, null, null, null, false, true); + setContent(Lang.getString(R.string.Error), new ContentPreview(null, R.string.DeletedMessage), null, true); return true; } return false; @@ -605,14 +704,35 @@ public boolean deleteMessageContent (long messageId) { // private boolean isPrivate; - private void setMessage (TdApi.Message msg, boolean forceRequestImage, boolean forceLocal) { - currentMessage = msg; - if ((flags & FLAG_USE_COLORIZE) != 0 && !tdlib.isSelfSender(msg)) { - nameColorId = TD.getNameColorId(new TdlibSender(tdlib, msg.chatId, msg.senderId).getAvatarColorId()); - } else { - nameColorId = ColorId.NONE; + private void setStory (TdApi.Story story, boolean forceRequestImage, boolean forceLocal) { + // TODO + } + + private String computeTitleText (String defaultText) { + return BitwiseUtils.hasFlag(flags, FLAG_FORCE_TITLE) ? this.title : + sender == null ? StringUtils.isEmpty(senderName) ? (StringUtils.isEmpty(title) ? defaultText : title) : senderName : + StringUtils.isEmpty(senderName) ? tdlib.senderName(sender, isMessageComponent()) : + Lang.getString(isChannel() ? R.string.format_channelAndSignature : R.string.format_chatAndSignature, tdlib.senderName(sender, isMessageComponent()), senderName); + } + + private static class MediaPreview { + private final ImageFile miniThumbnail; + private final ImageFile preview; + private final Path contour; + private final boolean hasSpoiler; + private final boolean previewCircle; + + public MediaPreview (ImageFile miniThumbnail, ImageFile preview, Path contour, boolean hasSpoiler, boolean previewCircle) { + this.miniThumbnail = miniThumbnail; + this.preview = preview; + this.contour = contour; + this.hasSpoiler = hasSpoiler; + this.previewCircle = previewCircle; } - boolean isPrivate = msg.selfDestructTime != 0; + } + + private MediaPreview newMediaPreview (long chatId, TdApi.MessageContent content) { + boolean isPrivate = Td.isSecret(content); Path contour = null; TdApi.Thumbnail thumbnail = null; TdApi.PhotoSize photoSize = null; @@ -620,11 +740,11 @@ private void setMessage (TdApi.Message msg, boolean forceRequestImage, boolean f boolean previewCircle = false; boolean hasSpoiler = false; //noinspection SwitchIntDef - switch (msg.content.getConstructor()) { + switch (content.getConstructor()) { case TdApi.MessagePhoto.CONSTRUCTOR: { - TdApi.MessagePhoto messagePhoto = (TdApi.MessagePhoto) msg.content; + TdApi.MessagePhoto messagePhoto = (TdApi.MessagePhoto) content; TdApi.Photo photo = messagePhoto.photo; - photoSize = MediaWrapper.pickDisplaySize(tdlib, photo.sizes, msg.chatId); + photoSize = MediaWrapper.pickDisplaySize(tdlib, photo.sizes, chatId); TdApi.PhotoSize smallest = Td.findSmallest(photo); if (smallest != null && smallest != photoSize) { thumbnail = TD.toThumbnail(photoSize); @@ -634,8 +754,8 @@ private void setMessage (TdApi.Message msg, boolean forceRequestImage, boolean f break; } case TdApi.MessageChatChangePhoto.CONSTRUCTOR: { - TdApi.ChatPhoto photo = ((TdApi.MessageChatChangePhoto) msg.content).photo; - photoSize = MediaWrapper.pickDisplaySize(tdlib, photo.sizes, msg.chatId); + TdApi.ChatPhoto photo = ((TdApi.MessageChatChangePhoto) content).photo; + photoSize = MediaWrapper.pickDisplaySize(tdlib, photo.sizes, chatId); TdApi.PhotoSize smallest = Td.findSmallest(photo.sizes); if (smallest != null && smallest != photoSize) { thumbnail = TD.toThumbnail(photoSize); @@ -645,33 +765,27 @@ private void setMessage (TdApi.Message msg, boolean forceRequestImage, boolean f break; } case TdApi.MessageGame.CONSTRUCTOR: { - TdApi.Photo photo = ((TdApi.MessageGame) msg.content).game.photo; - photoSize = MediaWrapper.pickDisplaySize(tdlib, photo.sizes, msg.chatId); + TdApi.Photo photo = ((TdApi.MessageGame) content).game.photo; + photoSize = MediaWrapper.pickDisplaySize(tdlib, photo.sizes, chatId); miniThumbnail = photo.minithumbnail; break; } case TdApi.MessageSticker.CONSTRUCTOR: { - TdApi.Sticker sticker = ((TdApi.MessageSticker) msg.content).sticker; - if (sticker == null) { - return; - } + TdApi.Sticker sticker = ((TdApi.MessageSticker) content).sticker; thumbnail = sticker.thumbnail != null ? sticker.thumbnail : TD.toThumbnail(sticker); contour = Td.buildOutline(sticker, isMessageComponent() ? mHeight : height); break; } case TdApi.MessageVideo.CONSTRUCTOR: { - TdApi.MessageVideo messageVideo = (TdApi.MessageVideo) msg.content; + TdApi.MessageVideo messageVideo = (TdApi.MessageVideo) content; TdApi.Video video = messageVideo.video; - if (video == null) { - return; - } miniThumbnail = video.minithumbnail; thumbnail = video.thumbnail; hasSpoiler = messageVideo.hasSpoiler; break; } case TdApi.MessageAnimation.CONSTRUCTOR: { - TdApi.MessageAnimation messageAnimation = (TdApi.MessageAnimation) msg.content; + TdApi.MessageAnimation messageAnimation = (TdApi.MessageAnimation) content; TdApi.Animation animation = messageAnimation.animation; miniThumbnail = animation.minithumbnail; thumbnail = animation.thumbnail; @@ -679,26 +793,23 @@ private void setMessage (TdApi.Message msg, boolean forceRequestImage, boolean f break; } case TdApi.MessageVideoNote.CONSTRUCTOR: { - TdApi.VideoNote videoNote = ((TdApi.MessageVideoNote) msg.content).videoNote; + TdApi.VideoNote videoNote = ((TdApi.MessageVideoNote) content).videoNote; miniThumbnail = videoNote.minithumbnail; thumbnail = videoNote.thumbnail; previewCircle = true; break; } case TdApi.MessageDocument.CONSTRUCTOR: { - TdApi.Document doc = ((TdApi.MessageDocument) msg.content).document; - if (doc == null) { - return; - } + TdApi.Document doc = ((TdApi.MessageDocument) content).document; miniThumbnail = doc.minithumbnail; thumbnail = doc.thumbnail; break; } case TdApi.MessageText.CONSTRUCTOR: { - TdApi.WebPage webPage = ((TdApi.MessageText) msg.content).webPage; + TdApi.WebPage webPage = ((TdApi.MessageText) content).webPage; if (webPage != null) { if (webPage.photo != null) { - photoSize = MediaWrapper.pickDisplaySize(tdlib, webPage.photo.sizes, msg.chatId); + photoSize = MediaWrapper.pickDisplaySize(tdlib, webPage.photo.sizes, chatId); miniThumbnail = webPage.photo.minithumbnail; } else if (webPage.document != null && webPage.document.thumbnail != null) { thumbnail = webPage.document.thumbnail; @@ -742,63 +853,89 @@ private void setMessage (TdApi.Message msg, boolean forceRequestImage, boolean f } else { miniPreview = null; } - TD.ContentPreview contentPreview = TD.getChatListPreview(tdlib, msg.chatId, msg); - if (msg.forwardInfo != null && (parent != null && parent.getMessage().forwardInfo != null)) { - switch (msg.forwardInfo.origin.getConstructor()) { - case TdApi.MessageForwardOriginUser.CONSTRUCTOR: - sender = new TdApi.MessageSenderUser(((TdApi.MessageForwardOriginUser) msg.forwardInfo.origin).senderUserId); - senderName = null; - break; - case TdApi.MessageForwardOriginChannel.CONSTRUCTOR: { - TdApi.MessageForwardOriginChannel channel = ((TdApi.MessageForwardOriginChannel) msg.forwardInfo.origin); - sender = new TdApi.MessageSenderChat(channel.chatId); - senderName = !StringUtils.isEmpty(channel.authorSignature) ? channel.authorSignature : null; - break; - } - case TdApi.MessageForwardOriginChat.CONSTRUCTOR: { - TdApi.MessageForwardOriginChat chat = (TdApi.MessageForwardOriginChat) msg.forwardInfo.origin; - sender = new TdApi.MessageSenderChat(chat.senderChatId); - senderName = chat.authorSignature; - break; - } - case TdApi.MessageForwardOriginHiddenUser.CONSTRUCTOR: { - senderName = ((TdApi.MessageForwardOriginHiddenUser) msg.forwardInfo.origin).senderName; - sender = null; + return new MediaPreview(miniPreview, preview, contour, hasSpoiler, previewCircle); + } + + private void setMessage (TdApi.Message msg, boolean forceRequestImage, boolean forceLocal) { + currentMessage = msg; + MediaPreview mediaPreview = newMediaPreview(msg.chatId, msg.content); + ContentPreview contentPreview = ContentPreview.getChatListPreview(tdlib, msg.chatId, msg, true); + if (!Td.isEmpty(quote)) { + contentPreview = new ContentPreview(contentPreview, quote.text); + } + if (msg.forwardInfo != null /*&& (parent != null && parent.getMessage().forwardInfo != null)*/) { + handleOrigin(msg.forwardInfo.origin); + } else if (msg.importInfo != null) { + handleOrigin(new TdApi.MessageOriginHiddenUser(msg.importInfo.senderName)); + } else { + switch (msg.senderId.getConstructor()) { + case TdApi.MessageSenderUser.CONSTRUCTOR: { + handleOrigin(new TdApi.MessageOriginUser(((TdApi.MessageSenderUser) msg.senderId).userId)); break; } - case TdApi.MessageForwardOriginMessageImport.CONSTRUCTOR: { - TdApi.MessageForwardOriginMessageImport messageImport = (TdApi.MessageForwardOriginMessageImport) msg.forwardInfo.origin; - senderName = messageImport.senderName; - sender = null; + case TdApi.MessageSenderChat.CONSTRUCTOR: { + handleOrigin(new TdApi.MessageOriginChat(((TdApi.MessageSenderChat) msg.senderId).chatId, tdlib.isFromAnonymousGroupAdmin(msg) ? msg.authorSignature : null)); break; } default: { - throw new IllegalArgumentException(msg.forwardInfo.origin.toString()); + Td.assertMessageSender_439d4c9c(); + throw Td.unsupported(msg.senderId); } } - } else { - sender = msg.senderId; - senderName = tdlib.isFromAnonymousGroupAdmin(msg) ? msg.authorSignature : null; } - String title = BitwiseUtils.hasFlag(flags, FLAG_FORCE_TITLE) ? this.title : - sender == null ? senderName : - StringUtils.isEmpty(senderName) ? tdlib.senderName(sender, isMessageComponent()) : - Lang.getString(isChannel() ? R.string.format_channelAndSignature : R.string.format_chatAndSignature, tdlib.senderName(sender, isMessageComponent()), senderName); + String title = computeTitleText(null); + if (parent != null) { + UI.execute(parent::onReplyLoaded); + } if (Thread.currentThread() == Background.instance().thread() || forceLocal) { - this.content = new TD.ContentPreview(translatedText, contentPreview); + this.content = new ContentPreview(translatedText, contentPreview); setTitleImpl(title); - this.miniThumbnail = miniPreview; - this.contour = contour; - this.preview = preview; - this.hasSpoiler = hasSpoiler; - this.previewCircle = previewCircle; + this.mediaPreview = mediaPreview; buildLayout(); - invalidate(forceRequestImage || !isMessageComponent() || miniPreview != null || preview != null); + invalidate(forceRequestImage || !isMessageComponent() || mediaPreview != null); } else { - setContent(title, contentPreview, hasSpoiler, contour, miniPreview, preview, previewCircle, false); + setContent(title, contentPreview, mediaPreview, false); + } + if (parent != null) { + UI.execute(() -> parent.updateReactionAvatars(true)); } } + private void handleOrigin (TdApi.MessageOrigin origin) { + switch (origin.getConstructor()) { + case TdApi.MessageOriginUser.CONSTRUCTOR: + sender = new TdApi.MessageSenderUser(((TdApi.MessageOriginUser) origin).senderUserId); + senderName = null; + break; + case TdApi.MessageOriginChannel.CONSTRUCTOR: { + TdApi.MessageOriginChannel channel = ((TdApi.MessageOriginChannel) origin); + sender = new TdApi.MessageSenderChat(channel.chatId); + senderName = !StringUtils.isEmpty(channel.authorSignature) ? channel.authorSignature : null; + break; + } + case TdApi.MessageOriginChat.CONSTRUCTOR: { + TdApi.MessageOriginChat chat = (TdApi.MessageOriginChat) origin; + sender = new TdApi.MessageSenderChat(chat.senderChatId); + senderName = chat.authorSignature; + break; + } + case TdApi.MessageOriginHiddenUser.CONSTRUCTOR: { + senderName = ((TdApi.MessageOriginHiddenUser) origin).senderName; + sender = null; + break; + } + default: { + Td.assertMessageOrigin_f2224a59(); + throw Td.unsupported(origin); + } + } + accentColor = sender != null ? tdlib.senderAccentColor(sender) : null; + } + + private boolean useAccentColor () { + return accentColor != null && BitwiseUtils.hasFlag(flags, FLAG_USE_COLORIZE); + } + private void setTitleImpl (CharSequence title) { this.title = title != null ? title.toString() : null; // TODO charSequence? } @@ -816,6 +953,10 @@ public void onResult (TdApi.Object object) { parseContent((TdApi.Message) object, false); break; } + case TdApi.Story.CONSTRUCTOR: { + parseContent((TdApi.Story) object, false); + break; + } case TdApi.Error.CONSTRUCTOR: { if (retryFunction != null) { TdApi.Function function = retryFunction; @@ -823,13 +964,16 @@ public void onResult (TdApi.Object object) { tdlib.client().send(function, this); return; } - setContent(Lang.getString(R.string.Error), TD.errorCode(object) == 404 ? new TD.ContentPreview(null, R.string.DeletedMessage) : new TD.ContentPreview(null, 0, TD.toErrorString(object), true), false, null, null, null, false, false); - currentMessage = null; + if (!ignoreFailures) { + this.currentError = (TdApi.Error) object; + String errorTitle = Lang.getString(R.string.Error); + setContent((sender == null && StringUtils.isEmpty(senderName)) ? errorTitle : computeTitleText(errorTitle), TD.errorCode(object) == 404 ? new ContentPreview(null, R.string.DeletedMessage) : new ContentPreview(null, 0, TD.toErrorString(object), true), null, false); + currentMessage = null; + } break; } default: { - Log.unexpectedTdlibResponse(object, TdApi.GetMessage.class, TdApi.Message.class); - break; + throw new UnsupportedOperationException(object.toString()); } } flags &= ~FLAG_LOADING; @@ -861,6 +1005,7 @@ private static void initSizes () { @Override public void performDestroy () { + this.cornerDrawable = null; this.isDestroyed = true; } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/ReplyView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/ReplyView.java deleted file mode 100644 index 5aef78e481..0000000000 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/ReplyView.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * This file is a part of Telegram X - * Copyright © 2014 (tgx-android@pm.me) - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * File created on 21/02/2016 at 21:08 - */ -package org.thunderdog.challegram.component.chat; - -import android.content.Context; -import android.graphics.Canvas; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import androidx.annotation.Nullable; - -import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.R; -import org.thunderdog.challegram.config.Config; -import org.thunderdog.challegram.core.Lang; -import org.thunderdog.challegram.data.TD; -import org.thunderdog.challegram.data.TGWebPage; -import org.thunderdog.challegram.loader.ComplexReceiver; -import org.thunderdog.challegram.loader.DoubleImageReceiver; -import org.thunderdog.challegram.navigation.ViewController; -import org.thunderdog.challegram.telegram.Tdlib; -import org.thunderdog.challegram.theme.ColorId; -import org.thunderdog.challegram.theme.Theme; -import org.thunderdog.challegram.tool.Screen; -import org.thunderdog.challegram.tool.Strings; -import org.thunderdog.challegram.tool.Views; - -import me.vkryl.android.util.InvalidateContentProvider; -import me.vkryl.android.widget.FrameLayoutFix; -import me.vkryl.core.StringUtils; -import me.vkryl.core.lambda.Destroyable; -import me.vkryl.td.Td; - -public class ReplyView extends FrameLayoutFix implements View.OnClickListener, Destroyable, InvalidateContentProvider { - private int startX; - private int startY; - - private final DoubleImageReceiver receiver; - private final ComplexReceiver textMediaReceiver; - - private ReplyComponent reply; - private Callback callback; - - public ReplyView (Context context, Tdlib tdlib) { - super(context); - - setWillNotDraw(false); - - startX = Screen.dp(60f); - startY = Screen.dp(7f); - - receiver = new DoubleImageReceiver(this, 0); - textMediaReceiver = new ComplexReceiver(this, Config.MAX_ANIMATED_EMOJI_REFRESH_RATE); - - reply = new ReplyComponent(tdlib); - reply.setCurrentView(this); - } - - public ComplexReceiver getTextMediaReceiver () { - return textMediaReceiver; - } - - @Override - public void onClick (View v) { - if (callback != null) { - callback.onCloseReply(this); - } - } - - @Override - protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - layoutIfNeeded(); - } - - private int lastMeasuredWidth; - - private void layoutIfNeeded () { - int width = getMeasuredWidth(); - if (lastMeasuredWidth != width) { - lastMeasuredWidth = width; - reply.layout(width - startX - Screen.dp(12f)); - } - } - - @Override - public boolean invalidateContent (Object cause) { - if (reply == cause) { - reply.requestPreview(receiver, textMediaReceiver); - return true; - } - return false; - } - - @Override - protected void onDraw (Canvas c) { - reply.draw(c, startX, startY, getMeasuredWidth() - startX, reply.width(false), receiver, textMediaReceiver, Lang.rtl()); - } - - ImageView closeView; - - public void checkRtl () { - if (Views.setGravity(closeView, Lang.gravity())) { - Views.updateLayoutParams(closeView); - } - invalidate(); - } - - public void initWithCallback (Callback callbacK, ViewController themeProvider) { - this.callback = callbacK; - - FrameLayoutFix.LayoutParams params; - - params = FrameLayoutFix.newParams(Screen.dp(56f), ViewGroup.LayoutParams.MATCH_PARENT); - params.gravity = Lang.gravity(); - - closeView = new ImageView(getContext()); - closeView.setImageResource(R.drawable.baseline_close_24); - closeView.setColorFilter(Theme.iconColor()); - themeProvider.addThemeFilterListener(closeView, ColorId.icon); - closeView.setScaleType(ImageView.ScaleType.CENTER); - closeView.setLayoutParams(params); - closeView.setOnClickListener(this); - Views.setClickable(closeView); - closeView.setBackgroundResource(R.drawable.bg_btn_header); - - addView(closeView); - themeProvider.addThemeInvalidateListener(this); - } - - public void setReplyTo (TdApi.Message msg, @Nullable CharSequence forcedTitle) { - layoutIfNeeded(); - reply.set(forcedTitle, msg); - invalidate(); - } - - public void setPinnedMessage (TdApi.Message msg) { - layoutIfNeeded(); - reply.set(Lang.getString(R.string.PinnedMessage), msg,true); - invalidate(); - } - - public ReplyComponent getReply () { - return reply; - } - - @Override - public boolean onTouchEvent (MotionEvent event) { - return reply.onTouchEvent(this, event) || super.onTouchEvent(event); - } - - public void setWebPage (String link, TdApi.WebPage page) { - layoutIfNeeded(); - if (page == null) { - reply.set(Lang.getString(R.string.GettingLinkInfo), new TD.ContentPreview(link, false), null, null); - } else { - String title = Strings.any(page.title, page.siteName); - if (StringUtils.isEmpty(title)) { - if (page.photo != null || (page.sticker != null && Math.max(page.sticker.width, page.sticker.height) > TGWebPage.STICKER_SIZE_LIMIT)) { - title = Lang.getString(R.string.Photo); - } else if (page.video != null) { - title = Lang.getString(R.string.Video); - } else if (page.document != null || page.voiceNote != null) { - title = page.document != null ? page.document.fileName : Lang.getString(R.string.Audio); - if (StringUtils.isEmpty(title)) { - title = Lang.getString(R.string.File); - } - } else if (page.audio != null) { - title = TD.getTitle(page.audio) + " – " + TD.getSubtitle(page.audio); - } else if (page.sticker != null) { - title = Lang.getString(R.string.Sticker); - } else { - title = Lang.getString(R.string.LinkPreview); - } - } - String desc = !Td.isEmpty(page.description) ? page.description.text : page.displayUrl; - reply.set(title, new TD.ContentPreview(desc, false), page.photo != null ? page.photo.minithumbnail : null, TD.getWebPagePreviewImage(page)); - } - invalidate(); - } - - public void clear () { - receiver.clear(); - textMediaReceiver.clear(); - } - - @Override - public void performDestroy () { - receiver.destroy(); - textMediaReceiver.performDestroy(); - reply.performDestroy(); - } - - public interface Callback { - void onCloseReply (ReplyView view); - } -} diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/StickerSuggestionAdapter.java b/app/src/main/java/org/thunderdog/challegram/component/chat/StickerSuggestionAdapter.java index 8fd32caef8..f7a2a4ddc3 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/StickerSuggestionAdapter.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/StickerSuggestionAdapter.java @@ -16,7 +16,6 @@ import android.content.Context; import android.graphics.drawable.Drawable; -import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -43,22 +42,27 @@ public class StickerSuggestionAdapter extends RecyclerView.Adapter implements StickerSmallView.StickerMovementCallback { public interface Callback { boolean onSendStickerSuggestion (View view, TGStickerObj sticker, TdApi.MessageSendOptions sendOptions); - int getStickerSuggestionsTop (); + int getStickerSuggestionsTop (boolean isEmoji); int getStickerSuggestionPreviewViewportHeight (); long getStickerSuggestionsChatId (); } private final RecyclerView.LayoutManager manager; private final ViewController context; - private final Callback callback; + private Callback callback; + private final boolean isEmoji; private @Nullable ArrayList stickers; private @Nullable ViewController themeProvider; - public StickerSuggestionAdapter (ViewController context, Callback callback, RecyclerView.LayoutManager manager, @Nullable ViewController themeProvider) { + public StickerSuggestionAdapter (ViewController context, RecyclerView.LayoutManager manager, @Nullable ViewController themeProvider, boolean isEmoji) { this.context = context; - this.callback = callback; this.manager = manager; this.themeProvider = themeProvider; + this.isEmoji = isEmoji; + } + + public void setCallback (Callback callback) { + this.callback = callback; } public boolean hasStickers () { @@ -99,7 +103,7 @@ public void setStickers (@Nullable ArrayList stickers) { @Override public StickerSuggestionHolder onCreateViewHolder (ViewGroup parent, int viewType) { - return StickerSuggestionHolder.create(context.context(), context.tdlib(), viewType, this, themeProvider); + return StickerSuggestionHolder.create(context.context(), context.tdlib(), viewType, this, themeProvider, isEmoji); } @Override @@ -177,7 +181,7 @@ public void setStickerPressed (StickerSmallView view, TGStickerObj sticker, bool int i = indexOfSticker(sticker); if (i != -1) { final View childView = manager != null ? manager.findViewByPosition(i + 1) : null; - if (childView != null && childView instanceof StickerSmallView) { + if (childView instanceof StickerSmallView) { ((StickerSmallView) childView).setStickerPressed(isPressed); } else { notifyItemChanged(i + 1); @@ -212,7 +216,7 @@ public void onStickerPreviewChanged (StickerSmallView view, TGStickerObj otherOr @Override public int getStickersListTop () { - return callback.getStickerSuggestionsTop(); + return callback.getStickerSuggestionsTop(isEmoji); } // Holder @@ -226,12 +230,9 @@ public StickerSuggestionHolder (View itemView) { super(itemView); } - public static StickerSuggestionHolder create (Context context, Tdlib tdlib, int viewType, StickerSmallView.StickerMovementCallback callback, @Nullable ViewController themeProvider) { + public static StickerSuggestionHolder create (Context context, Tdlib tdlib, int viewType, StickerSmallView.StickerMovementCallback callback, @Nullable ViewController themeProvider, boolean isEmoji) { switch (viewType) { case TYPE_START: { - FrameLayoutFix contentView = new FrameLayoutFix(context); - contentView.setLayoutParams(new RecyclerView.LayoutParams(Screen.dp(34f), ViewGroup.LayoutParams.MATCH_PARENT)); - Drawable drawable = Theme.filteredDrawable(R.drawable.stickers_back_left, ColorId.overlayFilling, themeProvider); View view = new View(context); @@ -239,15 +240,10 @@ public static StickerSuggestionHolder create (Context context, Tdlib tdlib, int if (themeProvider != null) { themeProvider.addThemeInvalidateListener(view); } - view.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(12f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.RIGHT)); - contentView.addView(view); - - return new StickerSuggestionHolder(contentView); + view.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(4f), ViewGroup.LayoutParams.MATCH_PARENT)); + return new StickerSuggestionHolder(view); } case TYPE_END: { - FrameLayoutFix contentView = new FrameLayoutFix(context); - contentView.setLayoutParams(new RecyclerView.LayoutParams(Screen.dp(34f), ViewGroup.LayoutParams.MATCH_PARENT)); - Drawable drawable = Theme.filteredDrawable(R.drawable.stickers_back_right, ColorId.overlayFilling, themeProvider); View view = new View(context); @@ -255,10 +251,8 @@ public static StickerSuggestionHolder create (Context context, Tdlib tdlib, int if (themeProvider != null) { themeProvider.addThemeInvalidateListener(view); } - view.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(12f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.LEFT)); - contentView.addView(view); - - return new StickerSuggestionHolder(contentView); + view.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(4f), ViewGroup.LayoutParams.MATCH_PARENT)); + return new StickerSuggestionHolder(view); } case TYPE_STICKER: { StickerSmallView stickerView = new StickerSmallView(context); @@ -271,9 +265,12 @@ public static StickerSuggestionHolder create (Context context, Tdlib tdlib, int if (themeProvider != null) { themeProvider.addThemeInvalidateListener(stickerView); } - stickerView.setIsSuggestion(); - stickerView.setPadding(0, Screen.dp(2.5f), 0, Screen.dp(6.5f)); + stickerView.setIsSuggestion(isEmoji); + stickerView.setPadding(0, Screen.dp(4f), 0, Screen.dp(4f)); stickerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + if (isEmoji) { + stickerView.setPadding(Screen.dp(2)); + } return new StickerSuggestionHolder(stickerView); } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/TdlibSingleUnreadReactionsManager.java b/app/src/main/java/org/thunderdog/challegram/component/chat/TdlibSingleUnreadReactionsManager.java new file mode 100644 index 0000000000..0e9eb99e1d --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/TdlibSingleUnreadReactionsManager.java @@ -0,0 +1,262 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 25/09/2023 + */ +package org.thunderdog.challegram.component.chat; + +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.telegram.ChatListener; +import org.thunderdog.challegram.telegram.MessageListener; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibThread; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.util.CancellableResultHandler; + +import java.util.HashMap; +import java.util.Iterator; + +import me.vkryl.core.lambda.Destroyable; +import me.vkryl.core.reference.ReferenceLongMap; +import me.vkryl.td.Td; + +public class TdlibSingleUnreadReactionsManager implements ChatListener, MessageListener, Destroyable { + private final Tdlib tdlib; + private final HashMap chatStates = new HashMap<>(); + + public TdlibSingleUnreadReactionsManager (Tdlib tdlib) { + this.tdlib = tdlib; + + tdlib.listeners().subscribeForGlobalUpdates(this); + } + + @Override + public void performDestroy () { + tdlib.listeners().unsubscribeFromGlobalUpdates(this); + } + + @UiThread + private void onUpdateChatUnreadReactionCount (long chatId, @Nullable TdApi.UnreadReaction[] unreadReactions, int unreadReactionCount) { + ChatState chatState = chatStates.get(chatId); + if (chatState == null) { + chatState = new ChatState(tdlib, this, chatId); + chatStates.put(chatId, chatState); + } + + chatState.onUpdateChatUnreadReactionCount(chatId, unreadReactions, unreadReactionCount); + } + + @UiThread + public void checkChat (TdApi.Chat chat) { + final long chatId = chat.id; + ChatState chatState = chatStates.get(chatId); + if (chatState == null && chat.unreadReactionCount == 1) { + chatState = new ChatState(tdlib, this, chatId); + chatStates.put(chatId, chatState); + chatState.onUpdateChatUnreadReactionCount(chatId, null, chat.unreadReactionCount); + } + } + + @Nullable + public TdApi.UnreadReaction getSingleUnreadReaction (long chatId) { + ChatState chatState = chatStates.get(chatId); + return chatState != null ? chatState.getLastSingleUnreadReaction() : null; + } + + private static class ChatState { + private static final int STATE_NO_SINGLE_REACTION = 1; + private static final int STATE_LOADING = 2; + private static final int STATE_SINGLE_REACTION_FOUND = 3; + + private final TdlibSingleUnreadReactionsManager manager; + private final Tdlib tdlib; + private final long chatId; + + private int state; + private TdApi.UnreadReaction lastSingleUnreadReaction; + + private CancellableResultHandler handler; + + public ChatState (Tdlib tdlib, TdlibSingleUnreadReactionsManager manager, long chatId) { + this.tdlib = tdlib; + this.chatId = chatId; + this.state = STATE_NO_SINGLE_REACTION; + this.manager = manager; + } + + @Nullable + public TdApi.UnreadReaction getLastSingleUnreadReaction () { + return state == STATE_SINGLE_REACTION_FOUND ? lastSingleUnreadReaction : null; + } + + @UiThread + public void onUpdateChatUnreadReactionCount (long chatId, @Nullable TdApi.UnreadReaction[] unreadReactions, int unreadReactionCount) { + if (this.chatId != chatId) { + return; + } + if (handler != null) { + handler.cancel(); + handler = null; + } + + if (unreadReactionCount != 1 || unreadReactions != null && unreadReactions.length > 1) { + setState(STATE_NO_SINGLE_REACTION, null); + return; + } + if (unreadReactions != null && unreadReactions.length == 1) { + setState(STATE_SINGLE_REACTION_FOUND, unreadReactions[0]); + return; + } + + setState(STATE_LOADING, null); + handler = new CancellableResultHandler() { + @Override + public void processResult (TdApi.Object object) { + if (object.getConstructor() != TdApi.FoundChatMessages.CONSTRUCTOR) { + UI.post(() -> setState(STATE_NO_SINGLE_REACTION, null)); + return; + } + UI.post(() -> onUnreadReactionsLoaded((TdApi.FoundChatMessages) object)); + } + }; + tdlib.client().send(new TdApi.SearchChatMessages( + chatId, null, null, 0, 0, 100, + new TdApi.SearchMessagesFilterUnreadReaction(), 0), handler + ); + } + + @UiThread + private void setState (int state, TdApi.UnreadReaction foundUnreadReaction) { + this.state = state; + this.lastSingleUnreadReaction = foundUnreadReaction; + if (foundUnreadReaction != null) { + tdlib.getReaction(foundUnreadReaction.type); // preload reaction + } + manager.updateUnreadSingleReaction(chatId, foundUnreadReaction); + } + + private static TdApi.UnreadReaction findSingleReactionType (TdApi.Message[] messages) { + TdApi.UnreadReaction singleReaction = null; + for (TdApi.Message message : messages) { + if (message.unreadReactions == null) { + continue; + } + for (TdApi.UnreadReaction unreadReaction : message.unreadReactions) { + if (singleReaction == null) { + singleReaction = unreadReaction; + } else if (!Td.equalsTo(singleReaction.type, unreadReaction.type)) { + return null; + } + } + } + return singleReaction; + } + + @UiThread + private void onUnreadReactionsLoaded (TdApi.FoundChatMessages foundChatMessages) { + TdApi.UnreadReaction singleReaction = foundChatMessages.totalCount <= foundChatMessages.messages.length ? findSingleReactionType(foundChatMessages.messages) : null; + if (singleReaction != null) { + setState(STATE_SINGLE_REACTION_FOUND, singleReaction); + } else { + setState(STATE_NO_SINGLE_REACTION, null); + } + } + } + + + + /* + * Listeners + * + * TdApi.UpdateMessageUnreadReactions first calls updateMessageUnreadReactions, + * then immediately updatesChatUnreadReactionCount. The second call should be ignored. + * + */ + + @Override + public void onMessageUnreadReactionsChanged (long chatId, long messageId, @Nullable TdApi.UnreadReaction[] unreadReactions, int unreadReactionCount) { + scheduleOrIgnoreUpdate(chatId, new ScheduledUpdate(() -> UI.post(() -> + onUpdateChatUnreadReactionCount(chatId, unreadReactions, unreadReactionCount)), true)); + } + + @Override + public void onChatUnreadReactionCount (long chatId, int unreadReactionCount, boolean availabilityChanged) { + scheduleOrIgnoreUpdate(chatId, new ScheduledUpdate(() -> UI.post(() -> + onUpdateChatUnreadReactionCount(chatId, null, unreadReactionCount)), false)); + } + + private final HashMap scheduledUpdates = new HashMap<>(); + + private static class ScheduledUpdate { + public final Runnable update; + public final boolean isPriority; + + public ScheduledUpdate (Runnable update, boolean isPriority) { + this.update = update; + this.isPriority = isPriority; + } + } + + @TdlibThread + private void scheduleOrIgnoreUpdate (final long chatId, ScheduledUpdate update) { + final boolean hasScheduledUpdate = scheduledUpdates.containsKey(chatId); + final ScheduledUpdate oldUpdate = hasScheduledUpdate ? scheduledUpdates.get(chatId) : null; + final boolean hasPriorityScheduledUpdate = oldUpdate == null && hasScheduledUpdate; + + if (update.isPriority) { + update.update.run(); + scheduledUpdates.put(chatId, null); + } else if (!hasPriorityScheduledUpdate) { + scheduledUpdates.put(chatId, update); + } + + if (!hasScheduledUpdate) { + tdlib.runOnTdlibThread(() -> executeScheduledUpdate(chatId)); + } + } + + @TdlibThread + private void executeScheduledUpdate (long chatId) { + ScheduledUpdate update = scheduledUpdates.remove(chatId); + if (update != null) { + update.update.run(); + } + } + + /* * */ + + public interface UnreadSingleReactionListener { + void onUnreadSingleReactionUpdate (long chatId, @Nullable TdApi.UnreadReaction unreadReaction); + } + + private final ReferenceLongMap listeners = new ReferenceLongMap<>(); + + public void subscribeToUnreadSingleReactionUpdates (long chatId, UnreadSingleReactionListener listener) { + listeners.add(chatId, listener); + } + + public void unsubscribeFromUnreadSingleReactionUpdates (long chatId, UnreadSingleReactionListener listener) { + listeners.remove(chatId, listener); + } + + private void updateUnreadSingleReaction (long chatId, TdApi.UnreadReaction unreadReaction) { + Iterator list = listeners.iterator(chatId); + if (list != null) { + while (list.hasNext()) { + list.next().onUnreadSingleReactionUpdate(chatId, unreadReaction); + } + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/WallpaperAdapter.java b/app/src/main/java/org/thunderdog/challegram/component/chat/WallpaperAdapter.java index df7d2e029c..d4100ef6b2 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/WallpaperAdapter.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/WallpaperAdapter.java @@ -512,10 +512,10 @@ public void setWallpaper (TGBackground wallpaper, boolean isSelected) { if (wallpaper != null) { if (wallpaper.isPattern()) { int color = wallpaper.getPatternColor(); - receiver.setColorFilter(color); + receiver.setPorterDuffColorFilter(color); preview.requestFile(null); } else { - receiver.disableColorFilter(); + receiver.disablePorterDuffColorFilter(); preview.requestFile(wallpaper.getPreview(true)); } receiver.requestFile(wallpaper.getPreview(false)); @@ -647,7 +647,7 @@ protected void onDraw (Canvas c) { c.drawCircle(centerX, centerY, Screen.dp(28f), Paints.fillingPaint(ColorUtils.color((int) (86f * circleFactor), 0))); if (isCustom) { - Paint paint = Paints.getPorterDuffPaint(0xffffffff); + Paint paint = Paints.whitePorterDuffPaint(); paint.setAlpha((int) (255f * (1f - factor))); Drawable drawable = getSparseDrawable(R.drawable.baseline_image_24, 0); Drawables.draw(c, drawable, centerX - drawable.getMinimumWidth() / 2, centerY - drawable.getMinimumHeight() / 2, paint); diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/WallpaperView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/WallpaperView.java index 8970781ea3..6954f7db81 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/WallpaperView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/WallpaperView.java @@ -298,7 +298,7 @@ private static void drawWallpaper (TGBackground wallpaper, Canvas c, DrawAlgorit } else { c.drawColor(ColorUtils.alphaColor(alpha, wallpaper.getBackgroundColor(defaultColor))); } - receiver.getReceiver().setColorFilter(wallpaper.getPatternColor()); + receiver.getReceiver().setPorterDuffColorFilter(wallpaper.getPatternColor()); alpha *= wallpaper.getPatternIntensity(); if (alpha != 1f) receiver.setPaintAlpha(alpha); @@ -309,7 +309,7 @@ private static void drawWallpaper (TGBackground wallpaper, Canvas c, DrawAlgorit if (receiver.needPlaceholder()) { c.drawColor(ColorUtils.alphaColor(alpha, defaultColor)); } - receiver.disableColorFilter(); + receiver.disablePorterDuffColorFilter(); if (alpha != 1f) { receiver.setPaintAlpha(alpha); } diff --git a/app/src/main/java/org/thunderdog/challegram/component/dialogs/ChatView.java b/app/src/main/java/org/thunderdog/challegram/component/dialogs/ChatView.java index 28f9c474d3..738d18d16c 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/dialogs/ChatView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/dialogs/ChatView.java @@ -26,6 +26,7 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; +import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; @@ -35,10 +36,13 @@ import org.thunderdog.challegram.loader.ComplexReceiver; import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.receiver.RefreshRateLimiter; import org.thunderdog.challegram.support.RippleSupport; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; import org.thunderdog.challegram.telegram.TdlibSettingsManager; import org.thunderdog.challegram.telegram.TdlibStatusManager; +import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.PropertyId; import org.thunderdog.challegram.theme.Theme; @@ -59,6 +63,8 @@ import org.thunderdog.challegram.util.text.TextMedia; import org.thunderdog.challegram.widget.BaseView; +import java.util.List; + import me.vkryl.android.AnimatorUtils; import me.vkryl.android.animator.BoolAnimator; import me.vkryl.android.util.InvalidateContentProvider; @@ -66,7 +72,7 @@ import me.vkryl.core.collection.IntList; import me.vkryl.td.ChatPosition; -public class ChatView extends BaseView implements TdlibSettingsManager.PreferenceChangeListener, InvalidateContentProvider, EmojiStatusHelper.EmojiStatusReceiverInvalidateDelegate { +public class ChatView extends BaseView implements TdlibSettingsManager.PreferenceChangeListener, InvalidateContentProvider, EmojiStatusHelper.EmojiStatusReceiverInvalidateDelegate, TdlibUi.MessageProvider { private static Paint timePaint; private static TextPaint titlePaint, titlePaintFake; // counterTextPaint @@ -174,22 +180,30 @@ public ComplexReceiver getTextMediaReceiver () { private final AvatarReceiver avatarReceiver; private final ComplexReceiver emojiStatusReceiver; private final ComplexReceiver textMediaReceiver; + private final ComplexReceiver reactionsReceiver; private final BoolAnimator isSelected = new BoolAnimator(this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); + private final RefreshRateLimiter refreshRateLimiter; public ChatView (Context context, Tdlib tdlib) { super(context, tdlib); if (titlePaint == null) { initPaints(); } + this.refreshRateLimiter = new RefreshRateLimiter(this, Config.MAX_ANIMATED_EMOJI_REFRESH_RATE); setId(R.id.chat); RippleSupport.setTransparentSelector(this); int chatListMode = getChatListMode(); - emojiStatusReceiver = new ComplexReceiver(this, Config.MAX_ANIMATED_EMOJI_REFRESH_RATE); - avatarReceiver = new AvatarReceiver(this); + emojiStatusReceiver = new ComplexReceiver(this) + .setUpdateListener(refreshRateLimiter); + reactionsReceiver = new ComplexReceiver(this) + .setUpdateListener(refreshRateLimiter); + avatarReceiver = new AvatarReceiver(this) + .setUpdateListener(refreshRateLimiter.passThroughUpdateListener()); avatarReceiver.setAvatarRadiusPropertyIds(PropertyId.AVATAR_RADIUS_CHAT_LIST, PropertyId.AVATAR_RADIUS_CHAT_LIST_FORUM); avatarReceiver.setBounds(getAvatarLeft(chatListMode), getAvatarTop(chatListMode), getAvatarLeft(chatListMode) + getAvatarSize(chatListMode), getAvatarTop(chatListMode) + getAvatarSize(chatListMode)); - textMediaReceiver = new ComplexReceiver(this, Config.MAX_ANIMATED_EMOJI_REFRESH_RATE); + textMediaReceiver = new ComplexReceiver(this) + .setUpdateListener(refreshRateLimiter); setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } @@ -201,6 +215,7 @@ public void setAnimationsDisabled (boolean disabled) { avatarReceiver.setAnimationDisabled(disabled); textMediaReceiver.setAnimationDisabled(disabled); emojiStatusReceiver.setAnimationDisabled(disabled); + reactionsReceiver.setAnimationDisabled(disabled); } public static int getViewHeight (int chatListMode) { @@ -334,12 +349,14 @@ public void attach () { avatarReceiver.attach(); textMediaReceiver.attach(); emojiStatusReceiver.attach(); + reactionsReceiver.attach(); } public void detach () { avatarReceiver.detach(); textMediaReceiver.detach(); emojiStatusReceiver.detach(); + reactionsReceiver.detach(); } public void setChat (TGChat chat) { @@ -354,7 +371,6 @@ public void setChat (TGChat chat) { this.isPinnedArchive.setValue(chat != null && chat.isArchive() && !tdlib.settings().needHideArchive(), false); if (chat != null) { chat.checkLayout(getMeasuredWidth()); - chat.syncCounter(); chat.attachToView(this); if (chat.isArchive()) { this.tdlib.settings().addUserPreferenceChangeListener(this); @@ -408,6 +424,18 @@ private void requestEmojiStatus () { } } + public ComplexReceiver getReactionsReceiver () { + return reactionsReceiver; + } + + public void requestReactionFiles () { + if (chat != null) { + chat.requestReactionFiles(reactionsReceiver); + } else { + reactionsReceiver.clear(); + } + } + private void requestTextContent () { Text text = chat != null ? chat.getText() : null; if (text != null) { @@ -420,6 +448,7 @@ private void requestTextContent () { private void requestContent () { requestTextContent(); requestEmojiStatus(); + requestReactionFiles(); if (chat != null) { AvatarPlaceholder.Metadata avatarPlaceholder = chat.getAvatarPlaceholder(); if (avatarPlaceholder != null) { @@ -527,6 +556,7 @@ public boolean invalidateContent (Object cause) { if (this.chat == cause) { requestTextContent(); requestEmojiStatus(); + requestReactionFiles(); return true; } return false; @@ -591,20 +621,27 @@ protected void onDraw (Canvas c) { if (chat.isSending()) { int x = chat.getChecksRight() - Screen.dp(10f) - Screen.dp(Icons.CLOCK_SHIFT_X); Drawables.drawRtl(c, Icons.getClockIcon(ColorId.iconLight), x, getClockTop(chatListMode) - Screen.dp(Icons.CLOCK_SHIFT_Y), Paints.getIconLightPorterDuffPaint(), viewWidth, rtl); - } else if (chat.isOutgoing() && !chat.isSelfChat()) { + } else { int x = chat.getChecksRight(); int y = getClockTop(chatListMode); - if (chat.showViews()) { - y -= Screen.dp(.5f); - } else if (chat.isUnread()) { - x += Screen.dp(4f); + if (chat.isOutgoing() && !chat.isSelfChat()) { + if (chat.showViews()) { + y -= Screen.dp(.5f); + } else if (chat.isUnread()) { + x += Screen.dp(4f); + } + if (chat.showViews()) { + chat.getViewCounter().draw(c, x + Screen.dp(3f), y + Screen.dp(14f) / 2f, Gravity.RIGHT, 1f, this, ColorId.ticksRead); + x -= chat.getViewCounter().getScaledWidth(Screen.dp(3)); + } else { + int iconX = x - Screen.dp(Icons.TICKS_SHIFT_X) - Screen.dp(14f); + boolean unread = chat.isUnread(); + Drawables.drawRtl(c, unread ? Icons.getSingleTick(ColorId.ticks) : Icons.getDoubleTick(ColorId.ticks), iconX, y - Screen.dp(Icons.TICKS_SHIFT_Y), unread ? Paints.getTicksPaint() : Paints.getTicksReadPaint(), viewWidth, rtl); + x -= Screen.dp(24 - 8 + 3); + } } - if (chat.showViews()) { - chat.getViewCounter().draw(c, x + Screen.dp(3f), y + Screen.dp(14f) / 2f, Gravity.RIGHT, 1f, this, ColorId.ticksRead); - } else { - int iconX = x - Screen.dp(Icons.TICKS_SHIFT_X) - Screen.dp(14f); - boolean unread = chat.isUnread(); - Drawables.drawRtl(c, unread ? Icons.getSingleTick(ColorId.ticks) : Icons.getDoubleTick(ColorId.ticks), iconX, y - Screen.dp(Icons.TICKS_SHIFT_Y), unread ? Paints.getTicksPaint() : Paints.getTicksReadPaint(), viewWidth, rtl); + if (chat.needDrawReactionsPreview()) { + chat.getReactionsCounterDrawable().draw(c, x - chat.getReactionsWidth(), y + Screen.dp(6f)); } } @@ -620,7 +657,7 @@ protected void onDraw (Canvas c) { counterRight -= mentionCounter.getScaledWidth(getTimePaddingLeft()); Counter reactionCounter = chat.getReactionsCounter(); - reactionCounter.draw(c, counterRight - counterRadius, counterCenterY, Gravity.RIGHT, 1f, this, chat.notificationsEnabled() ? ColorId.badgeText: ColorId.badgeMutedText); + reactionCounter.draw(c, counterRight - counterRadius, counterCenterY, Gravity.RIGHT, 1f, this, chat.notificationsEnabled() ? ColorId.badgeText : ColorId.badgeMutedText); counterRight -= reactionCounter.getScaledWidth(getTimePaddingLeft()); TdlibStatusManager.Helper status = chat.statusHelper(); @@ -700,4 +737,24 @@ protected void onDraw (Canvas c) { DrawAlgorithms.drawIcon(c, avatarReceiver, 315f, chat.getScheduleAnimator().getFloatValue(), Theme.fillingColor(), getSparseDrawable(R.drawable.baseline_watch_later_10, ColorId.badgeMuted), PorterDuffPaint.get(ColorId.badgeMuted, chat.getScheduleAnimator().getFloatValue())); DrawAlgorithms.drawSimplestCheckBox(c, avatarReceiver, isSelected.getFloatValue()); } + + @Override + public boolean isMediaGroup () { + return chat != null && chat.isMediaGroup(); + } + + @Override + public List getVisibleMediaGroup () { + return chat != null ? chat.getVisibleMediaGroup() : null; + } + + @Override + public TdApi.Message getVisibleMessage () { + return chat != null ? chat.getVisibleMessage() : null; + } + + @Override + public int getVisibleMessageFlags () { + return TdlibMessageViewer.Flags.NO_SENSITIVE_SCREENSHOT_NOTIFICATION | (chat != null && chat.needRefreshInteractionInfo() ? TdlibMessageViewer.Flags.REFRESH_INTERACTION_INFO : 0); + } } \ No newline at end of file diff --git a/app/src/main/java/org/thunderdog/challegram/component/dialogs/ChatsAdapter.java b/app/src/main/java/org/thunderdog/challegram/component/dialogs/ChatsAdapter.java index 7cba60e1b6..40ca25f81e 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/dialogs/ChatsAdapter.java +++ b/app/src/main/java/org/thunderdog/challegram/component/dialogs/ChatsAdapter.java @@ -630,6 +630,12 @@ public int updateSecretChat (TdApi.SecretChat secretChat) { return -1; } + public void updateRelativeDate () { + for (TGChat chat : chats) { + chat.updateDate(); + } + } + public void updateLocale (boolean forceText) { for (TGChat chat : chats) { chat.updateLocale(forceText); diff --git a/app/src/main/java/org/thunderdog/challegram/component/dialogs/SearchManager.java b/app/src/main/java/org/thunderdog/challegram/component/dialogs/SearchManager.java index 4bdeab90e1..c8b72496fb 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/dialogs/SearchManager.java +++ b/app/src/main/java/org/thunderdog/challegram/component/dialogs/SearchManager.java @@ -368,41 +368,32 @@ private void loadTopChats (final int currentContextId, final TdApi.ChatList chat searchLocalChats(currentContextId, chatList, query); return; } - tdlib.client().send(new TdApi.GetTopChats((searchFlags & FLAG_TOP_SEARCH_CATEGORY_GROUPS) != 0 ? new TdApi.TopChatCategoryGroups() : new TdApi.TopChatCategoryUsers(), 30), object -> { + tdlib.send(new TdApi.GetTopChats((searchFlags & FLAG_TOP_SEARCH_CATEGORY_GROUPS) != 0 ? new TdApi.TopChatCategoryGroups() : new TdApi.TopChatCategoryUsers(), 30), (topChats, error) -> { if (contextId == currentContextId || isCheck) { final ArrayList foundTopChats; final long[] foundTopChatIds; - switch (object.getConstructor()) { - case TdApi.Chats.CONSTRUCTOR: { - long[] chatIds = ((TdApi.Chats) object).chatIds; - ArrayList foundChats = new ArrayList<>(chatIds.length); - int resultCount = parseResult(tdlib, listener, searchFlags, foundChats, chatList, chatIds, null, false, null); - if (resultCount == 0) { - foundTopChats = null; - foundTopChatIds = null; - } else if (resultCount == chatIds.length) { - foundTopChats = foundChats; - foundTopChatIds = chatIds; - } else { - foundTopChats = foundChats; - foundTopChatIds = new long[resultCount]; - int i = 0; - for (TGFoundChat chat : foundChats) { - foundTopChatIds[i] = chat.getId(); - i++; - } - } - break; - } - case TdApi.Error.CONSTRUCTOR: { - Log.i("GetTopChats error, displaying no results: %s", TD.toErrorString(object)); + if (error != null) { + Log.i("GetTopChats error, displaying no results: %s", TD.toErrorString(error)); + foundTopChats = null; + foundTopChatIds = null; + } else { + long[] chatIds = topChats.chatIds; + ArrayList foundChats = new ArrayList<>(chatIds.length); + int resultCount = parseResult(tdlib, listener, searchFlags, foundChats, chatList, chatIds, null, false, null); + if (resultCount == 0) { foundTopChats = null; foundTopChatIds = null; - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.GetTopChats.class, TdApi.Chats.class, TdApi.Error.class); - return; + } else if (resultCount == chatIds.length) { + foundTopChats = foundChats; + foundTopChatIds = chatIds; + } else { + foundTopChats = foundChats; + foundTopChatIds = new long[resultCount]; + int i = 0; + for (TGFoundChat chat : foundChats) { + foundTopChatIds[i] = chat.getId(); + i++; + } } } tdlib.ui().post(() -> { @@ -420,7 +411,7 @@ private void loadTopChats (final int currentContextId, final TdApi.ChatList chat private void setTopChats (final int currentContextId, final @Nullable TdApi.ChatList chatList, final @Nullable String query, final @Nullable ArrayList topChats, final long[] topChatIds, final boolean isCheck) { boolean isSilent = this.contextId != currentContextId || isCheck; final int oldChatsCount = this.topChats != null ? this.topChats.size() : 0; - final int newChatsCount = topChats != null ? topChats.size(): 0; + final int newChatsCount = topChats != null ? topChats.size() : 0; if (oldChatsCount == 0 && newChatsCount == 0) { if (!isSilent) { @@ -442,7 +433,7 @@ private void processTopChats (final int currentContextId, final ArrayList localChats; private String localChatsQuery; - private static int indexOfLocalPrivateChat (ArrayList chats, int userId) { - int i = 0; - for (TGFoundChat chat : chats) { - if (chat.getUserId() == userId) { - return i; - } - i++; - } - return -1; - } - private boolean isFiltered () { return isFiltered(searchFlags); } @@ -649,8 +629,7 @@ public void onResult (final TdApi.Object object) { break; } default: { - Log.unexpectedTdlibResponse(object, TdApi.SearchChats.class, TdApi.Chats.class, TdApi.Users.class, TdApi.Error.class); - return; + throw new UnsupportedOperationException(object.toString()); } } @@ -660,6 +639,7 @@ public void onResult (final TdApi.Object object) { switch (++state[0]) { case 1: if (sentRequest = foundChatIds.size() < 100) { + Log.ensureReturnType(TdApi.SearchChatsOnServer.class, TdApi.Chats.class); tdlib.client().send(new TdApi.SearchChatsOnServer(query, 100 - foundChatIds.size()), this); } break; @@ -889,26 +869,17 @@ public void act () { tdlib.ui().post(runnable); - tdlib.client().send(new TdApi.SearchPublicChats(usernameQuery), object -> { + tdlib.send(new TdApi.SearchPublicChats(usernameQuery), (remoteChats, error) -> { if (contextId == currentContextId) { runnable.cancel(); final ArrayList foundChats; - switch (object.getConstructor()) { - case TdApi.Chats.CONSTRUCTOR: { - long[] chatIds = ((TdApi.Chats) object).chatIds; - foundChats = new ArrayList<>(chatIds.length); - parseResult(tdlib, listener, searchFlags & (~FLAG_ONLY_CONTACTS), foundChats, chatList, chatIds, usernameQuery, true, null); - break; - } - case TdApi.Error.CONSTRUCTOR: { - Log.i("SearchPublicChats error, showing no results: %s", TD.toErrorString(object)); - foundChats = null; - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.SearchChats.class, TdApi.Chats.class, TdApi.Error.class); - return; - } + if (error != null) { + Log.i("SearchPublicChats error, showing no results: %s", TD.toErrorString(error)); + foundChats = null; + } else { + long[] chatIds = remoteChats.chatIds; + foundChats = new ArrayList<>(chatIds.length); + parseResult(tdlib, listener, searchFlags & (~FLAG_ONLY_CONTACTS), foundChats, chatList, chatIds, usernameQuery, true, null); } tdlib.ui().post(() -> { if (contextId == currentContextId) { @@ -1019,9 +990,9 @@ public void act () { if (isMore) { offset = messageList.nextOffset; } - tdlib.client().send(new TdApi.SearchMessages(chatList, query, offset, loadCount, null, 0, 0), new Client.ResultHandler() { + tdlib.send(new TdApi.SearchMessages(chatList, query, offset, loadCount, null, 0, 0), new Tdlib.ResultHandler<>() { @Override - public void onResult (TdApi.Object object) { + public void onResult (TdApi.FoundMessages foundMessages, @Nullable TdApi.Error error) { if (contextId != currentContextId) { return; } @@ -1030,36 +1001,26 @@ public void onResult (TdApi.Object object) { } final TGFoundMessage[] messages; final String nextOffset; - switch (object.getConstructor()) { - case TdApi.FoundMessages.CONSTRUCTOR: { - TdApi.FoundMessages foundMessages = (TdApi.FoundMessages) object; - List foundMessageList = new ArrayList<>(foundMessages.messages.length); - TdApi.Chat chat = null; - for (TdApi.Message message : foundMessages.messages) { - if (chat == null || chat.id != message.chatId) - chat = tdlib.chat(message.chatId); - if (listener.filterMessageSearchResultSource(chat)) { - foundMessageList.add(new TGFoundMessage(tdlib, chatList, chat, message, query)); - } - } - if (foundMessageList.isEmpty() && !StringUtils.isEmpty(foundMessages.nextOffset)) { - tdlib.client().send(new TdApi.SearchMessages(chatList, query, foundMessages.nextOffset, loadCount, null, 0, 0), this); - return; + if (error != null) { + Log.w("SearchMessages returned error, displaying no results: %s", TD.toErrorString(error)); + messages = null; + nextOffset = null; + } else { + List foundMessageList = new ArrayList<>(foundMessages.messages.length); + TdApi.Chat chat = null; + for (TdApi.Message message : foundMessages.messages) { + if (chat == null || chat.id != message.chatId) + chat = tdlib.chat(message.chatId); + if (listener.filterMessageSearchResultSource(chat)) { + foundMessageList.add(new TGFoundMessage(tdlib, chatList, chat, message, query)); } - messages = foundMessageList.toArray(new TGFoundMessage[0]); - nextOffset = foundMessages.nextOffset; - break; - } - case TdApi.Error.CONSTRUCTOR: { - Log.w("SearchMessages returned error, displaying no results: %s", TD.toErrorString(object)); - messages = null; - nextOffset = null; - break; } - default: { - Log.unexpectedTdlibResponse(object, TdApi.SearchMessages.class, TdApi.FoundMessages.class, TdApi.Error.class); + if (foundMessageList.isEmpty() && !StringUtils.isEmpty(foundMessages.nextOffset)) { + tdlib.send(new TdApi.SearchMessages(chatList, query, foundMessages.nextOffset, loadCount, null, 0, 0), this); return; } + messages = foundMessageList.toArray(new TGFoundMessage[0]); + nextOffset = foundMessages.nextOffset; } tdlib.ui().post(() -> { if (contextId == currentContextId) { diff --git a/app/src/main/java/org/thunderdog/challegram/component/emoji/AnimatedEmojiDrawable.java b/app/src/main/java/org/thunderdog/challegram/component/emoji/AnimatedEmojiDrawable.java index 923b7f71c5..629d1cbfbe 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/emoji/AnimatedEmojiDrawable.java +++ b/app/src/main/java/org/thunderdog/challegram/component/emoji/AnimatedEmojiDrawable.java @@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable; import android.view.View; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -31,6 +32,7 @@ import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.loader.gif.GifReceiver; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.tool.Drawables; import me.vkryl.core.lambda.Destroyable; @@ -114,6 +116,28 @@ public void setAlpha (int i) { } } + public void setThemedPorterDuffColorId (@PorterDuffColorId int colorId) { + gifReceiver.setThemedPorterDuffColorId(colorId); + imageReceiver.setThemedPorterDuffColorId(colorId); + if (drawable != null) { + drawable.setColorFilter(imageReceiver.getBitmapPaint().getColorFilter()); + } + } + public void setPorterDuffColorFilter (@ColorInt int color) { + gifReceiver.setPorterDuffColorFilter(color); + imageReceiver.setPorterDuffColorFilter(color); + if (drawable != null) { + drawable.setColorFilter(imageReceiver.getBitmapPaint().getColorFilter()); + } + } + public void disablePorterDuffColorFilter () { + gifReceiver.disablePorterDuffColorFilter(); + imageReceiver.disablePorterDuffColorFilter(); + if (drawable != null) { + drawable.setColorFilter(null); + } + } + @Override public void setColorFilter (@Nullable ColorFilter colorFilter) { diff --git a/app/src/main/java/org/thunderdog/challegram/component/emoji/AnimatedEmojiEffect.java b/app/src/main/java/org/thunderdog/challegram/component/emoji/AnimatedEmojiEffect.java index 60e49df8c3..7fac00aae2 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/emoji/AnimatedEmojiEffect.java +++ b/app/src/main/java/org/thunderdog/challegram/component/emoji/AnimatedEmojiEffect.java @@ -18,8 +18,10 @@ import android.graphics.Rect; import android.view.View; +import androidx.annotation.ColorInt; + import org.thunderdog.challegram.charts.CubicBezierInterpolator; -import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; @@ -27,8 +29,9 @@ import kotlin.random.Random; import me.vkryl.core.MathUtils; +import me.vkryl.core.lambda.Destroyable; -public class AnimatedEmojiEffect { +public class AnimatedEmojiEffect implements Destroyable { public AnimatedEmojiDrawable animatedEmojiDrawable; Rect bounds = new Rect(); @@ -40,23 +43,23 @@ public class AnimatedEmojiEffect { boolean longAnimation; boolean firsDraw = true; - private AnimatedEmojiEffect(AnimatedEmojiDrawable animatedEmojiDrawable, boolean longAnimation) { + private AnimatedEmojiEffect (AnimatedEmojiDrawable animatedEmojiDrawable, boolean longAnimation) { this.animatedEmojiDrawable = animatedEmojiDrawable; this.longAnimation = longAnimation; startTime = System.currentTimeMillis(); } - public static AnimatedEmojiEffect createFrom(AnimatedEmojiDrawable animatedEmojiDrawable, boolean longAnimation) { + public static AnimatedEmojiEffect createFrom (AnimatedEmojiDrawable animatedEmojiDrawable, boolean longAnimation) { return new AnimatedEmojiEffect(animatedEmojiDrawable, longAnimation); } - public void setBounds(int l, int t, int r, int b) { + public void setBounds (int l, int t, int r, int b) { bounds.set(l, t, r, b); } long lastGenerateTime; - public void draw(Canvas canvas) { + public void draw (Canvas canvas) { if (!longAnimation) { if (firsDraw) { for (int i = 0; i < 7; i++) { @@ -90,19 +93,34 @@ public void draw(Canvas canvas) { firsDraw = false; } - public boolean done() { + public boolean done () { return System.currentTimeMillis() - startTime > 2500; } - public void setView(View view) { + public void setView (View view) { animatedEmojiDrawable.attach(); parentView = view; } - public void removeView() { + public void removeView () { animatedEmojiDrawable.detach(); } + @Override + public void performDestroy () { + animatedEmojiDrawable.performDestroy(); + } + + public void setThemedPorterDuffColorId (@PorterDuffColorId int colorId) { + animatedEmojiDrawable.setThemedPorterDuffColorId(colorId); + } + public void setPorterDuffColorFilter (@ColorInt int color) { + animatedEmojiDrawable.setPorterDuffColorFilter(color); + } + public void disablePorterDuffColorFilter () { + animatedEmojiDrawable.disablePorterDuffColorFilter(); + } + private class Particle { float fromX, fromY; float toX; @@ -116,7 +134,7 @@ private class Particle { boolean mirror; float randomRotation; - public void generate() { + public void generate () { progress = 0; float bestDistance = 0; float bestX = randX(); @@ -155,8 +173,6 @@ public void generate() { fromY = bounds.height() * 0.45f + bounds.height() * 0.1f * (Math.abs(Random.Default.nextInt() % 100) / 100f); - - if (longAnimation) { fromSize = bounds.width() * 0.05f + bounds.width() * 0.1f * (Math.abs(Random.Default.nextInt() % 100) / 100f); toSize = fromSize * (1.5f + 1.5f * (Math.abs(Random.Default.nextInt() % 100) / 100f)); @@ -175,15 +191,15 @@ public void generate() { randomRotation = 20 * ((Random.Default.nextInt() % 100) / 100f); } - private float randY() { + private float randY () { return (bounds.height() * 0.5f * (Math.abs(Random.Default.nextInt() % 100) / 100f)); } - private long randDuration() { + private long randDuration () { return 1000 + Math.abs(Random.Default.nextInt() % 900); } - private float randX() { + private float randX () { if (longAnimation) { return bounds.width() * -0.25f + bounds.width() * 1.5f * (Math.abs(Random.Default.nextInt() % 100) / 100f); } else { @@ -191,7 +207,7 @@ private float randX() { } } - public void draw(Canvas canvas) { + public void draw (Canvas canvas) { progress += (float) Math.min(40, 1000f / Screen.refreshRate()) / duration; progress = MathUtils.clamp(progress); float progressInternal = CubicBezierInterpolator.EASE_OUT.getInterpolation(progress); diff --git a/app/src/main/java/org/thunderdog/challegram/component/emoji/MediaStickersAdapter.java b/app/src/main/java/org/thunderdog/challegram/component/emoji/MediaStickersAdapter.java index 930194020a..79c496a250 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/emoji/MediaStickersAdapter.java +++ b/app/src/main/java/org/thunderdog/challegram/component/emoji/MediaStickersAdapter.java @@ -15,31 +15,41 @@ package org.thunderdog.challegram.component.emoji; import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.collection.LongSparseArray; import androidx.recyclerview.widget.RecyclerView; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; +import org.thunderdog.challegram.charts.LayoutHelper; +import org.thunderdog.challegram.component.chat.EmojiToneHelper; +import org.thunderdog.challegram.component.chat.EmojiView; +import org.thunderdog.challegram.component.sticker.StickerPreviewView; import org.thunderdog.challegram.component.sticker.StickerSmallView; import org.thunderdog.challegram.component.sticker.TGStickerObj; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TGDefaultEmoji; import org.thunderdog.challegram.data.TGStickerSetInfo; import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.support.RippleSupport; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Fonts; import org.thunderdog.challegram.tool.Screen; @@ -49,20 +59,25 @@ import org.thunderdog.challegram.widget.NonMaterialButton; import org.thunderdog.challegram.widget.ProgressComponentView; import org.thunderdog.challegram.widget.SeparatorView; +import org.thunderdog.challegram.widget.TrendingPackHeaderView; import java.util.ArrayList; -import java.util.Random; import me.vkryl.android.widget.FrameLayoutFix; public class MediaStickersAdapter extends RecyclerView.Adapter implements View.OnClickListener { private final ViewController context; - private final ArrayList items; + protected final ArrayList items; private final StickerSmallView.StickerMovementCallback callback; private final boolean isTrending; private @Nullable RecyclerView.LayoutManager manager; - private @Nullable ViewController themeProvider; + private final @Nullable ViewController themeProvider; + private final @Nullable OffsetProvider offsetProvider; + private final boolean canViewStickerPackByClick; + private final @Nullable EmojiToneHelper emojiToneHelper; + private View.OnClickListener classicEmojiClickListener; + private StickerPreviewView.MenuStickerPreviewCallback menuStickerPreviewCallback; private boolean isBig; @@ -76,52 +91,78 @@ public MediaStickersAdapter (ViewController context, StickerSmallView.Sticker this.isTrending = isTrending; this.themeProvider = themeProvider; this.items = new ArrayList<>(); + this.offsetProvider = null; + this.canViewStickerPackByClick = true; + this.emojiToneHelper = null; + } + + public MediaStickersAdapter (ViewController context, StickerSmallView.StickerMovementCallback callback, boolean isTrending, @Nullable ViewController themeProvider, OffsetProvider offsetProvider, boolean canViewStickerPackByClick, @Nullable EmojiToneHelper emojiToneHelper) { + this.context = context; + this.callback = callback; + this.isTrending = isTrending; + this.themeProvider = themeProvider; + this.items = new ArrayList<>(); + this.offsetProvider = offsetProvider; + this.canViewStickerPackByClick = canViewStickerPackByClick; + this.emojiToneHelper = emojiToneHelper; } public void setManager (@NonNull RecyclerView.LayoutManager manager) { this.manager = manager; } - @Override + public void setClassicEmojiClickListener (View.OnClickListener classicEmojiClickListener) { + this.classicEmojiClickListener = classicEmojiClickListener; + } + + public void setMenuStickerPreviewCallback (StickerPreviewView.MenuStickerPreviewCallback menuStickerPreviewCallback) { + this.menuStickerPreviewCallback = menuStickerPreviewCallback; + } + + @NonNull @Override public StickerHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { - return StickerHolder.create(context.context(), context.tdlib(), viewType, isTrending, this, callback, isBig, themeProvider); + return StickerHolder.create(context.context(), context.tdlib(), this, viewType, isTrending, this, classicEmojiClickListener, callback, isBig, themeProvider, offsetProvider, emojiToneHelper, repaintingColorId); } - public int measureScrollTop (int position, int spanCount, int sectionIndex, ArrayList sections, boolean haveRecentsTitle) { + public int measureScrollTop (int position, int spanCount, int sectionIndex, ArrayList sections, @Nullable RecyclerView recyclerView, boolean haveRecentsTitle) { if (position == 0 || sections == null || sectionIndex == -1) { return 0; } position--; - int scrollY = EmojiLayout.getHeaderSize() + EmojiLayout.getHeaderPadding(); + int scrollY = LayoutParams.getKeyboardTopViewHeight(layoutParams); if (position == 0) { return scrollY; } - - final int rowSize = ((sections.get(0).isTrending() ? Screen.smallestSide() : Screen.currentWidth()) / spanCount); + final int stickerViewForceHeight = LayoutParams.getStickerViewForceHeight(layoutParams); + final int recyclerWidth = recyclerView != null ? recyclerView.getMeasuredWidth() - recyclerView.getPaddingLeft() - recyclerView.getPaddingRight(): 0; + final int stickersRowHeight = stickerViewForceHeight > 0 ? stickerViewForceHeight : + ((sections.get(0).isTrending() ? Screen.smallestSide() : (recyclerWidth > 0 ? recyclerWidth: Screen.currentWidth())) / spanCount); + // final int rowSize = ((sections.get(0).isTrending() ? Screen.smallestSide() : Screen.currentWidth()) / spanCount); boolean hadFavorite = false; for (int i = 0; i < sectionIndex + 1 && position > 0 && i < sections.size(); i++) { TGStickerSetInfo stickerSet = sections.get(i); if (!stickerSet.isSystem() || stickerSet.isDefaultEmoji()) { - scrollY += Screen.dp(stickerSet.isTrending() ? 52f : 32f); + scrollY += Screen.dp(stickerSet.isTrending() ? 52f : 27f) + + (stickerSet.isTrending() ? 0: LayoutParams.getHeaderViewPaddingTop(layoutParams)); position--; } else if (stickerSet.isFavorite()) { // position--; hadFavorite = true; - } else if (stickerSet.isRecent()) { + } else if (stickerSet.isRecent() && !stickerSet.isFakeClassicEmoji()) { position--; if (haveRecentsTitle) { - scrollY += Screen.dp(32f); + scrollY += Screen.dp(27f) + LayoutParams.getHeaderViewPaddingTop(layoutParams);; } } if (position > 0) { - int itemCount = Math.min(stickerSet.isDefaultEmoji() ? stickerSet.getSize() + 1: stickerSet.isTrending() ? 5 : stickerSet.getSize(), position); + int itemCount = Math.min(stickerSet.isDefaultEmoji() ? stickerSet.getSize() + 1 : stickerSet.isTrending() ? (stickerSet.isEmoji() ? 16 : 5) : stickerSet.getSize(), position); int rowCount = (int) Math.ceil((double) itemCount / (double) spanCount); - scrollY += rowCount * rowSize; + scrollY += rowCount * stickersRowHeight; position -= itemCount; } } @@ -224,7 +265,7 @@ public void onClick (View v) { } else if (viewId == R.id.btn_toggleCollapseRecentStickers) { onToggleCollapseRecentStickers((TextView) v, stickerSet); updateCollapseView((TextView) v, stickerSet); - } else { + } else if (canViewStickerPackByClick) { stickerSet.show(context); } } @@ -238,12 +279,16 @@ public void updateCollapseView (ViewGroup viewGroup, TGStickerSetInfo stickerSet } public void updateCollapseView (TextView collapseView, TGStickerSetInfo stickerSet) { + updateCollapseView(collapseView, stickerSet, R.string.ShowXMoreStickers); + } + + public void updateCollapseView (TextView collapseView, TGStickerSetInfo stickerSet, @StringRes int showMoreRes) { if (stickerSet != null && stickerSet.getFullSize() > Config.DEFAULT_SHOW_RECENT_STICKERS_COUNT) { if (stickerSet.isCollapsed()) { int moreSize = stickerSet.getFullSize() - stickerSet.getSize(); - collapseView.setText(Lang.pluralBold(R.string.ShowXMoreStickers, moreSize)); + collapseView.setText(Lang.pluralBold(showMoreRes, moreSize)); } else { - collapseView.setText(R.string.ShowLessStickers); + collapseView.setText(Lang.getString(R.string.ShowLessStickers)); } collapseView.setVisibility(View.VISIBLE); } else { @@ -272,6 +317,13 @@ public void onBindViewHolder (StickerHolder holder, int position) { Views.setTextGravity((TextView) holder.itemView, Lang.gravity()); break; } + case StickerHolder.TYPE_SEPARATOR_COLLAPSABLE: { + TGStickerSetInfo stickerSet = getStickerSet(position); + TextView collapseView = ((CollapsableSeparatorView) holder.itemView).textView; + updateCollapseView(collapseView, stickerSet, R.string.ShowXMoreEmoji); + collapseView.setTag(stickerSet); + break; + } case StickerHolder.TYPE_HEADER_COLLAPSABLE: { TGStickerSetInfo stickerSet = getStickerSet(position); TextView titleView = (TextView) ((ViewGroup) holder.itemView).getChildAt(0); @@ -285,65 +337,22 @@ public void onBindViewHolder (StickerHolder holder, int position) { } case StickerHolder.TYPE_HEADER_TRENDING: { TGStickerSetInfo stickerSet = getStickerSet(position); + String highlight = getHighlightText(position); if (stickerSet != null && !stickerSet.isViewed()) { stickerSet.view(); } - RelativeLayout contentView = (RelativeLayout) holder.itemView; - View newView = contentView.getChildAt(0); - NonMaterialButton button = (NonMaterialButton) contentView.getChildAt(1); - TextView titleView = (TextView) contentView.getChildAt(2); - TextView subtitleView = (TextView) contentView.getChildAt(3); - - contentView.setTag(stickerSet); - - newView.setVisibility(stickerSet == null || stickerSet.isViewed() ? View.GONE : View.VISIBLE); - - button.setInProgress(stickerSet != null && !stickerSet.isRecent() && isInProgress(stickerSet.getId()), false); - button.setIsDone(stickerSet != null && stickerSet.isInstalled(), false); - button.setTag(stickerSet); - - Views.setMediumText(titleView, stickerSet != null ? stickerSet.getTitle() : ""); - subtitleView.setText(stickerSet != null ? Lang.plural(R.string.xStickers, stickerSet.getSize()) : ""); - - if (Views.setAlignParent(newView, Lang.rtl())) { - int rightMargin = Screen.dp(6f); - int topMargin = Screen.dp(3f); - Views.setMargins(newView, Lang.rtl() ? rightMargin : 0, topMargin, Lang.rtl() ? 0 : rightMargin, 0); - Views.updateLayoutParams(newView); - } - - if (Views.setAlignParent(button, Lang.rtl() ? RelativeLayout.ALIGN_PARENT_LEFT : RelativeLayout.ALIGN_PARENT_RIGHT)) { - int leftMargin = Screen.dp(16f); - int topMargin = Screen.dp(5f); - Views.setMargins(button, Lang.rtl() ? 0 : leftMargin, topMargin, Lang.rtl() ? leftMargin : 0, 0); - Views.updateLayoutParams(button); - } - - RelativeLayout.LayoutParams params; - params = (RelativeLayout.LayoutParams) titleView.getLayoutParams(); - if (Lang.rtl()) { - int leftMargin = Screen.dp(12f); - if (params.leftMargin != leftMargin) { - params.leftMargin = leftMargin; - params.rightMargin = 0; - params.addRule(RelativeLayout.LEFT_OF, R.id.btn_new); - params.addRule(RelativeLayout.RIGHT_OF, R.id.btn_addStickerSet); - Views.updateLayoutParams(titleView); - } - } else { - int rightMargin = Screen.dp(12f); - if (params.rightMargin != rightMargin) { - params.rightMargin = rightMargin; - params.leftMargin = 0; - params.addRule(RelativeLayout.RIGHT_OF, R.id.btn_new); - params.addRule(RelativeLayout.LEFT_OF, R.id.btn_addStickerSet); - Views.updateLayoutParams(titleView); - } - } - Views.setTextGravity(titleView, Lang.gravity()); - - if (Views.setAlignParent(subtitleView, Lang.rtl())) { - Views.updateLayoutParams(subtitleView); + TrendingPackHeaderView contentView = (TrendingPackHeaderView) holder.itemView; + contentView.setStickerSetInfo(stickerSet, highlight, + stickerSet != null && isInProgress(stickerSet.getId()), + stickerSet != null && !stickerSet.isViewed() + ); + break; + } + case StickerHolder.TYPE_DEFAULT_EMOJI: { + TGDefaultEmoji defaultEmoji = getDefaultEmojiString(position); + if (defaultEmoji != null) { + holder.itemView.setId(defaultEmoji.isRecent ? R.id.emoji_recent : R.id.emoji); + ((EmojiView) holder.itemView).setEmoji(defaultEmoji.emoji, defaultEmoji.emojiColorState); } break; } @@ -362,6 +371,10 @@ public void onViewAttachedToWindow (StickerHolder holder) { ((ProgressComponentView) holder.itemView).attach(); break; } + case StickerHolder.TYPE_PROGRESS_OFFSETABLE: { + ((ProgressComponentView) ((ViewGroup) (holder.itemView)).getChildAt(0)).attach(); + break; + } } } @@ -377,6 +390,10 @@ public void onViewDetachedFromWindow (StickerHolder holder) { ((ProgressComponentView) holder.itemView).detach(); break; } + case StickerHolder.TYPE_PROGRESS_OFFSETABLE: { + ((ProgressComponentView) ((ViewGroup) (holder.itemView)).getChildAt(0)).detach(); + break; + } } } @@ -392,6 +409,10 @@ public void onViewRecycled (StickerHolder holder) { ((ProgressComponentView) holder.itemView).performDestroy(); break; } + case StickerHolder.TYPE_PROGRESS_OFFSETABLE: { + ((ProgressComponentView) ((ViewGroup) (holder.itemView)).getChildAt(0)).performDestroy(); + break; + } } } @@ -469,27 +490,51 @@ public int indexOfSticker (TGStickerObj sticker, int startIndex) { return -1; } + public interface OffsetProvider { + int provideOffset (); + int provideReverseOffset (); + void onContentScroll (float shadowFactor); + void onScrollFinished (); + } + public static class StickerItem { public int viewType; public final TGStickerObj sticker; public final TGStickerSetInfo stickerSet; + public final TGDefaultEmoji defaultEmoji; + public String highlight; public StickerItem (int viewType) { this.viewType = viewType; this.sticker = null; this.stickerSet = null; + this.defaultEmoji = null; } public StickerItem (int viewType, TGStickerObj sticker) { this.viewType = viewType; this.sticker = sticker; this.stickerSet = null; + this.defaultEmoji = null; } public StickerItem (int viewType, TGStickerSetInfo info) { this.viewType = viewType; this.sticker = null; this.stickerSet = info; + this.defaultEmoji = null; + } + + public StickerItem (int viewType, TGDefaultEmoji defaultEmojiString) { + this.viewType = viewType; + this.sticker = null; + this.stickerSet = null; + this.defaultEmoji = defaultEmojiString; + } + + public StickerItem setHighlightValue (String highlight) { + this.highlight = highlight; + return this; } public boolean setViewType (int viewType) { @@ -513,6 +558,15 @@ public void setItem (StickerItem item) { } } + public void replaceItem (int index, StickerItem item) { + items.set(index, item); + notifyItemChanged(index); + } + + public ArrayList getItems () { + return items; + } + private void clear () { if (!this.items.isEmpty()) { int count = this.items.size(); @@ -537,6 +591,12 @@ public void addItems (ArrayList items) { } } + public void addItem (StickerItem item) { + int index = this.items.size(); + this.items.add(item); + notifyItemRangeInserted(index, 1); + } + @Override public int getItemViewType (int position) { return items.get(position).viewType; @@ -550,6 +610,20 @@ public int getItemViewType (int position) { return position >= 0 && position < items.size() ? items.get(position).stickerSet : null; } + public @Nullable TGDefaultEmoji getDefaultEmojiString (int position) { + return position >= 0 && position < items.size() ? items.get(position).defaultEmoji : null; + } + + public @Nullable String getHighlightText (int position) { + return position >= 0 && position < items.size() ? items.get(position).highlight : null; + } + + private @PorterDuffColorId int repaintingColorId = ColorId.iconActive; + + public void setRepaintingColorId (@PorterDuffColorId int repaintingColorId) { + this.repaintingColorId = repaintingColorId; + } + public static class StickerHolder extends RecyclerView.ViewHolder { public static final int TYPE_STICKER = 0; public static final int TYPE_EMPTY = 1; @@ -563,22 +637,29 @@ public static class StickerHolder extends RecyclerView.ViewHolder { public static final int TYPE_SEPARATOR = 10; public static final int TYPE_EMOJI_STATUS_DEFAULT = 11; public static final int TYPE_NO_EMOJISETS = 12; + public static final int TYPE_PROGRESS_OFFSETABLE = 13; + public static final int TYPE_PADDING_OFFSETABLE = 14; + public static final int TYPE_DEFAULT_EMOJI = 15; + public static final int TYPE_SEPARATOR_COLLAPSABLE = 16; public StickerHolder (View itemView) { super(itemView); } - public static @NonNull StickerHolder create (Context context, Tdlib tdlib, int viewType, boolean isTrending, View.OnClickListener onClickListener, StickerSmallView.StickerMovementCallback callback, boolean isBig, @Nullable ViewController themeProvider) { + public static @NonNull StickerHolder create (Context context, Tdlib tdlib, MediaStickersAdapter adapter, int viewType, boolean isTrending, View.OnClickListener onClickListener, View.OnClickListener classicEmojiClickListener, StickerSmallView.StickerMovementCallback callback, boolean isBig, @Nullable ViewController themeProvider, @Nullable OffsetProvider offsetProvider, @Nullable EmojiToneHelper toneHelper, @PorterDuffColorId int repaintingColorId) { switch (viewType) { case TYPE_EMOJI_STATUS_DEFAULT: case TYPE_STICKER: { StickerSmallView view; view = new StickerSmallView(context); + view.setForceHeight(LayoutParams.getStickerViewForceHeight(adapter.layoutParams)); view.init(tdlib); + view.setThemedColorId(repaintingColorId); if (isTrending) { view.setIsTrending(); } view.setStickerMovementCallback(callback); + view.setMenuStickerPreviewCallback(adapter.menuStickerPreviewCallback); view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); if (viewType == TYPE_EMOJI_STATUS_DEFAULT) { view.setIsPremiumStar(); @@ -591,6 +672,9 @@ public StickerHolder (View itemView) { return new StickerHolder(view); } case TYPE_HEADER: { + final int paddingTop = LayoutParams.getHeaderViewPaddingTop(adapter.layoutParams); + final int paddingHorizontal = LayoutParams.getHeaderViewPaddingHorizontal(adapter.layoutParams); + TextView textView = new NoScrollTextView(context); textView.setTypeface(Fonts.getRobotoMedium()); textView.setTextColor(Theme.textDecentColor()); @@ -601,10 +685,15 @@ public StickerHolder (View itemView) { textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15f); textView.setSingleLine(true); textView.setEllipsize(TextUtils.TruncateAt.END); - textView.setPadding(Screen.dp(14f), Screen.dp(5f), Screen.dp(14f), Screen.dp(5f)); - textView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(32f))); + textView.setPadding(paddingHorizontal, paddingTop, paddingHorizontal, Screen.dp(5f)); + textView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(27f) + paddingTop)); return new StickerHolder(textView); } + case TYPE_SEPARATOR_COLLAPSABLE: { + CollapsableSeparatorView v = new CollapsableSeparatorView(context); + v.init(themeProvider, onClickListener); + return new StickerHolder(v); + } case TYPE_HEADER_COLLAPSABLE: { LinearLayout viewGroup = new LinearLayout(context); viewGroup.setOrientation(LinearLayout.HORIZONTAL); @@ -636,98 +725,18 @@ public StickerHolder (View itemView) { return new StickerHolder(viewGroup); } case TYPE_HEADER_TRENDING: { - RelativeLayout contentView = new RelativeLayout(context); + TrendingPackHeaderView contentView = new TrendingPackHeaderView(context); contentView.setOnClickListener(onClickListener); contentView.setPadding(Screen.dp(16f), Screen.dp(isBig ? 18f : 13f) - EmojiLayout.getHeaderPadding(), Screen.dp(16f), 0); contentView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(isBig ? 57f : 52f))); - RelativeLayout.LayoutParams params; - - params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, Screen.dp(16f)); - params.addRule(Lang.alignParent()); - if (Lang.rtl()) { - params.leftMargin = Screen.dp(6f); - } else { - params.rightMargin = Screen.dp(6f); - } - params.topMargin = Screen.dp(3f); - TextView newView = new NoScrollTextView(context); - ViewSupport.setThemedBackground(newView, ColorId.promo, themeProvider).setCornerRadius(3f); - newView.setId(R.id.btn_new); - newView.setSingleLine(true); - newView.setPadding(Screen.dp(4f), Screen.dp(1f), Screen.dp(4f), 0); - newView.setTextColor(Theme.getColor(ColorId.promoContent)); - if (themeProvider != null) { - themeProvider.addThemeTextColorListener(newView, ColorId.promoContent); - themeProvider.addThemeInvalidateListener(newView); - } - newView.setTypeface(Fonts.getRobotoBold()); - newView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 10f); - newView.setText(Lang.getString(R.string.New).toUpperCase()); - newView.setLayoutParams(params); - - params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, Screen.dp(28f)); - if (Lang.rtl()) { - params.rightMargin = Screen.dp(16f); - } else { - params.leftMargin = Screen.dp(16f); - } - params.topMargin = Screen.dp(5f); - params.addRule(Lang.rtl() ? RelativeLayout.ALIGN_PARENT_LEFT : RelativeLayout.ALIGN_PARENT_RIGHT); - NonMaterialButton button = new NonMaterialButton(context); - if (themeProvider != null) { - themeProvider.addThemeInvalidateListener(button); - } - button.setId(R.id.btn_addStickerSet); - button.setText(R.string.Add); - button.setOnClickListener(onClickListener); - button.setLayoutParams(params); - - params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - if (Lang.rtl()) { - params.leftMargin = Screen.dp(12f); - params.addRule(RelativeLayout.LEFT_OF, R.id.btn_new); - params.addRule(RelativeLayout.RIGHT_OF, R.id.btn_addStickerSet); - } else { - params.rightMargin = Screen.dp(12f); - params.addRule(RelativeLayout.RIGHT_OF, R.id.btn_new); - params.addRule(RelativeLayout.LEFT_OF, R.id.btn_addStickerSet); - } - TextView titleView = new NoScrollTextView(context); - titleView.setTypeface(Fonts.getRobotoMedium()); - titleView.setTextColor(Theme.textAccentColor()); - titleView.setGravity(Lang.gravity()); - if (themeProvider != null) { - themeProvider.addThemeTextAccentColorListener(titleView); - } - titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16f); - titleView.setSingleLine(true); - titleView.setEllipsize(TextUtils.TruncateAt.END); - titleView.setLayoutParams(params); - - params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.addRule(Lang.alignParent()); - params.topMargin = Screen.dp(22f); - TextView subtitleView = new NoScrollTextView(context); - subtitleView.setTypeface(Fonts.getRobotoRegular()); - subtitleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15f); - subtitleView.setTextColor(Theme.textDecentColor()); - if (themeProvider != null) { - themeProvider.addThemeTextDecentColorListener(subtitleView); - } - subtitleView.setSingleLine(true); - subtitleView.setEllipsize(TextUtils.TruncateAt.END); - subtitleView.setLayoutParams(params); - - contentView.addView(newView); - contentView.addView(button); - contentView.addView(titleView); - contentView.addView(subtitleView); + contentView.setButtonOnClickListener(onClickListener); + contentView.setThemeProvider(themeProvider); return new StickerHolder(contentView); } case TYPE_KEYBOARD_TOP: { View view = new View(context); - view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, EmojiLayout.getHeaderSize() + EmojiLayout.getHeaderPadding())); + view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.getKeyboardTopViewHeight(adapter.layoutParams))); return new StickerHolder(view); } case TYPE_SEPARATOR: { @@ -751,7 +760,7 @@ public StickerHolder (View itemView) { } textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15f); textView.setSingleLine(true); - textView.setText(Lang.getString(viewType == TYPE_NO_EMOJISETS ? R.string.NoEmojiSetsFound: viewType == TYPE_COME_AGAIN_LATER ? R.string.ComeAgainLater : R.string.NoStickerSets)); + textView.setText(Lang.getString(viewType == TYPE_NO_EMOJISETS ? R.string.NoEmojiSetsFound : viewType == TYPE_COME_AGAIN_LATER ? R.string.ComeAgainLater : R.string.NoStickerSets)); textView.setGravity(Gravity.CENTER); textView.setEllipsize(TextUtils.TruncateAt.END); //noinspection ResourceType @@ -761,13 +770,146 @@ public StickerHolder (View itemView) { case TYPE_PROGRESS: { ProgressComponentView progressView = new ProgressComponentView(context); progressView.initBig(1f); - //noinspection ResourceType progressView.setPadding(0, isBig ? 0 : EmojiLayout.getHeaderSize(), 0, 0); progressView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return new StickerHolder(progressView); } + case TYPE_PROGRESS_OFFSETABLE: { + FrameLayoutFix contentView = new FrameLayoutFix(context) { + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, offsetProvider != null ? MeasureSpec.makeMeasureSpec(offsetProvider.provideReverseOffset(), MeasureSpec.EXACTLY) : heightMeasureSpec); + } + }; + ProgressComponentView view = new ProgressComponentView(context); + view.initBig(1f); + view.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); + contentView.addView(view); + return new StickerHolder(contentView); + } + case TYPE_PADDING_OFFSETABLE: { + View view = new View(context) { + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension( + getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), + offsetProvider != null ? MeasureSpec.makeMeasureSpec(offsetProvider.provideOffset(), MeasureSpec.EXACTLY) : heightMeasureSpec); + } + }; + return new StickerHolder(view); + } + case TYPE_DEFAULT_EMOJI: { + EmojiView imageView = new EmojiView(context, tdlib, toneHelper); + imageView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + imageView.setOnClickListener(classicEmojiClickListener); + Views.setClickable(imageView); + RippleSupport.setTransparentSelector(imageView); + return new StickerHolder(imageView); + } } throw new UnsupportedOperationException("viewType == " + viewType); } } + + public static class CollapsableSeparatorView extends FrameLayoutFix { + private final TextView textView; + private final LinearLayout linearLayout; + private final ImageView imageView; + private final SeparatorView separatorView; + + public CollapsableSeparatorView (@NonNull Context context) { + super(context); + + textView = new NoScrollTextView(context); + textView.setTextColor(Theme.textDecentColor()); + textView.setGravity(Lang.gravity() | Gravity.CENTER_VERTICAL); + textView.setSingleLine(true); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13f); + textView.setTypeface(Fonts.getRobotoRegular()); + textView.setId(R.id.btn_toggleCollapseRecentStickers); + textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER_VERTICAL)); + + imageView = new ImageView(context); + imageView.setScaleType(ImageView.ScaleType.CENTER); + imageView.setImageResource(R.drawable.baseline_small_arrow_down_18); + imageView.setColorFilter(new PorterDuffColorFilter(Theme.iconColor(), PorterDuff.Mode.SRC_IN)); + imageView.setLayoutParams(LayoutHelper.createLinear(18, 18, 0, Gravity.NO_GRAVITY, 0, 0, 4, 0)); + + separatorView = new SeparatorView(context); + separatorView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(5f), Gravity.CENTER)); + + linearLayout = new LinearLayout(context); + linearLayout.setOrientation(LinearLayout.HORIZONTAL); + linearLayout.setGravity(Gravity.CENTER); + linearLayout.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); + linearLayout.addView(imageView); + linearLayout.addView(textView); + linearLayout.setPadding(Screen.dp(24), Screen.dp(3), Screen.dp(24), Screen.dp(3)); + ViewSupport.setThemedBackground(linearLayout, ColorId.filling); + + setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(24f))); + addView(separatorView); + addView(linearLayout); + } + + public void init (ViewController themeProvider, View.OnClickListener onClickListener) { + if (themeProvider != null) { + themeProvider.addThemeTextDecentColorListener(textView); + themeProvider.addThemeInvalidateListener(linearLayout); + themeProvider.addThemeSpecialFilterListener(imageView, ColorId.icon); + themeProvider.addThemeInvalidateListener(separatorView); + } + linearLayout.setOnClickListener(v -> onClickListener.onClick(textView)); + } + } + + @Nullable + private LayoutParams layoutParams; + + public void setLayoutParams (LayoutParams layoutParams) { + this.layoutParams = layoutParams; + } + + public static class LayoutParams { // todo: make Builder class? + public final static int DEFAULT = -1; + + public final int keyboardTopViewHeight; + public final int recyclerHorizontalPadding; + public final int headerViewPaddingTop; + public final int headerViewPaddingHorizontal; + public final int stickerViewHeight; + + public LayoutParams (int keyboardTopViewHeight, int recyclerHorizontalPadding, int headerViewPaddingTop, int headerViewPaddingHorizontal, int stickerViewHeight) { + this.keyboardTopViewHeight = keyboardTopViewHeight; + this.recyclerHorizontalPadding = recyclerHorizontalPadding; + this.headerViewPaddingTop = headerViewPaddingTop; + this.headerViewPaddingHorizontal = headerViewPaddingHorizontal; + this.stickerViewHeight = stickerViewHeight; + } + + public static int getKeyboardTopViewHeight (LayoutParams layoutParams) { + return layoutParams != null && layoutParams.keyboardTopViewHeight != LayoutParams.DEFAULT ? + layoutParams.keyboardTopViewHeight : EmojiLayout.getHeaderSize() + EmojiLayout.getHeaderPadding(); + } + + public static int getRecyclerViewPaddingHorizontal (LayoutParams layoutParams) { + return layoutParams != null && layoutParams.recyclerHorizontalPadding != LayoutParams.DEFAULT ? + layoutParams.recyclerHorizontalPadding : 0; + } + + public static int getHeaderViewPaddingTop (LayoutParams layoutParams) { + return layoutParams != null && layoutParams.headerViewPaddingTop != LayoutParams.DEFAULT ? + layoutParams.headerViewPaddingTop : Screen.dp(5); + } + + public static int getHeaderViewPaddingHorizontal (LayoutParams layoutParams) { + return layoutParams != null && layoutParams.headerViewPaddingHorizontal != LayoutParams.DEFAULT ? + layoutParams.headerViewPaddingHorizontal : Screen.dp(14); + } + + public static int getStickerViewForceHeight (LayoutParams layoutParams) { + return layoutParams != null && layoutParams.stickerViewHeight != LayoutParams.DEFAULT ? + layoutParams.stickerViewHeight : -1; + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/inline/CustomResultView.java b/app/src/main/java/org/thunderdog/challegram/component/inline/CustomResultView.java index 81fdc0eacc..6546baee3b 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/inline/CustomResultView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/inline/CustomResultView.java @@ -21,12 +21,15 @@ import androidx.annotation.Nullable; +import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.user.RemoveHelper; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.data.InlineResult; import org.thunderdog.challegram.loader.ComplexReceiver; import org.thunderdog.challegram.support.RippleSupport; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; +import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.util.DrawableProvider; import org.thunderdog.challegram.util.SelectableItemDelegate; @@ -39,7 +42,7 @@ import me.vkryl.core.BitwiseUtils; import me.vkryl.core.lambda.Destroyable; -public class CustomResultView extends SparseDrawableView implements Destroyable, SelectableItemDelegate, FactorAnimator.Target, RemoveHelper.RemoveDelegate, DrawableProvider, InvalidateContentProvider { +public class CustomResultView extends SparseDrawableView implements Destroyable, SelectableItemDelegate, FactorAnimator.Target, RemoveHelper.RemoveDelegate, DrawableProvider, InvalidateContentProvider, TdlibUi.MessageProvider { private static final int FLAG_DETACHED = 1; private static final int FLAG_CAUGHT = 1 << 1; private static final int FLAG_SELECTED = 1 << 2; @@ -272,4 +275,14 @@ public void onRemoveSwipe () { } helper.onSwipe(); } + + @Override + public TdApi.Message getVisibleMessage () { + return result != null ? result.getMessage() : null; + } + + @Override + public int getVisibleMessageFlags () { + return TdlibMessageViewer.Flags.NO_SENSITIVE_SCREENSHOT_NOTIFICATION; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/passcode/PinButtonView.java b/app/src/main/java/org/thunderdog/challegram/component/passcode/PinButtonView.java index 25c17f62b4..5cd919d89a 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/passcode/PinButtonView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/passcode/PinButtonView.java @@ -25,6 +25,7 @@ import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; @@ -124,7 +125,7 @@ private void buildLayout () { @Override protected void onDraw (Canvas c) { if (number == -1) { - Drawables.draw(c, icon, bigLeft, bigTop, Paints.getPorterDuffPaint(Theme.getColor(ColorId.passcodeText))); + Drawables.draw(c, icon, bigLeft, bigTop, PorterDuffPaint.get(ColorId.passcodeText)); } else { c.drawText(getNumber(), bigLeft, bigTop, Paints.getRegularTextPaint(TEXT_SIZE_BIG, Theme.getColor(ColorId.passcodeText))); c.drawText(getCodes(), smallLeft, smallTop, Paints.getRegularTextPaint(TEXT_SIZE_SMALL, Theme.passcodeSubtitleColor())); diff --git a/app/src/main/java/org/thunderdog/challegram/component/popups/JoinRequestsComponent.java b/app/src/main/java/org/thunderdog/challegram/component/popups/JoinRequestsComponent.java index 4a2013733b..a48de836ad 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/popups/JoinRequestsComponent.java +++ b/app/src/main/java/org/thunderdog/challegram/component/popups/JoinRequestsComponent.java @@ -43,11 +43,11 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.concurrent.TimeUnit; import me.vkryl.android.AnimatorUtils; import me.vkryl.core.ArrayUtils; +import me.vkryl.core.ObjectUtils; public class JoinRequestsComponent implements TGLegacyManager.EmojiLoadListener, Client.ResultHandler { private static final String UTYAN_EMOJI = "\uD83D\uDE0E"; @@ -364,7 +364,7 @@ public int getHeight (int predictUserCount) { } public void search (String query) { - if (Objects.equals(currentQuery, query)) { + if (ObjectUtils.equals(currentQuery, query)) { return; } diff --git a/app/src/main/java/org/thunderdog/challegram/component/popups/MessageSeenController.java b/app/src/main/java/org/thunderdog/challegram/component/popups/MessageSeenController.java index f51a07285b..f84fae3ff1 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/popups/MessageSeenController.java +++ b/app/src/main/java/org/thunderdog/challegram/component/popups/MessageSeenController.java @@ -48,6 +48,7 @@ public class MessageSeenController extends MediaBottomBaseController imple private final TdApi.MessageViewers viewers; public static CharSequence getViewString (TGMessage msg, int count) { + //noinspection SwitchIntDef switch (msg.getMessage().content.getConstructor()) { case TdApi.MessageVoiceNote.CONSTRUCTOR: { return Lang.pluralBold(R.string.MessageSeenXListened, count); @@ -62,6 +63,7 @@ public static CharSequence getViewString (TGMessage msg, int count) { } public static String getNobodyString (TGMessage msg) { + //noinspection SwitchIntDef switch (msg.getMessage().content.getConstructor()) { case TdApi.MessageVoiceNote.CONSTRUCTOR: { return Lang.getString(R.string.MessageSeenNobodyListened); diff --git a/app/src/main/java/org/thunderdog/challegram/component/sharedmedia/MediaSmallView.java b/app/src/main/java/org/thunderdog/challegram/component/sharedmedia/MediaSmallView.java index ff75b9b3a0..b9febce815 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/sharedmedia/MediaSmallView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/sharedmedia/MediaSmallView.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; +import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; import org.thunderdog.challegram.U; import org.thunderdog.challegram.config.Config; @@ -28,6 +29,8 @@ import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.mediaview.data.MediaItem; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; +import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.DrawAlgorithms; @@ -49,7 +52,7 @@ import me.vkryl.core.MathUtils; import me.vkryl.core.lambda.Destroyable; -public class MediaSmallView extends SparseDrawableView implements Destroyable, FactorAnimator.Target, SelectableItemDelegate { +public class MediaSmallView extends SparseDrawableView implements Destroyable, FactorAnimator.Target, SelectableItemDelegate, TdlibUi.MessageProvider { private final ImageReceiver miniThumbnail; private final ImageReceiver preview, imageReceiver; private final GifReceiver gifReceiver; @@ -244,6 +247,10 @@ private void setText (String text) { private static final float SCALE = .24f; + private Receiver findTargetReceiver () { + return item == null ? null : imageReceiver.getCurrentFile() != null ? imageReceiver : gifReceiver; + } + @Override protected void onDraw (Canvas c) { if (item == null) { @@ -260,7 +267,7 @@ protected void onDraw (Canvas c) { c.scale(scale, scale, preview.centerX(), preview.centerY()); } - Receiver receiver = imageReceiver.getCurrentFile() != null ? imageReceiver : gifReceiver; + Receiver receiver = findTargetReceiver(); final boolean scaled = receiver == gifReceiver && item != null && item.getType() == MediaItem.TYPE_VIDEO_MESSAGE; if (scaled) { c.save(); @@ -345,4 +352,21 @@ protected void onDraw (Canvas c) { public void initWithClickDelegate (ClickHelper.Delegate delegate) { helper = new ClickHelper(delegate); } + + // MessageProvider + + + @Override + public TdApi.Message getVisibleMessage () { + return item != null ? item.getMessage() : null; + } + + @Override + public int getVisibleMessageFlags () { + Receiver receiver = findTargetReceiver(); + if (item.hasSpoiler() || receiver == null || receiver.needPlaceholder()) { + return TdlibMessageViewer.Flags.NO_SCREENSHOT_NOTIFICATION; + } + return 0; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerPreviewView.java b/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerPreviewView.java index 201a5aa5e8..1804b8d69a 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerPreviewView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerPreviewView.java @@ -33,14 +33,16 @@ import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; -import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGReaction; +import org.thunderdog.challegram.emoji.Emoji; import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.loader.gif.GifActor; @@ -52,6 +54,7 @@ import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.ColorState; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ThemeChangeListener; import org.thunderdog.challegram.theme.ThemeListenerList; @@ -59,6 +62,7 @@ import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Fonts; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; @@ -66,6 +70,8 @@ import org.thunderdog.challegram.widget.NoScrollTextView; import org.thunderdog.challegram.widget.PopupLayout; +import java.util.ArrayList; + import me.vkryl.android.AnimatorUtils; import me.vkryl.android.animator.FactorAnimator; import me.vkryl.android.widget.FrameLayoutFix; @@ -88,7 +94,6 @@ public class StickerPreviewView extends FrameLayoutFix implements FactorAnimator private final GifReceiver gifReceiver; private final ImageReceiver preview; private Drawable defaultPremiumStarDrawable; - private boolean isEmojiStatus; private final ImageReceiver effectImageReceiver; private final GifReceiver effectGifReceiver; @@ -134,9 +139,11 @@ public void onThemeColorsChanged (boolean areTemp, ColorState state) { } private StickerSmallView controllerView; + private @PorterDuffColorId int repaintingColorId = ColorId.iconActive; public void setControllerView (StickerSmallView stickerView) { this.controllerView = stickerView; + this.repaintingColorId = stickerView != null ? stickerView.getThemedColorId() : ColorId.iconActive; } @Override @@ -304,13 +311,8 @@ private void layoutReceivers () { private @Nullable EmojiString emojiString; private boolean disableEmojis; - private long emojiStatusId; private Path contour; - public void setIsEmojiStatus (boolean emojiStatus) { - isEmojiStatus = emojiStatus; - } - private void setSticker (TGStickerObj sticker, boolean loadPreview) { setSticker(sticker, null, loadPreview); } @@ -337,7 +339,6 @@ private void setSticker (TGStickerObj sticker, @Nullable TGStickerObj effectStic } else { defaultPremiumStarDrawable = null; } - this.emojiStatusId = TD.getStickerCustomEmojiId(sticker.getSticker()); if (currentEffectSticker != null && currentEffectSticker.isAnimated()) { GifActor.addFreezeReason(currentEffectSticker.getFullAnimation(), false); @@ -462,6 +463,46 @@ public void getOutline (View view, android.graphics.Outline outline) { FrameLayoutFix.LayoutParams params = FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, Screen.dp(48f) + menu.getPaddingTop() + menu.getPaddingBottom(), Gravity.CENTER_HORIZONTAL); params.topMargin = getStickerCenterY() + stickerHeight / 2 + Screen.dp(32f); menu.setLayoutParams(params); + + StickerPreviewView.MenuStickerPreviewCallback menuStickerPreviewCallback = controllerView != null ? + controllerView.getMenuStickerPreviewCallback() : null; + + if (menuStickerPreviewCallback != null && sticker != null) { + ArrayList menuItems = new ArrayList<>(5); + menuStickerPreviewCallback.buildMenuStickerPreview(menuItems, sticker, controllerView); + + View.OnClickListener onClickListener = view -> menuStickerPreviewCallback.onMenuStickerPreviewClick(view, findRoot(), sticker, controllerView); + View.OnLongClickListener onLongClickListener = view -> menuStickerPreviewCallback.onMenuStickerPreviewLongClick(view, findRoot(), sticker, controllerView); + + for (int a = 0; a < menuItems.size(); a++) { + final MenuItem item = menuItems.get(a); + final View view; + + if (item.type == MenuItem.MENU_ITEM_TEXT) { + view = makeMenuButtonText(getContext(), item, onClickListener, onLongClickListener, a == 0, a == menuItems.size() - 1, themeListenerList); + } else { + throw new IllegalArgumentException(); + } + + if (Lang.rtl()) { + menu.addView(view, 0); + } else { + menu.addView(view); + } + } + + menu.setAlpha(0f); + addView(menu); + + applyMaximumMenuItemsWidth(); + + setMenuVisible(true, true); + return; + } + + + // Deprecated ??? + View.OnClickListener onClickListener = v -> { final int viewId = v.getId(); if (viewId == R.id.btn_favorite) { @@ -486,86 +527,18 @@ public void getOutline (View view, android.graphics.Outline outline) { } } else if (viewId == R.id.btn_removeRecent) { final int stickerId = sticker.getId(); - tdlib.client().send(new TdApi.RemoveRecentSticker(false, new TdApi.InputFileId(stickerId)), tdlib.okHandler()); - closePreviewIfNeeded(); - } else if (viewId == R.id.btn_setEmojiStatus) { - final long emojiId = sticker.getCustomEmojiId(); - controllerView.onSetEmojiStatus(v, sticker, emojiId, 0); - tdlib.client().send(new TdApi.SetEmojiStatus(new TdApi.EmojiStatus(emojiId), 0), tdlib.okHandler()); - closePreviewIfNeeded(); - } else if (viewId == R.id.btn_setEmojiStatusTimed) { - ViewController context = findRoot(); - if (context != null) { - context.showOptions(null, new int[] { - R.id.btn_setEmojiStatusTimed1Hour, - R.id.btn_setEmojiStatusTimed2Hours, - R.id.btn_setEmojiStatusTimed8Hours, - R.id.btn_setEmojiStatusTimed2Days, - R.id.btn_setEmojiStatusTimedCustom, - }, new String[] { - Lang.getString(R.string.SetEmojiAsStatusTimed1Hour), - Lang.getString(R.string.SetEmojiAsStatusTimed2Hours), - Lang.getString(R.string.SetEmojiAsStatusTimed8Hours), - Lang.getString(R.string.SetEmojiAsStatusTimed2Days), - Lang.getString(R.string.SetEmojiAsStatusTimedCustom) - }, new int[] { - ViewController.OPTION_COLOR_NORMAL, - ViewController.OPTION_COLOR_NORMAL, - ViewController.OPTION_COLOR_NORMAL, - ViewController.OPTION_COLOR_NORMAL, - ViewController.OPTION_COLOR_NORMAL, - }, new int[] { - R.drawable.baseline_access_time_24, - R.drawable.baseline_access_time_24, - R.drawable.baseline_access_time_24, - R.drawable.baseline_access_time_24, - R.drawable.baseline_date_range_24 - }, (optionItemView, id) -> { - final long emojiId = sticker.getCustomEmojiId(); - if (id == R.id.btn_setEmojiStatusTimedCustom) { - int titleRes, todayRes, tomorrowRes, futureRes; - titleRes = R.string.SetEmojiAsStatus; - todayRes = R.string.SetTodayAt; - tomorrowRes = R.string.SetTomorrowAt; - futureRes = R.string.SetDateAt; - - context.showDateTimePicker(tdlib, Lang.getString(titleRes), todayRes, tomorrowRes, futureRes, millis -> { - int duration = (int) ((millis - System.currentTimeMillis()) / 1000L); - controllerView.onSetEmojiStatus(v, sticker, emojiId, duration); - tdlib.client().send(new TdApi.SetEmojiStatus(new TdApi.EmojiStatus(emojiId), duration), tdlib.okHandler()); - closePreviewIfNeeded(); - }, null); - return true; - } - - final int duration; - if (id == R.id.btn_setEmojiStatusTimed1Hour) { - duration = 60 * 60; - } else if (id == R.id.btn_setEmojiStatusTimed2Hours) { - duration = 2 * 60 * 60; - } else if (id == R.id.btn_setEmojiStatusTimed8Hours) { - duration = 8 * 60 * 60; - } else if (id == R.id.btn_setEmojiStatusTimed2Days) { - duration = 2 * 24 * 60 * 60; - } else { - duration = 0; - } - controllerView.onSetEmojiStatus(v, sticker, emojiId, duration); - tdlib.client().send(new TdApi.SetEmojiStatus(new TdApi.EmojiStatus(emojiId), duration), tdlib.okHandler()); - closePreviewIfNeeded(); - return true; - }); + if (sticker.isCustomEmoji()) { + Emoji.instance().removeRecentCustomEmoji(sticker.getCustomEmojiId()); + } else { + tdlib.client().send(new TdApi.RemoveRecentSticker(false, new TdApi.InputFileId(stickerId)), tdlib.okHandler()); } + closePreviewIfNeeded(); } else { closePreviewIfNeeded(); } }; themeListenerList.addThemeInvalidateListener(menu); - if (isEmojiStatus || sticker.isDefaultPremiumStar()) { - makeMenuForEmojiStatus(sticker, onClickListener); - } else { - makeMenuForSticker(sticker, onClickListener); - } + makeMenuForSticker(sticker, onClickListener); menu.setAlpha(0f); addView(menu); @@ -574,63 +547,28 @@ public void getOutline (View view, android.graphics.Outline outline) { setMenuVisible(true, true); } - private void makeMenuForEmojiStatus (final TGStickerObj sticker, final View.OnClickListener onClickListener) { - TextView sendView = new NoScrollTextView(getContext()); - sendView.setId(R.id.btn_setEmojiStatus); - sendView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15f); - sendView.setTypeface(Fonts.getRobotoMedium()); - sendView.setTextColor(Theme.getColor(ColorId.textNeutral)); - sendView.setOnClickListener(onClickListener); - themeListenerList.addThemeColorListener(sendView, ColorId.textNeutral); - Views.setMediumText(sendView, Lang.getString(R.string.SetEmojiAsStatus).toUpperCase()); - sendView.setOnClickListener(onClickListener); - RippleSupport.setTransparentBlackSelector(sendView); - int paddingLeft = Screen.dp(16f); - int paddingRight = Screen.dp(12); - sendView.setPadding(Lang.rtl() ? paddingRight : paddingLeft, 0, Lang.rtl() ? paddingLeft : paddingRight, 0); - sendView.setGravity(Gravity.CENTER); - sendView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); - if (Lang.rtl()) - menu.addView(sendView, 0); - else - menu.addView(sendView); - - TextView viewView = new NoScrollTextView(getContext()); - viewView.setId(R.id.btn_setEmojiStatusTimed); - viewView.setTypeface(Fonts.getRobotoMedium()); - viewView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15f); - viewView.setTextColor(Theme.getColor(ColorId.textNeutral)); - Views.setMediumText(viewView, Lang.getString(R.string.SetEmojiAsStatusTimed).toUpperCase()); - themeListenerList.addThemeColorListener(viewView, ColorId.textNeutral); - viewView.setOnClickListener(onClickListener); - RippleSupport.setTransparentBlackSelector(viewView); - viewView.setPadding(Screen.dp(Lang.rtl() ? 16f : 12f), 0, Screen.dp(Lang.rtl() ? 12f : 16f), 0); - viewView.setGravity(Gravity.CENTER); - viewView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); - if (Lang.rtl()) - menu.addView(viewView, 0); - else - menu.addView(viewView); - } - + @Deprecated private void makeMenuForSticker (final TGStickerObj sticker, final View.OnClickListener onClickListener) { boolean isFavorite = tdlib.isStickerFavorite(sticker.getId()); - - ImageView imageView = new ImageView(getContext()); - imageView.setId(R.id.btn_favorite); - imageView.setScaleType(ImageView.ScaleType.CENTER); - imageView.setOnClickListener(onClickListener); - imageView.setImageResource(isFavorite ? R.drawable.baseline_star_24 : R.drawable.baseline_star_border_24); - imageView.setColorFilter(Theme.getColor(ColorId.textNeutral)); - themeListenerList.addThemeFilterListener(imageView, ColorId.textNeutral); - imageView.setLayoutParams(new ViewGroup.LayoutParams(Screen.dp(48f), ViewGroup.LayoutParams.MATCH_PARENT)); - imageView.setPadding(Lang.rtl() ? 0 : Screen.dp(8f), 0, Lang.rtl() ? Screen.dp(8f) : 0, 0); - RippleSupport.setTransparentBlackSelector(imageView); - Views.setClickable(imageView); - if (Lang.rtl()) - menu.addView(imageView, 0); - else - menu.addView(imageView); + boolean isEmoji = sticker.isCustomEmoji(); + + if (!isEmoji) { + ImageView imageView = new ImageView(getContext()); + imageView.setId(R.id.btn_favorite); + imageView.setScaleType(ImageView.ScaleType.CENTER); + imageView.setOnClickListener(onClickListener); + imageView.setImageResource(isFavorite ? R.drawable.baseline_star_24 : R.drawable.baseline_star_border_24); + imageView.setColorFilter(Theme.getColor(ColorId.textNeutral)); + themeListenerList.addThemeFilterListener(imageView, ColorId.textNeutral); + imageView.setLayoutParams(new ViewGroup.LayoutParams(Screen.dp(48f), ViewGroup.LayoutParams.MATCH_PARENT)); + imageView.setPadding(Lang.rtl() ? 0 : Screen.dp(8f), 0, Lang.rtl() ? Screen.dp(8f) : 0, 0); + RippleSupport.setTransparentBlackSelector(imageView); + Views.setClickable(imageView); + if (Lang.rtl()) + menu.addView(imageView, 0); + else + menu.addView(imageView); + } boolean needViewPackButton = sticker.needViewPackButton(); @@ -640,7 +578,7 @@ private void makeMenuForSticker (final TGStickerObj sticker, final View.OnClickL sendView.setTypeface(Fonts.getRobotoMedium()); sendView.setTextColor(Theme.getColor(ColorId.textNeutral)); themeListenerList.addThemeColorListener(sendView, ColorId.textNeutral); - Views.setMediumText(sendView, Lang.getString(R.string.SendSticker).toUpperCase()); + Views.setMediumText(sendView, Lang.getString(isEmoji ? R.string.PasteCustomEmoji : R.string.SendSticker).toUpperCase()); sendView.setOnClickListener(onClickListener); RippleSupport.setTransparentBlackSelector(sendView); int paddingLeft = Screen.dp(12f); @@ -820,7 +758,7 @@ public StickerView (Context context) { @Override protected void onDraw (Canvas c) { - final boolean needRepainting = currentSticker != null && currentSticker.isNeedRepainting(); + final boolean needThemedColorFilter = currentSticker != null && currentSticker.needThemedColorFilter(); final boolean savedAppear = appearFactor != 1f; if (savedAppear) { final float fromScale = (float) fromWidth / (float) stickerWidth; @@ -837,6 +775,14 @@ protected void onDraw (Canvas c) { boolean animated = currentSticker != null && currentSticker.isAnimated(); Receiver receiver = animated ? gifReceiver : imageReceiver; + if (needThemedColorFilter) { + preview.setThemedPorterDuffColorId(repaintingColorId); + receiver.setThemedPorterDuffColorId(repaintingColorId); + } else { + preview.disablePorterDuffColorFilter(); + receiver.disablePorterDuffColorFilter(); + } + if (emojiString != null) { int textX = receiver.centerX() - emojiString.getWidth() / 2; int textY = receiver.centerY() - stickerMaxWidth / 2 - Screen.dp(58f); @@ -852,15 +798,6 @@ protected void onDraw (Canvas c) { } } - if (needRepainting) { - c.saveLayerAlpha( - receiver.getLeft() - receiver.getWidth() / 4f, - receiver.getTop() - receiver.getHeight() / 4f, - receiver.getRight() + receiver.getWidth() / 4f, - receiver.getBottom() + receiver.getHeight() / 4f, - 255, Canvas.ALL_SAVE_FLAG); - } - final boolean savedReplace = replaceFactor != 0f; if (savedReplace) { c.save(); @@ -870,7 +807,7 @@ protected void onDraw (Canvas c) { float s = ((float) receiver.getWidth()) / Screen.dp(30); c.save(); c.scale(s, s, receiver.centerX(), receiver.centerY()); - Drawables.drawCentered(c, defaultPremiumStarDrawable, receiver.centerX(), receiver.centerY(), null); + Drawables.drawCentered(c, defaultPremiumStarDrawable, receiver.centerX(), receiver.centerY(), PorterDuffPaint.get(repaintingColorId)); c.restore(); } else if (animated) { if (receiver.needPlaceholder()) { @@ -896,27 +833,19 @@ protected void onDraw (Canvas c) { } if (currentEffectSticker != null) { - if (effectGifReceiver.needPlaceholder()) { - effectPreview.draw(c); + Receiver target = effectGifReceiver.needPlaceholder() ? effectPreview : effectGifReceiver; + if (needThemedColorFilter) { + target.setThemedPorterDuffColorId(repaintingColorId); } else { - effectGifReceiver.draw(c); + target.disablePorterDuffColorFilter(); } + target.draw(c); } if (savedReplace) { c.restore(); } - if (needRepainting) { - c.drawRect( - receiver.getLeft() - receiver.getWidth() / 4f, - receiver.getTop() - receiver.getHeight() / 4f, - receiver.getRight() + receiver.getWidth() / 4f, - receiver.getBottom() + receiver.getHeight() / 4f, - Paints.getSrcInPaint(Theme.getColor(ColorId.iconActive))); - c.restore(); - } - if (savedAppear) { c.restore(); } @@ -1003,4 +932,53 @@ public boolean onTouchEvent (MotionEvent event) { } return true; } + + + public interface MenuStickerPreviewCallback { + void buildMenuStickerPreview (ArrayList menuItems, @NonNull TGStickerObj sticker, @NonNull StickerSmallView stickerSmallView); + void onMenuStickerPreviewClick (View v, ViewController context, @NonNull TGStickerObj sticker, @NonNull StickerSmallView stickerSmallView); + default boolean onMenuStickerPreviewLongClick (View v, ViewController context, @NonNull TGStickerObj sticker, @NonNull StickerSmallView stickerSmallView) { return false; } + } + + public static class MenuItem { + public final static int MENU_ITEM_TEXT = 0; + public final static int MENU_ITEM_IMAGE = 1; // todo + + public final int type; + public final String title; + public final @IdRes int viewId; + public final @ColorId int colorId; + + public MenuItem (int type, String title, @IdRes int viewId, @ColorId int colorId) { + this.type = type; + this.title = title; + this.viewId = viewId; + this.colorId = colorId; + } + } + + private static TextView makeMenuButtonText (Context context, MenuItem item, + View.OnClickListener onClickListener, View.OnLongClickListener onLongClickListener, + boolean isFirst, boolean isLast, ThemeListenerList themeListenerList) { + + TextView buttonView = new NoScrollTextView(context); + buttonView.setId(item.viewId); + buttonView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15f); + buttonView.setTypeface(Fonts.getRobotoMedium()); + buttonView.setTextColor(Theme.getColor(item.colorId)); + buttonView.setGravity(Gravity.CENTER); + buttonView.setOnClickListener(onClickListener); + buttonView.setOnLongClickListener(onLongClickListener); + buttonView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + int paddingLeft = Screen.dp(isFirst ? 16f: 12f); + int paddingRight = Screen.dp(isLast ? 16f : 12f); + buttonView.setPadding(Lang.rtl() ? paddingRight : paddingLeft, 0, Lang.rtl() ? paddingLeft : paddingRight, 0); + + themeListenerList.addThemeColorListener(buttonView, item.colorId); + Views.setMediumText(buttonView, item.title); + RippleSupport.setTransparentBlackSelector(buttonView); + + return buttonView; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerSetWrap.java b/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerSetWrap.java index a8f4836312..dc6a89322f 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerSetWrap.java +++ b/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerSetWrap.java @@ -14,6 +14,7 @@ */ package org.thunderdog.challegram.component.sticker; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.os.Build; @@ -25,16 +26,22 @@ import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.collection.LongSparseArray; + import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.BaseActivity; import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.emoji.MediaStickersAdapter; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TGStickerSetInfo; import org.thunderdog.challegram.navigation.HeaderView; import org.thunderdog.challegram.navigation.NavigationController; import org.thunderdog.challegram.navigation.OverlayView; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.ViewSupport; +import org.thunderdog.challegram.telegram.StickersListener; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibDelegate; import org.thunderdog.challegram.telegram.TdlibUi; @@ -55,20 +62,31 @@ import org.thunderdog.challegram.widget.ProgressComponentView; import org.thunderdog.challegram.widget.ShadowView; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + import me.vkryl.android.AnimatorUtils; import me.vkryl.android.animator.FactorAnimator; import me.vkryl.android.widget.FrameLayoutFix; import me.vkryl.core.ColorUtils; +import me.vkryl.core.collection.LongList; import me.vkryl.core.lambda.CancellableRunnable; +import me.vkryl.core.lambda.RunnableBool; +import me.vkryl.td.Td; -public class StickerSetWrap extends FrameLayoutFix implements StickersListController.StickerSetProvider, StickersListController.OffsetProvider, View.OnClickListener, FactorAnimator.Target, PopupLayout.PopupHeightProvider { - private HeaderView headerView; - private StickersListController stickersController; - private FrameLayoutFix bottomWrap; - private TextView textButton; - private ShadowView topShadow; - private RelativeLayout button; - private ProgressComponentView progressView; +@SuppressLint("ViewConstructor") +public class StickerSetWrap extends FrameLayoutFix implements StickersListController.StickerSetProvider, MediaStickersAdapter.OffsetProvider, View.OnClickListener, FactorAnimator.Target, PopupLayout.PopupHeightProvider, StickersListener { + private final HeaderView headerView; + private final StickersListController stickersController; + private final FrameLayoutFix bottomWrap; + private final TextView textButton; + private final ShadowView topShadow; + private final RelativeLayout button; + private final ProgressComponentView progressView; + + private final HashMap stickerSets = new HashMap<>(); private boolean isOneShot = true; @@ -175,13 +193,14 @@ public StickerSetWrap (Context context, Tdlib tdlib) { topLick = new LickView(context); themeListener.addThemeInvalidateListener(topLick); } - + themeListener.addThemeInvalidateListener(this); updateHeader(); } @Override public void invalidate () { headerView.resetColors(stickersController, null); + super.invalidate(); } @Override @@ -193,7 +212,11 @@ public boolean onStickerClick (View view, TGStickerObj sticker, boolean isMenuCl return false; } if (c instanceof MessagesController && ((MessagesController) c).canWriteMessages()) { - if (((MessagesController) c).onSendSticker(view, sticker, sendOptions)) { + if (sticker.isCustomEmoji()) { + ((MessagesController) c).onEnterCustomEmoji(sticker); + popupLayout.hideWindow(true); + return true; + } else if (((MessagesController) c).onSendSticker(view, sticker, sendOptions)) { popupLayout.hideWindow(true); return true; } @@ -220,18 +243,26 @@ public long getStickerOutputChatId () { } @Override - public boolean canArchiveStickerSet () { - return info.isInstalled && !info.isArchived; + public boolean canArchiveStickerSet (long id) { + TGStickerSetInfo info = stickerSets.get(id); + return info != null && info.isInstalled() && !info.isArchived(); } @Override - public boolean canRemoveStickerSet () { - return !info.isInstalled && info.isArchived; + public boolean canRemoveStickerSet (long id) { + TGStickerSetInfo info = stickerSets.get(id); + return info != null && !info.isInstalled() && info.isArchived(); } @Override - public void removeStickerSet () { - makeRequest(STATE_UNINSTALLED); + public boolean canInstallStickerSet (long id) { + TGStickerSetInfo info = stickerSets.get(id); + return info != null && !info.isInstalled(); + } + + @Override + public void removeStickerSets (long[] setIds) { + makeRequest(STATE_UNINSTALLED, setIds); } @Override @@ -240,8 +271,13 @@ public boolean canViewPack () { } @Override - public void archiveStickerSet () { - archive(); + public void archiveStickerSets (long[] setIds) { + makeRequest(STATE_ARCHIVED, setIds); + } + + @Override + public void installStickerSets (long[] setIds) { + makeRequest(STATE_INSTALLED, setIds); } private float statusBarFactor; @@ -286,9 +322,11 @@ private void updateHeader () { } } - if (headerView != null && headerView.getFilling() != null) { + if (headerView.getFilling() != null) { headerView.getFilling().setShadowAlpha(factor); } + + super.invalidate(); } @Override @@ -351,22 +389,37 @@ public void act () { progressView.animateFactor(1f); } }; - button.postDelayed(scheduledProgress, 180l); + button.postDelayed(scheduledProgress, 180L); } } } - private void archive () { - if (!inProgress) { - makeRequest(STATE_ARCHIVED); - } - } - private static final int STATE_UNINSTALLED = 0; private static final int STATE_ARCHIVED = 1; private static final int STATE_INSTALLED = 2; - private void makeRequest (final int state) { + private final AtomicInteger pendingRequests = new AtomicInteger(); + + private void makeRequest (final int state, final long[] setIds) { + if (setIds.length > 0) { + setInProgress(true); + pendingRequests.getAndSet(setIds.length); + for (long setId : setIds) { + performRequest(state, setId, success -> { + if (pendingRequests.decrementAndGet() == 0) { + setInProgress(false); + if (isOneShot && success) { + popupLayout.hideWindow(true); + } else { + updateButton(true); + } + } + }); + } + } + } + + private void performRequest (final int state, final long setId, RunnableBool after) { final boolean isInstalled, isArchived; switch (state) { case STATE_ARCHIVED: @@ -382,45 +435,90 @@ private void makeRequest (final int state) { isInstalled = isArchived = false; break; } - if (!inProgress) { - setInProgress(true); - tdlib.client().send(new TdApi.ChangeStickerSet(info.id, isInstalled, isArchived), object -> { - final boolean ok = object.getConstructor() == TdApi.Ok.CONSTRUCTOR; - tdlib.ui().post(() -> { - setInProgress(false); - if (ok) { - info.isInstalled = isInstalled; - info.isArchived = isArchived; + tdlib.client().send(new TdApi.ChangeStickerSet(setId, isInstalled, isArchived), object -> { + final boolean ok = object.getConstructor() == TdApi.Ok.CONSTRUCTOR; + tdlib.ui().post(() -> { + if (ok) { + TGStickerSetInfo info = stickerSets.get(setId); + if (info != null) { + if (isArchived) { + info.setIsArchived(); + } else if (isInstalled) { + info.setIsInstalled(); + } else { + info.setIsNotInstalled(); + } switch (state) { case STATE_ARCHIVED: - tdlib.listeners().notifyStickerSetArchived(info); + tdlib.listeners().notifyStickerSetArchived(info.getInfo()); break; case STATE_INSTALLED: - tdlib.listeners().notifyStickerSetInstalled(info); + tdlib.listeners().notifyStickerSetInstalled(info.getInfo()); break; case STATE_UNINSTALLED: - tdlib.listeners().notifyStickerSetRemoved(info); + tdlib.listeners().notifyStickerSetRemoved(info.getInfo()); break; } - if (isOneShot) { - popupLayout.hideWindow(true); - } else { - updateButton(true); - } - } else if (object.getConstructor() == TdApi.Error.CONSTRUCTOR) { - UI.showError(object); - updateButton(true); } - }); + } else if (object.getConstructor() == TdApi.Error.CONSTRUCTOR) { + UI.showError(object); + } + if (after != null) { + after.runWithBool(ok); + } }); + }); + } + + private int getInstalledStickersCount () { + int installedCount = 0; + for(Map.Entry entry : stickerSets.entrySet()) { + TdApi.StickerSetInfo setInfo = entry.getValue().getInfo(); + if (setInfo != null && setInfo.isInstalled && !setInfo.isArchived) { + installedCount++; + } } + + return installedCount; + } + + private @Nullable TdApi.StickerSetInfo getFirstStickersSetInfo () { + for(Map.Entry entry : stickerSets.entrySet()) { + return entry.getValue().getInfo(); + } + + return null; } private void updateButton (boolean animated) { + if (stickerSets.isEmpty()) return; + + final TdApi.StickerSetInfo info = getFirstStickersSetInfo(); + if (info == null) return; + + final int size = stickerSets.size(); + final int installedCount = getInstalledStickersCount(); + final boolean isEmoji = info.stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR; + final boolean isPositive = installedCount != size; + if (info.stickerType.getConstructor() == TdApi.StickerTypeMask.CONSTRUCTOR) { - updateButton(Lang.plural(info.isInstalled && !info.isArchived ? R.string.RemoveXMasks : R.string.AddXMasks, info.size), !info.isInstalled || info.isArchived, animated); + updateButton(Lang.plural(isPositive ? R.string.AddXMasks : R.string.RemoveXMasks, info.size), isPositive, animated); + return; + } + + if (size == 1) { + if (isEmoji) { + updateButton(Lang.plural(isPositive ? R.string.AddXEmoji : R.string.RemoveXEmoji, info.size), isPositive, animated); + } else { + updateButton(Lang.plural(isPositive ? R.string.AddXStickers : R.string.RemoveXStickers, info.size), isPositive, animated); + } } else { - updateButton(Lang.plural(info.isInstalled && !info.isArchived ? R.string.RemoveXStickers : R.string.AddXStickers, info.size), !info.isInstalled || info.isArchived, animated); + int num = installedCount == size ? installedCount : size - installedCount; + if (isEmoji) { + updateButton(Lang.plural(isPositive ? R.string.AddXEmojiPacks : R.string.RemoveXEmojiPacks, num), isPositive, animated); + } else { + updateButton(Lang.plural(isPositive ? R.string.AddXStickerPacks : R.string.RemoveXStickerPacks, num), isPositive, animated); + } } } @@ -430,7 +528,7 @@ private void updateButton (String str, boolean positive, boolean animated) { if (!textButton.getText().toString().equals(str) || textButton.getCurrentTextColor() != Theme.getColor(colorId)) { if (animated) { if (animator == null) { - animator = new FactorAnimator(0, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); + animator = new FactorAnimator(0, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); } else { animator.forceFactor(0f); } @@ -453,23 +551,37 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { updateHeader(); } - private TdApi.StickerSetInfo info; - public void initWithInfo (TdApi.StickerSetInfo info) { - this.info = info; + this.stickerSets.put(info.id, new TGStickerSetInfo(tdlib, info)); updateButton(false); stickersController.setStickerSetInfo(info); addViews(); } public void initWithSet (TdApi.StickerSet set) { - this.info = new TdApi.StickerSetInfo(set.id, set.title, set.name, set.thumbnail, set.thumbnailOutline, set.isInstalled, set.isArchived, set.isOfficial, set.stickerFormat, set.stickerType, false, set.stickers.length, null); + TdApi.StickerSetInfo info = new TdApi.StickerSetInfo( + set.id, set.title, set.name, + set.thumbnail, set.thumbnailOutline, + set.isInstalled, set.isArchived, set.isOfficial, + set.stickerFormat, set.stickerType, set.needsRepainting, set.isAllowedAsChatEmojiStatus, + false, set.stickers.length, + null + ); + this.stickerSets.put(set.id, new TGStickerSetInfo(tdlib, info)); updateButton(false); stickersController.setStickerSetInfo(info); stickersController.setStickers(set.stickers, info.stickerType, set.emojis); addViews(); } + private void initWithSets (long[] ids, boolean isEmojiPacks) { + updateButton(false); + stickersController.setStickerSets(ids); + stickersController.setIsEmojiPack(isEmojiPacks); + stickersController.setLoadStickerSetsListener(this::onMultiStickerSetsLoaded); + addViews(); + } + private void addViews () { headerView.initWithSingleController(stickersController, false); addView(stickersController.getValue()); @@ -481,18 +593,31 @@ private void addViews () { addView(bottomWrap); } + @Override + protected void dispatchDraw (Canvas canvas) { + canvas.drawRect(0, provideOffset() - stickersController.getOffsetScroll(), getMeasuredWidth(), getMeasuredHeight(), Paints.fillingPaint(Theme.fillingColor())); + super.dispatchDraw(canvas); + } + private int getHeaderTop () { return provideOffset() - stickersController.getOffsetScroll(); } + private boolean isScrollByHeader; + @Override public void onScrollFinished () { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (isScrollByHeader) { + isScrollByHeader = false; + return; + } if (Config.USE_FULLSCREEN_NAVIGATION) { if (topLick != null) { if (topLick.factor >= .4f) { stickersController.scrollBy((int) ((float) HeaderView.getTopOffset() * (1f - topLick.factor))); + isScrollByHeader = true; } else { stickersController.scrollBy(-(int) ((float) HeaderView.getTopOffset() * topLick.factor)); } @@ -502,6 +627,7 @@ public void onScrollFinished () { if (statusBarFactor != 0f && statusBarFactor != 1f) { if (statusBarFactor >= .4f) { stickersController.scrollBy(getHeaderTop()); + isScrollByHeader = true; } else { stickersController.scrollBy(-(getStatusBarLimit() - getHeaderTop())); } @@ -540,19 +666,52 @@ public int provideReverseOffset () { @Override public void onClick (View v) { + TdApi.StickerSetInfo info = getFirstStickersSetInfo(); if (info == null || inProgress) { return; } - if (info.isArchived || info.isOfficial) { - if (info.isArchived) { - makeRequest(STATE_INSTALLED); + final int size = stickerSets.size(); + final int installedCount = getInstalledStickersCount(); + final boolean isPositive = installedCount != size; + + LongList stickerSetsToInstall = null; + LongList stickerSetsToArchive = null; + LongList stickerSetsToUninstall = null; + + for (Map.Entry entry : stickerSets.entrySet()) { + TdApi.StickerSetInfo setInfo = entry.getValue().getInfo(); + if (isPositive) { + if (!setInfo.isInstalled || setInfo.isArchived) { + if (stickerSetsToInstall == null) { + stickerSetsToInstall = new LongList(stickerSets.size()); + } + stickerSetsToInstall.append(setInfo.id); + } } else { - archive(); + if (setInfo.isOfficial) { + if (stickerSetsToArchive == null) { + stickerSetsToArchive = new LongList(stickerSets.size()); + } + stickerSetsToArchive.append(setInfo.id); + } else if (setInfo.isInstalled) { + if (stickerSetsToUninstall == null) { + stickerSetsToUninstall = new LongList(stickerSets.size()); + } + stickerSetsToUninstall.append(setInfo.id); + } } - } else { - makeRequest(info.isInstalled ? STATE_UNINSTALLED : STATE_INSTALLED); } + if (stickerSetsToInstall != null) { + makeRequest(STATE_INSTALLED, stickerSetsToInstall.get()); + } + if (stickerSetsToArchive != null) { + makeRequest(STATE_ARCHIVED, stickerSetsToArchive.get()); + } + if (stickerSetsToUninstall != null) { + makeRequest(STATE_ARCHIVED, stickerSetsToUninstall.get()); + } + } // Show @@ -560,14 +719,14 @@ public void onClick (View v) { private PopupLayout popupLayout; public void showStickerSet () { + tdlib.listeners().subscribeToStickerUpdates(this); popupLayout = new PopupLayout(getContext()); popupLayout.setDismissListener(popup -> { stickersController.destroy(); progressView.performDestroy(); + tdlib.listeners().unsubscribeFromStickerUpdates(this); }); - popupLayout.setShowListener(popup -> { - stickersController.setItemAnimator(); - }); + popupLayout.setShowListener(popup -> stickersController.setItemAnimator()); popupLayout.setPopupHeightProvider(this); popupLayout.init(true); popupLayout.setHideKeyboard(); @@ -588,4 +747,76 @@ public static StickerSetWrap showStickerSet (TdlibDelegate context, TdApi.Sticke wrap.showStickerSet(); return wrap; } + + public static StickerSetWrap showStickerSets (TdlibDelegate context, long[] ids, boolean isEmojiPacks) { + StickerSetWrap wrap = new StickerSetWrap(context.context(), context.tdlib()); + wrap.initWithSets(ids, isEmojiPacks); + wrap.showStickerSet(); + return wrap; + } + + + + /* * */ + + private void onMultiStickerSetsLoaded (ArrayList sets) { + for (TdApi.StickerSet set : sets) { + this.stickerSets.put(set.id, new TGStickerSetInfo(tdlib, Td.toStickerSetInfo(set))); + } + + updateButton(true); + } + + + + /* * */ + + private void onUpdateStickerSet (long id, TdApi.StickerSetInfo stickerSet) { + TGStickerSetInfo info = stickerSets.get(id); + if (info != null) { + info.updateState(stickerSet); + } + updateButton(true); + } + + @Override + public void onInstalledStickerSetsUpdated (long[] stickerSetIds, TdApi.StickerType stickerType) { + final LongSparseArray sets = new LongSparseArray<>(stickerSetIds.length); + for (long setId : stickerSetIds) { + sets.put(setId, null); + } + UI.post(() -> { + for(Map.Entry entry : stickerSets.entrySet()) { + TGStickerSetInfo info = entry.getValue(); + TdApi.StickerSetInfo setInfo = info.getInfo(); + if (setInfo != null) { + int i = sets.indexOfKey(info.getId()); + if (i >= 0) { + info.setIsInstalled(); + } else { + info.setIsNotInstalled(); + } + } + } + updateButton(true); + }); + } + + @Override + public void onStickerSetArchived (TdApi.StickerSetInfo stickerSet) { + final long id = stickerSet.id; + UI.post(() -> this.onUpdateStickerSet(id, stickerSet)); + } + + @Override + public void onStickerSetRemoved (TdApi.StickerSetInfo stickerSet) { + final long id = stickerSet.id; + UI.post(() -> this.onUpdateStickerSet(id, stickerSet)); + } + + @Override + public void onStickerSetInstalled (TdApi.StickerSetInfo stickerSet) { + final long id = stickerSet.id; + UI.post(() -> this.onUpdateStickerSet(id, stickerSet)); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerSmallView.java b/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerSmallView.java index 9c44aa3212..9eab67527b 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerSmallView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/sticker/StickerSmallView.java @@ -37,11 +37,14 @@ import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.tool.Views; import me.vkryl.android.ViewUtils; import me.vkryl.android.animator.FactorAnimator; @@ -53,21 +56,18 @@ public class StickerSmallView extends View implements FactorAnimator.Target, Des public static final float PADDING = 8f; private static final Interpolator OVERSHOOT_INTERPOLATOR = new OvershootInterpolator(3.2f); - private ImageReceiver imageReceiver; - private GifReceiver gifReceiver; - private FactorAnimator animator; + private final ImageReceiver imageReceiver; + private final GifReceiver gifReceiver; + private final FactorAnimator animator; private @Nullable TGStickerObj sticker; private @Nullable Drawable premiumStarDrawable; private Path contour; private Tdlib tdlib; private int padding; + private int forceHeight = -1; public StickerSmallView (Context context) { - super(context); - this.imageReceiver = new ImageReceiver(this, 0); - this.gifReceiver = new GifReceiver(this); - this.animator = new FactorAnimator(0, this, OVERSHOOT_INTERPOLATOR, 230l); - this.padding = Screen.dp(PADDING); + this(context, Screen.dp(PADDING)); } public StickerSmallView (Context context, int padding) { @@ -89,6 +89,10 @@ public void setPadding (int padding) { measureReceivers(); } + public void setForceHeight (int forceHeight) { + this.forceHeight = forceHeight; + } + public void setSticker (@Nullable TGStickerObj sticker) { this.sticker = sticker; this.isAnimation = sticker != null && sticker.isAnimated(); @@ -103,6 +107,13 @@ public void setSticker (@Nullable TGStickerObj sticker) { gifReceiver.requestFile(gifFile); } + private boolean isChosen; + + public void setChosen (boolean chosen) { + isChosen = chosen; + invalidate(); + } + public void setIsPremiumStar () { premiumStarDrawable = Drawables.get(R.drawable.baseline_premium_star_28); } @@ -127,21 +138,15 @@ public void performDestroy () { gifReceiver.destroy(); } - private float factor; - private void resetStickerState () { - animator.forceFactor(0f, true); - factor = 0f; + animator.forceFactor(0f); } private static final float MIN_SCALE = .82f; @Override public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { - if (this.factor != factor) { - this.factor = factor; - invalidate(); - } + invalidate(); } @Override @@ -160,9 +165,16 @@ public void setIsTrending () { } private boolean isSuggestion; + private boolean isEmojiSuggestion; public void setIsSuggestion () { isSuggestion = true; + isEmojiSuggestion = false; + } + + public void setIsSuggestion (boolean isEmoji) { + isSuggestion = true; + isEmojiSuggestion = isEmoji; } private boolean emojiDisabled; @@ -174,12 +186,11 @@ public void setEmojiDisabled () { @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { if (isSuggestion) { - super.onMeasure(MeasureSpec.makeMeasureSpec(Screen.dp(72f), MeasureSpec.EXACTLY), heightMeasureSpec); + super.onMeasure(MeasureSpec.makeMeasureSpec(Screen.dp(isEmojiSuggestion ? 36 : 72), MeasureSpec.EXACTLY), heightMeasureSpec); } else if (isTrending) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Screen.smallestSide() / 5, MeasureSpec.EXACTLY)); } else { - //noinspection SuspiciousNameCombination - super.onMeasure(widthMeasureSpec, widthMeasureSpec); + super.onMeasure(widthMeasureSpec, forceHeight > 0 ? MeasureSpec.makeMeasureSpec(forceHeight, MeasureSpec.EXACTLY) : widthMeasureSpec); } measureReceivers(); } @@ -192,29 +203,57 @@ private void measureReceivers () { contour = sticker != null ? sticker.getContour(Math.min(imageReceiver.getWidth(), imageReceiver.getHeight())) : null; } + private @PorterDuffColorId int themedColorId = ColorId.iconActive; + + public void setThemedColorId (@PorterDuffColorId int themedColorId) { + if (this.themedColorId != themedColorId) { + this.themedColorId = themedColorId; + invalidate(); + } + } + + public @PorterDuffColorId int getThemedColorId () { + return themedColorId; + } + + private final Path tmpClipPath = new Path(); + @Override protected void onDraw (Canvas c) { + float factor = animator.getFactor(); + int restoreToCountClip = -1; + if (isChosen) { + float radius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 2f; + c.drawCircle(getMeasuredWidth() / 2f, getMeasuredHeight() / 2f, radius, Paints.fillingPaint(Theme.getColor(ColorId.fillingPositive))); + + tmpClipPath.reset(); + tmpClipPath.addCircle(getMeasuredWidth() / 2f, getMeasuredHeight() / 2f, radius - Screen.dp(1), Path.Direction.CW); + tmpClipPath.close(); + restoreToCountClip = Views.save(c); + c.clipPath(tmpClipPath); + } + float originalScale = sticker != null ? sticker.getDisplayScale() : 1f; boolean saved = originalScale != 1f || factor != 0f; - boolean repainting = sticker != null && sticker.isNeedRepainting(); + boolean needThemedColorFilter = sticker != null && sticker.needThemedColorFilter(); + if (needThemedColorFilter) { + imageReceiver.setThemedPorterDuffColorId(themedColorId); + gifReceiver.setThemedPorterDuffColorId(themedColorId); + } else { + imageReceiver.disablePorterDuffColorFilter(); + gifReceiver.disablePorterDuffColorFilter(); + } + + int restoreToCount = -1; int cx = imageReceiver.centerX(); int cy = imageReceiver.centerY(); if (saved) { - c.save(); + restoreToCount = Views.save(c); float scale = originalScale * (MIN_SCALE + (1f - MIN_SCALE) * (1f - factor)); c.scale(scale, scale, cx, cy); } - if (repainting) { - c.saveLayerAlpha( - cx - imageReceiver.getWidth(), - cy - imageReceiver.getHeight(), - cx + imageReceiver.getWidth(), - cy + imageReceiver.getHeight(), - 255, Canvas.ALL_SAVE_FLAG - ); - } if (premiumStarDrawable != null) { - Drawables.drawCentered(c, premiumStarDrawable, cx, cy, null); + Drawables.drawCentered(c, premiumStarDrawable, cx, cy, PorterDuffPaint.get(themedColorId)); } else if (isAnimation) { if (gifReceiver.needPlaceholder()) { if (imageReceiver.needPlaceholder()) { @@ -232,19 +271,13 @@ protected void onDraw (Canvas c) { if (Config.DEBUG_STICKER_OUTLINES) { imageReceiver.drawPlaceholderContour(c, contour); } - if (repainting) { - c.drawRect( - cx - imageReceiver.getWidth(), - cy - imageReceiver.getHeight(), - cx + imageReceiver.getWidth(), - cy + imageReceiver.getHeight(), - Paints.getSrcInPaint(Theme.getColor(ColorId.iconActive)) - ); - c.restore(); - } if (saved) { - c.restore(); + Views.restore(c, restoreToCount); } + if (isChosen) { + Views.restore(c, restoreToCountClip); + } + // c.drawRect(padding, padding, getMeasuredWidth() - padding, getMeasuredHeight() - padding, Paints.strokeSmallPaint(0XFF00FF00)); } // Touch @@ -277,6 +310,7 @@ private void cancelDelayedPreview () { public interface StickerMovementCallback { boolean onStickerClick (StickerSmallView view, View clickView, TGStickerObj sticker, boolean isMenuClick, TdApi.MessageSendOptions sendOptions); + default boolean onStickerLongClick (StickerSmallView view, TGStickerObj sticker) { return false; } long getStickerOutputChatId (); void setStickerPressed (StickerSmallView view, TGStickerObj sticker, boolean isPressed); boolean canFindChildViewUnder (StickerSmallView view, int recyclerX, int recyclerY); @@ -291,8 +325,7 @@ default void onStickerPreviewChanged (StickerSmallView view, TGStickerObj otherO default int getStickerViewTop (StickerSmallView v) { return -1; } default StickerSmallView getStickerViewUnder (StickerSmallView v, int x, int y) { return null; } default TGReaction getReactionForPreview (StickerSmallView v) { return null; } - default void onSetEmojiStatusFromPreview (StickerSmallView view, View clickView, TGStickerObj sticker, long emojiId, int duration) { } - default boolean isEmojiStatus () { return false; } + default void onSetEmojiStatusFromPreview (StickerSmallView view, View clickView, TGStickerObj sticker, long emojiId, long expirationDate) { } } private @Nullable StickerMovementCallback callback; @@ -462,6 +495,10 @@ private void openPreview () { return; } + if (callback != null && callback.onStickerLongClick(this, sticker)) { + return; + } + getParent().requestDisallowInterceptTouchEvent(true); UI.getContext(getContext()).setOrientationLockFlagEnabled(BaseActivity.ORIENTATION_FLAG_STICKER, true); @@ -501,7 +538,7 @@ private void openPreview () { } } - ((BaseActivity) getContext()).openStickerPreview(tdlib, this, sticker, left + width / 2, top + height / 2 + (callback != null ? callback.getStickersListTop() : 0), Math.min(width, height) - Screen.dp(PADDING) * 2, callback.getViewportHeight(), isSuggestion || emojiDisabled, callback != null && callback.isEmojiStatus()); + ((BaseActivity) getContext()).openStickerPreview(tdlib, this, sticker, left + width / 2, top + height / 2 + (callback != null ? callback.getStickersListTop() : 0), Math.min(width, height) - Screen.dp(PADDING) * 2, callback.getViewportHeight(), isSuggestion || emojiDisabled); } private int getRealLeft () { @@ -541,9 +578,9 @@ public void closePreviewIfNeeded () { } } - public void onSetEmojiStatus (View view, TGStickerObj sticker, long emojiId, int duration) { + public void onSetEmojiStatus (View view, TGStickerObj sticker, long emojiId, long expirationDate) { if (callback != null) { - callback.onSetEmojiStatusFromPreview(this, view, sticker, emojiId, duration); + callback.onSetEmojiStatusFromPreview(this, view, sticker, emojiId, expirationDate); } } @@ -555,6 +592,16 @@ public long getStickerOutputChatId () { return callback != null ? callback.getStickerOutputChatId() : 0; } + private StickerPreviewView.MenuStickerPreviewCallback menuStickerPreviewCallback; + + public StickerPreviewView.MenuStickerPreviewCallback getMenuStickerPreviewCallback () { + return menuStickerPreviewCallback; + } + + public void setMenuStickerPreviewCallback (StickerPreviewView.MenuStickerPreviewCallback menuStickerPreviewCallback) { + this.menuStickerPreviewCallback = menuStickerPreviewCallback; + } + private boolean previewDroppedButStillOpen; private void closePreview (@Nullable MotionEvent e) { diff --git a/app/src/main/java/org/thunderdog/challegram/component/sticker/TGStickerObj.java b/app/src/main/java/org/thunderdog/challegram/component/sticker/TGStickerObj.java index 059971854e..d4a80babde 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/sticker/TGStickerObj.java +++ b/app/src/main/java/org/thunderdog/challegram/component/sticker/TGStickerObj.java @@ -37,7 +37,7 @@ public class TGStickerObj { private GifFile previewAnimation, fullAnimation, premiumFullAnimation; private String foundByEmoji; private TdApi.ReactionType reactionType; - private boolean needRepainting; + private boolean needThemedColorFilter; private boolean isDefaultPremiumStar; private int flags; @@ -80,6 +80,14 @@ public boolean isCustomReaction () { return reactionType != null && reactionType.getConstructor() == TdApi.ReactionTypeCustomEmoji.CONSTRUCTOR; } + public boolean isEmojiReaction () { + return reactionType != null && reactionType.getConstructor() == TdApi.ReactionTypeEmoji.CONSTRUCTOR; + } + + public boolean isCustomEmoji () { + return stickerType != null && stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR; + } + public boolean needGenericAnimation () { return isCustomReaction(); } @@ -98,7 +106,12 @@ public TGStickerObj setReactionType (TdApi.ReactionType reactionType) { } public TdApi.ReactionType getReactionType () { - return reactionType; + if (reactionType != null) { + return reactionType; + } else if (isCustomEmoji()) { + return new TdApi.ReactionTypeCustomEmoji(getCustomEmojiId()); + } + return null; } public boolean set (Tdlib tdlib, @Nullable TdApi.Sticker sticker, TdApi.StickerFullType stickerType, String[] emojis) { @@ -113,7 +126,7 @@ public boolean set (Tdlib tdlib, @Nullable TdApi.Sticker sticker, TdApi.StickerT if (this.sticker == null || sticker == null || this.tdlib != tdlib || !Td.equalsTo(this.sticker, sticker)) { this.tdlib = tdlib; this.sticker = sticker; - this.needRepainting = TD.needRepainting(sticker); + this.needThemedColorFilter = TD.needThemedColorFilter(sticker); this.fullImage = null; this.previewAnimation = null; this.fullAnimation = null; @@ -122,9 +135,9 @@ public boolean set (Tdlib tdlib, @Nullable TdApi.Sticker sticker, TdApi.StickerT if (sticker != null && (sticker.thumbnail != null || !Td.isAnimated(sticker.format))) { this.preview = TD.toImageFile(tdlib, sticker.thumbnail); if (this.preview != null) { - this.preview.setSize(Screen.dp(82f)); - this.preview.setWebp(); - this.preview.setScaleType(ImageFile.FIT_CENTER); + this.preview.setSize(Screen.dp(isCustomEmoji() ? 40f : 82f)); // In some cases, emoji are drawn at more than 40 dp; + this.preview.setWebp(); // perhaps, in order not to lose quality when scaling, + this.preview.setScaleType(ImageFile.FIT_CENTER); // it is worth adding an arbitrary preview size } } else { this.preview = null; @@ -138,6 +151,16 @@ public long getStickerSetId () { return stickerSetId != 0 ? stickerSetId : sticker != null ? sticker.setId : 0; } + private long tag; + + public void setTag (long tag) { + this.tag = tag; + } + + public long getTag () { + return tag; + } + public boolean isPremium () { return Td.isPremium(sticker); } @@ -240,8 +263,8 @@ public GifFile getPremiumFullAnimation () { return premiumFullAnimation; } - public boolean isNeedRepainting () { - return needRepainting || isDefaultPremiumStar(); + public boolean needThemedColorFilter () { + return needThemedColorFilter || isDefaultPremiumStar(); } public boolean isDefaultPremiumStar () { @@ -249,7 +272,7 @@ public boolean isDefaultPremiumStar () { } public long getCustomEmojiId () { - return TD.getStickerCustomEmojiId(sticker); + return Td.customEmojiId(sticker); } public void setIsRecent () { @@ -285,11 +308,11 @@ public int getId () { } public int getWidth () { - return isDefaultPremiumStar ? 512: sticker != null ? sticker.width : 0; + return isDefaultPremiumStar ? 512 : sticker != null ? sticker.width : 0; } public int getHeight () { - return isDefaultPremiumStar ? 512: sticker != null ? sticker.height : 0; + return isDefaultPremiumStar ? 512 : sticker != null ? sticker.height : 0; } // If sticker set is not loaded yet diff --git a/app/src/main/java/org/thunderdog/challegram/component/user/RemoveHelper.java b/app/src/main/java/org/thunderdog/challegram/component/user/RemoveHelper.java index 7cd39f6012..dcbf2a44a4 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/user/RemoveHelper.java +++ b/app/src/main/java/org/thunderdog/challegram/component/user/RemoveHelper.java @@ -20,6 +20,7 @@ import android.view.View; import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; @@ -138,7 +139,7 @@ public void draw (Canvas c) { float alpha = (1f - fadeFactor); - Paint bitmapPaint = Paints.getPorterDuffPaint(0xffffffff); + Paint bitmapPaint = Paints.whitePorterDuffPaint(); if (alpha < 1f) { bitmapPaint.setAlpha((int) (255f * alpha)); } @@ -175,6 +176,9 @@ public interface ExtendedCallback extends Callback { boolean onMove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target); void onCompleteMovement (int fromPosition, int toPosition); boolean isLongPressDragEnabled (); + default boolean canDropOver (RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) { + return true; + } } public static ItemTouchHelper attach (RecyclerView recyclerView, final Callback callback) { @@ -247,6 +251,14 @@ public boolean onMove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHo return false; } + @Override + public boolean canDropOver (@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder current, @NonNull RecyclerView.ViewHolder target) { + if (callback instanceof ExtendedCallback) { + return ((ExtendedCallback) callback).canDropOver(recyclerView, current, target); + } + return super.canDropOver(recyclerView, current, target); + } + private void reallyMoved (int from, int to) { if (callback instanceof ExtendedCallback) { ((ExtendedCallback) callback).onCompleteMovement(from, to); diff --git a/app/src/main/java/org/thunderdog/challegram/component/user/UserView.java b/app/src/main/java/org/thunderdog/challegram/component/user/UserView.java index dde972bec9..fc04fc0513 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/user/UserView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/user/UserView.java @@ -33,8 +33,10 @@ import org.thunderdog.challegram.data.TGUser; import org.thunderdog.challegram.loader.AvatarReceiver; import org.thunderdog.challegram.loader.ComplexReceiver; +import org.thunderdog.challegram.loader.ComplexReceiverProvider; import org.thunderdog.challegram.navigation.TooltipOverlayView; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibContactManager; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; @@ -57,7 +59,7 @@ import me.vkryl.core.StringUtils; import me.vkryl.core.lambda.Destroyable; -public class UserView extends BaseView implements Destroyable, RemoveHelper.RemoveDelegate, TooltipOverlayView.LocationProvider { +public class UserView extends BaseView implements Destroyable, RemoveHelper.RemoveDelegate, TooltipOverlayView.LocationProvider, ComplexReceiverProvider { /*private static final int ACCENT_COLOR = 0xff569ace; private static final int DECENT_COLOR = 0xff8a8a8a;*/ @@ -77,7 +79,7 @@ public class UserView extends BaseView implements Destroyable, RemoveHelper.Remo public static final float DEFAULT_HEIGHT = 72f; - private RemoveHelper removeHelper; + private final RemoveHelper removeHelper; public UserView (Context context, Tdlib tdlib) { super(context, tdlib); @@ -146,6 +148,7 @@ public void detachReceiver () { avatarReceiver.detach(); } + @Override public ComplexReceiver getComplexReceiver () { return complexReceiver; } @@ -174,6 +177,13 @@ public void updateSubtext () { statusWidth = 0; } float availWidth = getMeasuredWidth() - textLeftMargin - offsetLeft - paddingRight - (unregisteredContact != null ? Screen.dp(32f) : 0); + if (drawModifiers != null) { + int maxWidth = 0; + for (DrawModifier modifier : drawModifiers) { + maxWidth = Math.max(maxWidth, modifier.getWidth()); + } + availWidth -= maxWidth; + } if (availWidth > 0) { sourceStatus = status; if (statusWidth > availWidth) { @@ -204,11 +214,17 @@ public void updateName () { name = null; } float availWidth = getMeasuredWidth() - textLeftMargin - offsetLeft - paddingRight - (unregisteredContact != null ? Screen.dp(32f) : 0); - - emojiStatusHelper.updateEmoji(user != null ? user.getUser(): null, new TextColorSetOverride(TextColorSets.Regular.NORMAL) { + if (drawModifiers != null) { + int maxWidth = 0; + for (DrawModifier modifier : drawModifiers) { + maxWidth = Math.max(maxWidth, modifier.getWidth()); + } + availWidth -= maxWidth - Screen.dp(12); + } + emojiStatusHelper.updateEmoji(user != null ? user.getUser() : null, new TextColorSetOverride(TextColorSets.Regular.NORMAL) { @Override - public int emojiStatusColor () { - return Theme.getColor(ColorId.iconActive); + public long mediaTextComplexColor () { + return Theme.newComplexColor(true, ColorId.iconActive); } }); if (emojiStatusHelper.needDrawEmojiStatus()) { @@ -275,7 +291,7 @@ public void setUser (@NonNull TGUser user) { private void updateLetters () { if (unregisteredContact != null) { - avatarPlaceholder = new AvatarPlaceholder.Metadata(ColorId.avatarInactive, unregisteredContact.letters.text); + avatarPlaceholder = new AvatarPlaceholder.Metadata(tdlib.accentColor(TdlibAccentColor.InternalId.INACTIVE), unregisteredContact.letters); } else { avatarPlaceholder = null; } @@ -313,7 +329,7 @@ protected void onDraw (Canvas c) { if (trimmedName != null) { trimmedName.draw(c, offsetLeft + textLeftMargin, (int) ((height - Screen.dp(DEFAULT_HEIGHT)) / 2f + Screen.dp(17f))); } - emojiStatusHelper.draw(c, offsetLeft + textLeftMargin + (trimmedName != null ? trimmedName.getWidth(): 0) + Screen.dp(6f), (int) ((height - Screen.dp(DEFAULT_HEIGHT)) / 2f + Screen.dp(17f))); + emojiStatusHelper.draw(c, offsetLeft + textLeftMargin + (trimmedName != null ? trimmedName.getWidth() : 0) + Screen.dp(6f), (int) ((height - Screen.dp(DEFAULT_HEIGHT)) / 2f + Screen.dp(17f))); if (trimmedStatus != null) { statusPaint.setColor(Theme.getColor(user != null && user.isOnline() ? ColorId.textNeutral : ColorId.textLight)); c.drawText(trimmedStatus, rtl ? viewWidth - offsetLeft - textLeftMargin - trimmedStatusWidth : offsetLeft + textLeftMargin, @@ -338,13 +354,13 @@ protected void onDraw (Canvas c) { int x1, x2; x1 = x - width; x2 = x; - c.drawRect(viewWidth - x2, centerY - height / 2, viewWidth - x1, centerY + height / 2 + height % 2, paint); + c.drawRect(viewWidth - x2, centerY - height / 2f, viewWidth - x1, centerY + height / 2f + height % 2, paint); x1 = x - width / 2 - height / 2; x2 = x - width / 2 + height / 2 + height % 2; - c.drawRect(viewWidth - x2, centerY - width / 2, viewWidth - x1, centerY + width / 2 + width % 2, paint); + c.drawRect(viewWidth - x2, centerY - width / 2f, viewWidth - x1, centerY + width / 2f + width % 2, paint); } else { - c.drawRect(x - width, centerY - height / 2, x, centerY + height / 2 + height % 2, paint); - c.drawRect(x - width / 2 - height / 2, centerY - width / 2, x - width / 2 + height / 2 + height % 2, centerY + width / 2 + width % 2, paint); + c.drawRect(x - width, centerY - height / 2f, x, centerY + height / 2f + height % 2, paint); + c.drawRect(x - width / 2f - height / 2f, centerY - width / 2f, x - width / 2f + height / 2f + height % 2, centerY + width / 2f + width % 2, paint); } } diff --git a/app/src/main/java/org/thunderdog/challegram/config/Config.java b/app/src/main/java/org/thunderdog/challegram/config/Config.java index cdd7d51445..22eea096fa 100644 --- a/app/src/main/java/org/thunderdog/challegram/config/Config.java +++ b/app/src/main/java/org/thunderdog/challegram/config/Config.java @@ -29,9 +29,14 @@ public class Config { public static final boolean SUPPORT_SYSTEM_UNDERLINE_SPAN = true; - public static final boolean COMMENTS_INLINE_BUTTON_SEPARATOR_1PX = false; public static final @Dimension(unit = Dimension.DP) int COMMENTS_BUBBLE_BUTTON_MIN_WIDTH = 200; public static final boolean SHOW_CHANNEL_POST_REPLY_INFO_IN_COMMENTS = true; + public static final boolean CHAT_FOLDERS_ENABLED = true; + public static final boolean CHAT_FOLDERS_SMART_CHAT_DELETION_ENABLED = true; + public static final boolean CHAT_FOLDERS_HIDE_BOTTOM_BAR_ON_SCROLL = true; + public static final boolean CHAT_FOLDERS_APPEARANCE_IS_GLOBAL = true; + public static final boolean RESTRICT_HIDING_MAIN_LIST = true; + public static final boolean SEARCH_MESSAGES_ONLY_IN_SELECTED_FOLDER = BuildConfig.EXPERIMENTAL; public static final boolean NEED_SILENT_BROADCAST = false; @@ -186,6 +191,7 @@ public static boolean useBundledWebp () { public static boolean useCloudPlayback (TdApi.Message playPauseFile) { if (USE_CLOUD_PLAYER && playPauseFile != null) { + //noinspection SwitchIntDef switch (playPauseFile.content.getConstructor()) { case TdApi.MessageAudio.CONSTRUCTOR: TdApi.Audio audio = ((TdApi.MessageAudio) playPauseFile.content).audio; @@ -268,6 +274,7 @@ public static boolean isThemeDoc (TdApi.Document doc) { public static final boolean DISABLE_PASSWORD_INVISIBILITY = true; public static final boolean DEBUG_STICKER_OUTLINES = false; // BuildConfig.DEBUG; + public static final boolean DEBUG_GIF_OPTIMIZATION_MODE = false; public static final int SUPPORTED_INSTANT_VIEW_VERSION = 2; public static final boolean INSTANT_VIEW_WRONG_LAYOUT = false; @@ -279,7 +286,6 @@ public static boolean isThemeDoc (TdApi.Document doc) { public static final boolean VIDEO_CLOUD_PLAYBACK_AVAILABLE = true; public static final float MAX_ANIMATED_EMOJI_REFRESH_RATE = 30.0f; - public static final boolean LOOP_BIG_CUSTOM_EMOJI = false; public static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider"; @@ -299,4 +305,14 @@ public static boolean isThemeDoc (TdApi.Document doc) { public static final int VOIP_CONNECTION_MIN_LAYER = 65; public static final boolean FORCE_DIRECT_TGVOIP = false; + + public static final boolean ALLOW_SPONSORED_MESSAGE_LINK_COPY = true; + public static final boolean PROTECT_ANONYMOUS_VOTING = false; + public static final boolean PROTECT_ANONYMOUS_REACTIONS = false; + public static final boolean DISABLE_ANONYMOUS_NON_OWNER_REACTIONS = true; + + public static final boolean KEEP_ORIGINAL_EMOJI_WHEN_INPUT_CUSTOM_EMOJI = true; + public static final boolean FORCE_REPLY_WHEN_FORWARDING_WITH_COMMENT = false; + public static final boolean DEBUG_VIEW_MESSAGES = false; + public static final boolean ENABLE_TEXT_ANIMATIONS = false; } diff --git a/app/src/main/java/org/thunderdog/challegram/config/Device.java b/app/src/main/java/org/thunderdog/challegram/config/Device.java index 7789080de4..2f295cdd36 100644 --- a/app/src/main/java/org/thunderdog/challegram/config/Device.java +++ b/app/src/main/java/org/thunderdog/challegram/config/Device.java @@ -85,6 +85,8 @@ private static int parseManufacturer (String manufacturer, String brand) { case "nvidia": return NVIDIA; case "xiaomi": + case "poco": + case "redmi": return XIAOMI; case "zte": return ZTE; @@ -187,4 +189,7 @@ private static int parseProduct (int manufacturer, String product) { public static final boolean ROUND_NOTIFICAITON_IMAGE = true; //MANUFACTURER != XIAOMI; public static final boolean FLYME = !StringUtils.isEmpty(Build.DISPLAY) && Build.DISPLAY.toLowerCase().contains("flyme"); + + // Android >= 13 has builtin clipboard toasts, but MIUI based on Android 13 ships without them + public static final boolean HAS_BUILTIN_CLIPBOARD_TOASTS = IS_XIAOMI ? Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU : Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; } diff --git a/app/src/main/java/org/thunderdog/challegram/core/Lang.java b/app/src/main/java/org/thunderdog/challegram/core/Lang.java index 559899ddb4..c3f4b25da4 100644 --- a/app/src/main/java/org/thunderdog/challegram/core/Lang.java +++ b/app/src/main/java/org/thunderdog/challegram/core/Lang.java @@ -22,7 +22,6 @@ import android.text.Spanned; import android.text.TextUtils; import android.text.style.BackgroundColorSpan; -import android.text.style.CharacterStyle; import android.view.Gravity; import android.widget.RelativeLayout; @@ -37,6 +36,7 @@ import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; +import org.thunderdog.challegram.data.ContentPreview; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.emoji.Emoji; @@ -78,6 +78,7 @@ import me.vkryl.core.StringUtils; import me.vkryl.core.reference.ReferenceList; import me.vkryl.td.ChatId; +import me.vkryl.td.Td; @SuppressWarnings(value = "SpellCheckingInspection") public class Lang { @@ -387,7 +388,7 @@ public static CharSequence getStringImpl (@Nullable TdApi.LanguagePackInfo langu } } - public static CharacterStyle newBoldSpan (boolean needFakeBold) { + public static Object newBoldSpan (boolean needFakeBold) { return TD.toDisplaySpan(new TdApi.TextEntityTypeBold(), null, needFakeBold); } @@ -395,11 +396,11 @@ public static SpanCreator boldCreator () { return (target, argStart, argEnd, argIndex, needFakeBold) -> newBoldSpan(needFakeBold); } - public static CharacterStyle newCodeSpan (boolean needFakeBold) { + public static Object newCodeSpan (boolean needFakeBold) { return TD.toDisplaySpan(new TdApi.TextEntityTypeCode(), null, needFakeBold); } - public static CharacterStyle newItalicSpan (boolean needFakeBold) { + public static Object newItalicSpan (boolean needFakeBold) { return TD.toDisplaySpan(new TdApi.TextEntityTypeItalic(), null, needFakeBold); } @@ -415,11 +416,15 @@ public static SpanCreator entityCreator (TdApi.TextEntityType entity) { return (target, argStart, argEnd, argIndex, needFakeBold) -> TD.toSpan(entity); } - public static CharacterStyle newUserSpan (TdlibDelegate context, long userId) { - return TD.toDisplaySpan(new TdApi.TextEntityTypeMentionName(userId)).setOnClickListener((view, span, clickedText) -> { - context.tdlib().ui().openPrivateProfile(context, userId, null); - return true; - }); + public static Object newUserSpan (TdlibDelegate context, long userId) { + Object span = TD.toDisplaySpan(new TdApi.TextEntityTypeMentionName(userId)); + if (span instanceof CustomTypefaceSpan) { + ((CustomTypefaceSpan) span).setOnClickListener((view, span1, clickedText) -> { + TD.handleLegacyClick(context, clickedText, span1); + return true; + }); + } + return span; } public static CharSequence getString (@StringRes int resId, SpanCreator creator, Object... formatArgs) { @@ -990,13 +995,17 @@ public static String getPinnedMessageText (Tdlib tdlib, TdApi.MessageSender send } String text = TD.getTextFromMessageSpoilerless(message); if (!needPerson) { - if (StringUtils.isEmpty(text)) - text = Lang.lowercase(TD.buildShortPreview(tdlib, message, true)); + if (StringUtils.isEmpty(text)) { + ContentPreview preview = ContentPreview.getNotificationPreview(tdlib, message.chatId, message, true); + text = Lang.lowercase(preview.buildText(false)); + } return Lang.getString(R.string.format_pinned, text); } if (userName == null) { - if (StringUtils.isEmpty(text)) - text = Lang.lowercase(TD.buildShortPreview(tdlib, message, true)); + if (StringUtils.isEmpty(text)) { + ContentPreview preview = ContentPreview.getNotificationPreview(tdlib, message.chatId, message, true); + text = Lang.lowercase(preview.buildText(false)); + } return Lang.getString(R.string.NewPinnedMessage, text); } if (!StringUtils.isEmpty(text)) { @@ -1042,6 +1051,9 @@ public static String getPinnedMessageText (Tdlib tdlib, TdApi.MessageSender send case TdApi.MessageVideoNote.CONSTRUCTOR: res = R.string.ActionPinnedRound; break; + case TdApi.MessageStory.CONSTRUCTOR: + res = R.string.ActionPinnedStory; + break; case TdApi.MessageGame.CONSTRUCTOR: { String gameName = TD.getGameName(((TdApi.MessageGame) message.content).game, true); if (!StringUtils.isEmpty(gameName)) @@ -1049,6 +1061,17 @@ public static String getPinnedMessageText (Tdlib tdlib, TdApi.MessageSender send res = R.string.ActionPinnedGameNoName; break; } + case TdApi.MessageText.CONSTRUCTOR: + case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: + case TdApi.MessageDice.CONSTRUCTOR: + case TdApi.MessageGameScore.CONSTRUCTOR: + case TdApi.MessageInvoice.CONSTRUCTOR: + case TdApi.MessageGiftedPremium.CONSTRUCTOR: + case TdApi.MessagePremiumGiftCode.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayCreated.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayCompleted.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayWinners.CONSTRUCTOR: + case TdApi.MessagePremiumGiveaway.CONSTRUCTOR: case TdApi.MessageBasicGroupChatCreate.CONSTRUCTOR: case TdApi.MessageCall.CONSTRUCTOR: case TdApi.MessageChatAddMembers.CONSTRUCTOR: @@ -1063,18 +1086,35 @@ public static String getPinnedMessageText (Tdlib tdlib, TdApi.MessageSender send case TdApi.MessageContactRegistered.CONSTRUCTOR: case TdApi.MessageCustomServiceAction.CONSTRUCTOR: case TdApi.MessageSupergroupChatCreate.CONSTRUCTOR: - case TdApi.MessageText.CONSTRUCTOR: case TdApi.MessageUnsupported.CONSTRUCTOR: - case TdApi.MessageGameScore.CONSTRUCTOR: - case TdApi.MessageInvoice.CONSTRUCTOR: case TdApi.MessagePassportDataReceived.CONSTRUCTOR: case TdApi.MessagePassportDataSent.CONSTRUCTOR: case TdApi.MessagePaymentSuccessful.CONSTRUCTOR: case TdApi.MessagePaymentSuccessfulBot.CONSTRUCTOR: case TdApi.MessagePinMessage.CONSTRUCTOR: case TdApi.MessageScreenshotTaken.CONSTRUCTOR: - case TdApi.MessageWebsiteConnected.CONSTRUCTOR: + case TdApi.MessageBotWriteAccessAllowed.CONSTRUCTOR: + case TdApi.MessageChatJoinByRequest.CONSTRUCTOR: + case TdApi.MessageChatSetBackground.CONSTRUCTOR: + case TdApi.MessageChatSetTheme.CONSTRUCTOR: + case TdApi.MessageChatShared.CONSTRUCTOR: + case TdApi.MessageForumTopicCreated.CONSTRUCTOR: + case TdApi.MessageForumTopicEdited.CONSTRUCTOR: + case TdApi.MessageForumTopicIsClosedToggled.CONSTRUCTOR: + case TdApi.MessageForumTopicIsHiddenToggled.CONSTRUCTOR: + case TdApi.MessageInviteVideoChatParticipants.CONSTRUCTOR: + case TdApi.MessageProximityAlertTriggered.CONSTRUCTOR: + case TdApi.MessageSuggestProfilePhoto.CONSTRUCTOR: + case TdApi.MessageUsersShared.CONSTRUCTOR: + case TdApi.MessageVideoChatEnded.CONSTRUCTOR: + case TdApi.MessageVideoChatScheduled.CONSTRUCTOR: + case TdApi.MessageVideoChatStarted.CONSTRUCTOR: + case TdApi.MessageWebAppDataReceived.CONSTRUCTOR: + case TdApi.MessageWebAppDataSent.CONSTRUCTOR: break; + default: + Td.assertMessageContent_d40af239(); + throw Td.unsupported(message.content); } String format = Lang.getString(res); int startIndex = format.indexOf("**"); @@ -1428,7 +1468,10 @@ public void makeString (String str, SpannableStringBuilder out, boolean useNewLi while (matcher.find()) { int start = matcher.start(); int end = matcher.end(); - out.setSpan(new CustomTypefaceSpan(Fonts.getRobotoMedium(), ColorId.textNeutral).setEntityType(new TdApi.TextEntityTypeBold()).setFakeBold(Text.needFakeBold(str, start, end)), startIndex + start, startIndex + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + CustomTypefaceSpan span = new CustomTypefaceSpan(Fonts.getRobotoMedium(), ColorId.textNeutral); + span.setTextEntityType(new TdApi.TextEntityTypeBold()); + span.setFakeBold(Text.needFakeBold(str, start, end)); + out.setSpan(span, startIndex + start, startIndex + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (num >= 0) { int index = StringUtils.indexOf(out, "%1$s", startIndex); @@ -3169,6 +3212,10 @@ public static boolean rtl () { return languageRtl; } + public static int reverseGravity () { + return rtl() ? Gravity.LEFT : Gravity.RIGHT; + } + public static int gravity () { return rtl() ? Gravity.RIGHT : Gravity.LEFT; } @@ -3774,7 +3821,7 @@ public static String[] getSupportedLanguagesForTranslate () { }; StringList list = new StringList(supportedLanguagesForTranslate.length); - for (String lang: supportedLanguagesForTranslate) { + for (String lang : supportedLanguagesForTranslate) { if (Lang.getLanguageName(lang, null) != null) { list.append(lang); } @@ -3786,7 +3833,7 @@ public static String[] getSupportedLanguagesForTranslate () { public static @Nullable String getDefaultLanguageToTranslateV2 (@Nullable String sourceLanguage) { ArrayList recents = Settings.instance().getTranslateLanguageRecents(); - for (String lang: recents) { + for (String lang : recents) { if (!StringUtils.equalsOrBothEmpty(lang, sourceLanguage)) { return lang; } @@ -3802,7 +3849,7 @@ public static String[] getSupportedLanguagesForTranslate () { } String[] notTranslatableLanguages = Settings.instance().getAllNotTranslatableLanguages(); - for (String lang: notTranslatableLanguages) { + for (String lang : notTranslatableLanguages) { if (!StringUtils.equalsOrBothEmpty(lang, sourceLanguage)) { return lang; } diff --git a/app/src/main/java/org/thunderdog/challegram/core/WatchDogObserver.java b/app/src/main/java/org/thunderdog/challegram/core/WatchDogObserver.java index ad5f71ad5e..7b66db6a6c 100644 --- a/app/src/main/java/org/thunderdog/challegram/core/WatchDogObserver.java +++ b/app/src/main/java/org/thunderdog/challegram/core/WatchDogObserver.java @@ -170,7 +170,7 @@ public void unregister () { private void onChange (boolean isProbablyImage, boolean isExternal) { synchronized (this) { - if (isProbablyImage && UI.getUiState() == UI.STATE_RESUMED) { + if (isProbablyImage && UI.getUiState() == UI.State.RESUMED) { checkScreenshots(isExternal); } } @@ -179,7 +179,7 @@ private void onChange (boolean isProbablyImage, boolean isExternal) { private void checkScreenshots (final boolean isExternal) { // UI.showToast("Check screenshots", Toast.LENGTH_SHORT); final Tdlib tdlib = TdlibManager.instance().current(); - if (!tdlib.hasOpenChats() || !UI.wasResumedRecently(1000)) { + if (!tdlib.hasPotentiallyVisibleMessages() || !UI.wasResumedRecently(1000)) { return; } diff --git a/app/src/main/java/org/thunderdog/challegram/data/AvatarInfo.java b/app/src/main/java/org/thunderdog/challegram/data/AvatarInfo.java index 499b810ef6..114643853c 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/AvatarInfo.java +++ b/app/src/main/java/org/thunderdog/challegram/data/AvatarInfo.java @@ -17,6 +17,7 @@ import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.util.text.Letters; @@ -25,7 +26,7 @@ public class AvatarInfo { public final long userId; public ImageFile imageFile; - public int avatarColorId; + public TdlibAccentColor accentColor; public Letters letters; public float lettersWidth15dp; @@ -39,7 +40,7 @@ public AvatarInfo (Tdlib tdlib, long userId) { public void updateUser () { TdApi.User user = tdlib.cache().user(userId); letters = TD.getLetters(user); - avatarColorId = TD.getAvatarColorId(user, tdlib.myUserId()); + accentColor = tdlib.cache().userAccentColor(user); imageFile = TD.getAvatar(tdlib, user); lettersWidth15dp = Paints.measureLetters(letters, 15f); } diff --git a/app/src/main/java/org/thunderdog/challegram/data/AvatarPlaceholder.java b/app/src/main/java/org/thunderdog/challegram/data/AvatarPlaceholder.java index e171ca1f51..2348102d76 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/AvatarPlaceholder.java +++ b/app/src/main/java/org/thunderdog/challegram/data/AvatarPlaceholder.java @@ -22,7 +22,7 @@ import androidx.annotation.Nullable; import org.thunderdog.challegram.R; -import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Icons; @@ -41,44 +41,55 @@ public class AvatarPlaceholder { public static class Metadata { - public final @ColorId int colorId; - public final @Nullable String letters; + public final @NonNull TdlibAccentColor accentColor; + public final @ColorId int iconColorId; + public final @Nullable Letters letters; public final @DrawableRes int drawableRes, extraDrawableRes; - public Metadata (int colorId, @Nullable Letters letters, int drawableRes, int extraDrawableRes) { - this(colorId, letters != null ? letters.text : null, drawableRes, extraDrawableRes); - } - public Metadata () { - this(ColorId.avatarInactive); + this(new TdlibAccentColor(TdlibAccentColor.InternalId.INACTIVE)); } - public Metadata (int colorId) { - this(colorId, Strings.ELLIPSIS, 0, 0); + public Metadata (@NonNull TdlibAccentColor accentColor) { + this(accentColor, new Letters(Strings.ELLIPSIS), 0, 0); } - public Metadata (int colorId, int iconRes) { - this(colorId, (Letters) null, iconRes, 0); + public Metadata (@NonNull TdlibAccentColor accentColor, int iconRes) { + this(accentColor, null, iconRes, 0); } - public Metadata (int colorId, @Nullable Letters letters) { - this(colorId, letters, 0, 0); + public Metadata (@NonNull TdlibAccentColor accentColor, @Nullable Letters letters) { + this(accentColor, letters, 0, 0); } - public Metadata (int colorId, @Nullable String letters) { - this(colorId, letters, 0, 0); + public Metadata (@NonNull TdlibAccentColor accentColor, @Nullable Letters letters, int drawableRes, int extraDrawableRes) { + this(accentColor, letters, drawableRes, extraDrawableRes, ColorId.avatar_content); } - public Metadata (int colorId, @Nullable String letters, int drawableRes, int extraDrawableRes) { - this.colorId = colorId; - this.letters = letters; + public Metadata (@NonNull TdlibAccentColor accentColor, @Nullable Letters letters, int drawableRes, int extraDrawableRes, @ColorId int iconColorId) { + this.accentColor = accentColor; + this.letters = letters != null && !StringUtils.isEmpty(letters.text) ? letters : null; this.drawableRes = drawableRes; this.extraDrawableRes = extraDrawableRes; + this.iconColorId = iconColorId; } @Override public boolean equals (@Nullable Object obj) { - return obj instanceof Metadata && ((Metadata) obj).colorId == colorId && StringUtils.equalsOrBothEmpty(((Metadata) obj).letters, letters) && ((Metadata) obj).colorId == colorId; + if (obj == this) { + return true; + } + if (obj == null) { + return false; + } + if (obj instanceof Metadata) { + Metadata other = (Metadata) obj; + return other.accentColor.equals(this.accentColor) && StringUtils.equalsOrBothEmpty( + other.letters != null ? other.letters.text : null, + this.letters != null ? this.letters.text : null + ); + } + return false; } } @@ -89,8 +100,8 @@ public boolean equals (@Nullable Object obj) { @NonNull public final Metadata metadata; - public AvatarPlaceholder (float radius, @ColorId int colorId) { - this(radius, new Metadata(colorId), null); + public AvatarPlaceholder (float radius, @NonNull TdlibAccentColor accentColor) { + this(radius, new Metadata(accentColor), null); } public AvatarPlaceholder (float radius, @Nullable Metadata metadata, @Nullable DrawableProvider provider) { @@ -99,7 +110,7 @@ public AvatarPlaceholder (float radius, @Nullable Metadata metadata, @Nullable D } this.metadata = metadata; this.radius = radius; - this.letters = StringUtils.isEmpty(metadata.letters) ? null : new Text.Builder(metadata.letters, Screen.dp(radius) * 3, Paints.robotoStyleProvider((int) (radius * .75f)), TextColorSets.Regular.AVATAR_CONTENT).allBold().singleLine().build(); + this.letters = metadata.letters == null ? null : new Text.Builder(metadata.letters.text, Screen.dp(radius) * 3, Paints.robotoStyleProvider((int) (radius * .75f)), TextColorSets.Regular.AVATAR_CONTENT).allBold().singleLine().build(); if (provider != null) { this.drawable = provider.getSparseDrawable(metadata.drawableRes, ColorId.avatar_content); } else { @@ -115,8 +126,8 @@ public int getRadius () { return Screen.dp(radius); } - public int getColor () { - return Theme.getColor(metadata.colorId); + public TdlibAccentColor getAccentColor () { + return metadata.accentColor; } public void draw (Canvas c, float centerX, float centerY) { @@ -134,8 +145,8 @@ public void draw (Canvas c, float centerX, float centerY, float alpha, float rad public void draw (Canvas c, float centerX, float centerY, float alpha, float radiusPx, boolean drawCircle) { if (alpha <= 0f) return; - if (drawCircle && metadata.colorId != 0) { - c.drawCircle(centerX, centerY, radiusPx, Paints.fillingPaint(ColorUtils.alphaColor(alpha, Theme.getColor(metadata.colorId)))); + if (drawCircle) { + c.drawCircle(centerX, centerY, radiusPx, Paints.fillingPaint(ColorUtils.alphaColor(alpha, metadata.accentColor.getPrimaryColor()))); } if (letters != null) { int currentRadiusPx = Screen.dp(this.radius); @@ -165,7 +176,7 @@ public void draw (Canvas c, float centerX, float centerY, float alpha, float rad } else { saveCount = -1; } - Drawables.draw(c, drawable, centerX - drawable.getMinimumWidth() / 2f, centerY - drawable.getMinimumHeight() / 2f, PorterDuffPaint.get(ColorId.avatar_content, alpha)); + Drawables.draw(c, drawable, centerX - drawable.getMinimumWidth() / 2f, centerY - drawable.getMinimumHeight() / 2f, PorterDuffPaint.get(metadata.iconColorId, alpha)); if (needRestore) { Views.restore(c, saveCount); } diff --git a/app/src/main/java/org/thunderdog/challegram/data/CallItem.java b/app/src/main/java/org/thunderdog/challegram/data/CallItem.java index cdddeb7ed8..990d41080c 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/CallItem.java +++ b/app/src/main/java/org/thunderdog/challegram/data/CallItem.java @@ -21,6 +21,7 @@ import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.tool.Strings; import java.util.ArrayList; @@ -94,12 +95,11 @@ public TdApi.Message lastMessage () { return isOutgoing ? R.drawable.baseline_call_made_18 : isMissed(call) ? R.drawable.baseline_call_missed_18 : R.drawable.baseline_call_received_18; } - public @ColorId - int getSubtitleIconColorId () { + public @PorterDuffColorId int getSubtitleIconColorId () { return getSubtitleIconColorId((TdApi.MessageCall) lastMessage().content); } - public static @ColorId int getSubtitleIconColorId (TdApi.MessageCall call) { + public static @PorterDuffColorId int getSubtitleIconColorId (TdApi.MessageCall call) { return isMissedOrCancelled(call) ? ColorId.iconNegative : ColorId.iconPositive; } diff --git a/app/src/main/java/org/thunderdog/challegram/data/ChatEventUtil.java b/app/src/main/java/org/thunderdog/challegram/data/ChatEventUtil.java index 1e483dde77..42f0d67691 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/ChatEventUtil.java +++ b/app/src/main/java/org/thunderdog/challegram/data/ChatEventUtil.java @@ -17,6 +17,7 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; +import androidx.annotation.StringRes; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; @@ -82,6 +83,9 @@ public static int getActionMessageMode (TdApi.ChatEventAction action) { return ActionMessageMode.SERVICE_AND_FULL; // only service message case TdApi.ChatEventPhotoChanged.CONSTRUCTOR: + case TdApi.ChatEventAccentColorChanged.CONSTRUCTOR: + case TdApi.ChatEventProfileAccentColorChanged.CONSTRUCTOR: + case TdApi.ChatEventEmojiStatusChanged.CONSTRUCTOR: case TdApi.ChatEventMessageUnpinned.CONSTRUCTOR: case TdApi.ChatEventInvitesToggled.CONSTRUCTOR: case TdApi.ChatEventSignMessagesToggled.CONSTRUCTOR: @@ -123,8 +127,11 @@ public static int getActionMessageMode (TdApi.ChatEventAction action) { case TdApi.ChatEventInviteLinkEdited.CONSTRUCTOR: case TdApi.ChatEventAvailableReactionsChanged.CONSTRUCTOR: return ActionMessageMode.ONLY_FULL; + default: { + Td.assertChatEventAction_57377883(); + throw Td.unsupported(action); + } } - throw new UnsupportedOperationException(action.toString()); } @NonNull @@ -146,6 +153,12 @@ private static TGMessage serviceMessage (MessagesManager context, TdApi.Message // only service message case TdApi.ChatEventPhotoChanged.CONSTRUCTOR: return new TGMessageService(context, msg, (TdApi.ChatEventPhotoChanged) action); + case TdApi.ChatEventAccentColorChanged.CONSTRUCTOR: + return new TGMessageService(context, msg, (TdApi.ChatEventAccentColorChanged) action); + case TdApi.ChatEventProfileAccentColorChanged.CONSTRUCTOR: + return new TGMessageService(context, msg, (TdApi.ChatEventProfileAccentColorChanged) action); + case TdApi.ChatEventEmojiStatusChanged.CONSTRUCTOR: + return new TGMessageService(context, msg, (TdApi.ChatEventEmojiStatusChanged) action); case TdApi.ChatEventMessageUnpinned.CONSTRUCTOR: return new TGMessageService(context, msg, (TdApi.ChatEventMessageUnpinned) action); case TdApi.ChatEventInvitesToggled.CONSTRUCTOR: @@ -211,8 +224,11 @@ private static TGMessage serviceMessage (MessagesManager context, TdApi.Message case TdApi.ChatEventInviteLinkEdited.CONSTRUCTOR: case TdApi.ChatEventAvailableReactionsChanged.CONSTRUCTOR: throw new IllegalArgumentException(action.toString()); + default: { + Td.assertChatEventAction_57377883(); + throw Td.unsupported(action); + } } - throw new UnsupportedOperationException(action.toString()); } @NonNull @@ -252,12 +268,12 @@ private static TGMessage fullMessage (MessagesManager context, TdApi.Message msg if (StringUtils.isEmpty(changed.newUsername)) { text = new TdApi.FormattedText("", null); } else { - String link = TD.getLink(changed.newUsername); + String link = tdlib.tMeUrl(changed.newUsername); text = new TdApi.FormattedText(link, new TdApi.TextEntity[] {new TdApi.TextEntity(0, link.length(), new TdApi.TextEntityTypeUrl())}); } fullMessage = new TGMessageText(context, msg, text); if (!StringUtils.isEmpty(changed.oldUsername)) { - String link = TD.getLink(changed.oldUsername); + String link = tdlib.tMeUrl(changed.oldUsername); fullMessage.setFooter(Lang.getString(R.string.EventLogPreviousLink), link, new TdApi.TextEntity[] {new TdApi.TextEntity(0, link.length(), new TdApi.TextEntityTypeUrl())}); } break; @@ -305,16 +321,13 @@ private static TGMessage fullMessage (MessagesManager context, TdApi.Message msg case TdApi.ChatEventMessageEdited.CONSTRUCTOR: { TdApi.ChatEventMessageEdited e = (TdApi.ChatEventMessageEdited) action; fullMessage = TGMessage.valueOf(context, TD.removeWebPage(e.newMessage)); - int footerRes; TdApi.Message oldMessage = TD.removeWebPage(e.oldMessage); TdApi.FormattedText originalText = Td.textOrCaption(oldMessage.content); - switch (oldMessage.content.getConstructor()) { - case TdApi.MessageText.CONSTRUCTOR: - footerRes = R.string.EventLogOriginalMessages; - break; - default: - footerRes = R.string.EventLogOriginalCaption; - break; + final @StringRes int footerRes; + if (Td.isText(oldMessage.content)) { + footerRes = R.string.EventLogOriginalMessages; + } else { + footerRes = R.string.EventLogOriginalCaption; } //noinspection UnsafeOptInUsageError String text = Td.isEmpty(originalText) ? Lang.getString(R.string.EventLogOriginalCaptionEmpty) : originalText.text; @@ -517,12 +530,32 @@ private static TGMessage fullMessage (MessagesManager context, TdApi.Message msg } else if (isAnonymous) { appendRight(b, R.string.EventLogPromotedRemainAnonymous, ((TdApi.ChatMemberStatusCreator) oldStatus).isAnonymous, ((TdApi.ChatMemberStatusCreator) newStatus).isAnonymous, false); } else if (isPromote) { + if (false) { + // Cause compilation error if signature changes + new TdApi.ChatAdministratorRights( + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true + ); + } final TdApi.ChatMemberStatusAdministrator oldAdmin = (TdApi.ChatMemberStatusAdministrator) oldStatus; final TdApi.ChatMemberStatusAdministrator newAdmin = (TdApi.ChatMemberStatusAdministrator) newStatus; appendRight(b, msg.isChannelPost ? R.string.EventLogPromotedManageChannel : R.string.EventLogPromotedManageGroup, oldAdmin.rights.canManageChat, newAdmin.rights.canManageChat, false); appendRight(b, msg.isChannelPost ? R.string.EventLogPromotedChangeChannelInfo : R.string.EventLogPromotedChangeGroupInfo, oldAdmin.rights.canChangeInfo, newAdmin.rights.canChangeInfo, false); if (msg.isChannelPost) { - appendRight(b, R.string.EventLogPromotedPostMessages, oldAdmin.rights.canChangeInfo, newAdmin.rights.canChangeInfo, false); + appendRight(b, R.string.EventLogPromotedPostMessages, oldAdmin.rights.canPostMessages, newAdmin.rights.canPostMessages, false); appendRight(b, R.string.EventLogPromotedEditMessages, oldAdmin.rights.canEditMessages, newAdmin.rights.canEditMessages, false); } appendRight(b, R.string.EventLogPromotedDeleteMessages, oldAdmin.rights.canDeleteMessages, newAdmin.rights.canDeleteMessages, false); @@ -532,7 +565,12 @@ private static TGMessage fullMessage (MessagesManager context, TdApi.Message msg appendRight(b, R.string.EventLogPromotedPinMessages, oldAdmin.rights.canPinMessages, newAdmin.rights.canPinMessages, false); } appendRight(b, msg.isChannelPost ? R.string.EventLogPromotedManageLiveStreams : R.string.EventLogPromotedManageVoiceChats, oldAdmin.rights.canManageVideoChats, newAdmin.rights.canManageVideoChats, false); - if (!msg.isChannelPost) { + if (msg.isChannelPost) { + appendRight(b, R.string.EventLogPromotedPostStories, oldAdmin.rights.canPostStories, newAdmin.rights.canPostStories, false); + appendRight(b, R.string.EventLogPromotedEditStories, oldAdmin.rights.canEditStories, newAdmin.rights.canEditStories, false); + appendRight(b, R.string.EventLogPromotedDeleteStories, oldAdmin.rights.canDeleteStories, newAdmin.rights.canDeleteStories, false); + } else { + appendRight(b, R.string.EventLogPromotedManageTopics, oldAdmin.rights.canManageTopics, newAdmin.rights.canManageTopics, false); appendRight(b, R.string.EventLogPromotedRemainAnonymous, oldAdmin.rights.isAnonymous, newAdmin.rights.isAnonymous, false); } appendRight(b, R.string.EventLogPromotedAddAdmins, oldAdmin.rights.canPromoteMembers, newAdmin.rights.canPromoteMembers, false); @@ -571,6 +609,9 @@ private static TGMessage fullMessage (MessagesManager context, TdApi.Message msg break; } // only service message + case TdApi.ChatEventAccentColorChanged.CONSTRUCTOR: + case TdApi.ChatEventProfileAccentColorChanged.CONSTRUCTOR: + case TdApi.ChatEventEmojiStatusChanged.CONSTRUCTOR: case TdApi.ChatEventMessageUnpinned.CONSTRUCTOR: case TdApi.ChatEventInvitesToggled.CONSTRUCTOR: case TdApi.ChatEventSignMessagesToggled.CONSTRUCTOR: @@ -597,8 +638,10 @@ private static TGMessage fullMessage (MessagesManager context, TdApi.Message msg case TdApi.ChatEventForumTopicToggleIsClosed.CONSTRUCTOR: case TdApi.ChatEventForumTopicToggleIsHidden.CONSTRUCTOR: throw new IllegalArgumentException(action.toString()); - default: - throw new UnsupportedOperationException(action.toString()); + default: { + Td.assertChatEventAction_57377883(); + throw Td.unsupported(action); + } } return fullMessage; } @@ -684,7 +727,7 @@ private static TdApi.MessageContent convertToNativeMessageContent (TdApi.ChatEve TdApi.FormattedText formattedText = new TdApi.FormattedText(b.toString().trim(), new TdApi.TextEntity[] {new TdApi.TextEntity(0, length, new TdApi.TextEntityTypeItalic())}); - return new TdApi.MessageText(formattedText, null); + return new TdApi.MessageText(formattedText, null, null); } case TdApi.ChatEventAvailableReactionsChanged.CONSTRUCTOR: { TdApi.ChatEventAvailableReactionsChanged e = (TdApi.ChatEventAvailableReactionsChanged) event.action; @@ -702,8 +745,10 @@ private static TdApi.MessageContent convertToNativeMessageContent (TdApi.ChatEve newReactions.add(TD.makeReactionKey(type)); } break; - default: - throw new UnsupportedOperationException(e.newAvailableReactions.toString()); + default: { + Td.assertChatAvailableReactions_21c76ded(); + throw Td.unsupported(e.newAvailableReactions); + } } boolean hadAll = false; @@ -798,7 +843,7 @@ private static TdApi.MessageContent convertToNativeMessageContent (TdApi.ChatEve TdApi.FormattedText formattedText = new TdApi.FormattedText(text.toString(), entities.toArray(new TdApi.TextEntity[0])); - return new TdApi.MessageText(formattedText, null); + return new TdApi.MessageText(formattedText, null, null); } case TdApi.ChatEventInviteLinkEdited.CONSTRUCTOR: { TdApi.ChatEventInviteLinkEdited e = (TdApi.ChatEventInviteLinkEdited) event.action; @@ -838,11 +883,14 @@ private static TdApi.MessageContent convertToNativeMessageContent (TdApi.ChatEve } TdApi.FormattedText formattedText = new TdApi.FormattedText(text, Td.findEntities(text)); - return new TdApi.MessageText(formattedText, null); + return new TdApi.MessageText(formattedText, null, null); } // No native message interpretation. case TdApi.ChatEventDescriptionChanged.CONSTRUCTOR: + case TdApi.ChatEventAccentColorChanged.CONSTRUCTOR: + case TdApi.ChatEventProfileAccentColorChanged.CONSTRUCTOR: + case TdApi.ChatEventEmojiStatusChanged.CONSTRUCTOR: case TdApi.ChatEventHasProtectedContentToggled.CONSTRUCTOR: case TdApi.ChatEventInviteLinkDeleted.CONSTRUCTOR: case TdApi.ChatEventInviteLinkRevoked.CONSTRUCTOR: @@ -878,9 +926,11 @@ private static TdApi.MessageContent convertToNativeMessageContent (TdApi.ChatEve case TdApi.ChatEventForumTopicToggleIsClosed.CONSTRUCTOR: throw new IllegalArgumentException(event.action.toString()); - // Unsupported - default: - throw new UnsupportedOperationException(event.action.toString()); + // Unsupported + default: { + Td.assertChatEventAction_57377883(); + throw Td.unsupported(event.action); + } } } } diff --git a/app/src/main/java/org/thunderdog/challegram/data/ComplexMediaItemCustomEmoji.java b/app/src/main/java/org/thunderdog/challegram/data/ComplexMediaItemCustomEmoji.java index 763e1d8c4b..18a7557f9f 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/ComplexMediaItemCustomEmoji.java +++ b/app/src/main/java/org/thunderdog/challegram/data/ComplexMediaItemCustomEmoji.java @@ -26,6 +26,7 @@ import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Views; import me.vkryl.td.Td; @@ -112,6 +113,8 @@ public void draw (Canvas c, Rect rect, ComplexReceiver mediaReceiver, long displ translate = false; } + boolean needThemedColorFilter = TD.needThemedColorFilter(sticker); + Receiver receiver; if (imageFile != null) { receiver = mediaReceiver.getImageReceiver(displayMediaKey); @@ -134,6 +137,11 @@ public void draw (Canvas c, Rect rect, ComplexReceiver mediaReceiver, long displ } if (receiver.needPlaceholder()) { DoubleImageReceiver preview = mediaReceiver.getPreviewReceiver(displayMediaKey); + if (needThemedColorFilter) { + preview.setThemedPorterDuffColorId(ColorId.text); + } else { + preview.disablePorterDuffColorFilter(); + } if (translate) { preview.setBounds(0, 0, rect.right - rect.left, rect.bottom - rect.top); } else { @@ -143,6 +151,11 @@ public void draw (Canvas c, Rect rect, ComplexReceiver mediaReceiver, long displ preview.drawPlaceholderContour(c, outline); } } + if (needThemedColorFilter) { + receiver.setThemedPorterDuffColorId(ColorId.text); + } else { + receiver.disablePorterDuffColorFilter(); + } receiver.draw(c); if (translate) { Views.restore(c, restoreToCount); diff --git a/app/src/main/java/org/thunderdog/challegram/data/ContentPreview.java b/app/src/main/java/org/thunderdog/challegram/data/ContentPreview.java new file mode 100644 index 0000000000..31291a6623 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/data/ContentPreview.java @@ -0,0 +1,1416 @@ +package org.thunderdog.challegram.data; + +import android.util.SparseIntArray; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.tool.Strings; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.unsorted.Settings; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import me.vkryl.core.ArrayUtils; +import me.vkryl.core.CurrencyUtils; +import me.vkryl.core.StringUtils; +import me.vkryl.td.ChatId; +import me.vkryl.td.Td; + +public class ContentPreview { + public static final Emoji + EMOJI_PHOTO = new Emoji("\uD83D\uDDBC", R.drawable.baseline_camera_alt_16); // "\uD83D\uDCF7" + public static final Emoji EMOJI_VIDEO = new Emoji("\uD83C\uDFA5", R.drawable.baseline_videocam_16); // "\uD83D\uDCF9" + public static final Emoji EMOJI_ROUND_VIDEO = new Emoji("\uD83D\uDCF9", R.drawable.deproko_baseline_msg_video_16); + public static final Emoji EMOJI_SECRET_PHOTO = new Emoji("\uD83D\uDD25", R.drawable.deproko_baseline_whatshot_16); + public static final Emoji EMOJI_SECRET_VIDEO = new Emoji("\uD83D\uDD25", R.drawable.deproko_baseline_whatshot_16); + public static final Emoji EMOJI_LINK = new Emoji("\uD83D\uDD17", R.drawable.baseline_link_16); + public static final Emoji EMOJI_GAME = new Emoji("\uD83C\uDFAE", R.drawable.baseline_videogame_asset_16); + public static final Emoji EMOJI_GROUP = new Emoji("\uD83D\uDC65", R.drawable.baseline_group_16); + public static final Emoji EMOJI_GIFT = new Emoji("\uD83C\uDF81", R.drawable.baseline_redeem_16); + public static final Emoji EMOJI_THEME = new Emoji("\uD83C\uDFA8", R.drawable.baseline_palette_16); + public static final Emoji EMOJI_GROUP_INVITE = new Emoji("\uD83D\uDC65", R.drawable.baseline_group_add_16); + public static final Emoji EMOJI_CHANNEL = new Emoji("\uD83D\uDCE2", R.drawable.baseline_bullhorn_16); // "\uD83D\uDCE3" + public static final Emoji EMOJI_FILE = new Emoji("\uD83D\uDCCE", R.drawable.baseline_insert_drive_file_16); + public static final Emoji EMOJI_AUDIO = new Emoji("\uD83C\uDFB5", R.drawable.baseline_music_note_16); + public static final Emoji EMOJI_CONTACT = new Emoji("\uD83D\uDC64", R.drawable.baseline_person_16); + public static final Emoji EMOJI_POLL = new Emoji("\uD83D\uDCCA", R.drawable.baseline_poll_16); + public static final Emoji EMOJI_QUIZ = new Emoji("\u2753", R.drawable.baseline_help_16); + public static final Emoji EMOJI_VOICE = new Emoji("\uD83C\uDFA4", R.drawable.baseline_mic_16); + public static final Emoji EMOJI_GIF = new Emoji("\uD83D\uDC7E", R.drawable.deproko_baseline_gif_filled_16); + public static final Emoji EMOJI_LOCATION = new Emoji("\uD83D\uDCCC", R.drawable.baseline_gps_fixed_16); + public static final Emoji EMOJI_INVOICE = new Emoji("\uD83D\uDCB8", R.drawable.baseline_receipt_16); + public static final Emoji EMOJI_USER_JOINED = new Emoji("\uD83C\uDF89", R.drawable.baseline_party_popper_16); + public static final Emoji EMOJI_SCREENSHOT = new Emoji("\uD83D\uDCF8", R.drawable.round_warning_16); + public static final Emoji EMOJI_PIN = new Emoji("\uD83D\uDCCC", R.drawable.deproko_baseline_pin_16); + public static final Emoji EMOJI_ALBUM_MEDIA = new Emoji("\uD83D\uDDBC", R.drawable.baseline_collections_16); + public static final Emoji EMOJI_ALBUM_PHOTOS = new Emoji("\uD83D\uDDBC", R.drawable.baseline_collections_16); + public static final Emoji EMOJI_ALBUM_AUDIO = new Emoji("\uD83C\uDFB5", R.drawable.ivanliana_baseline_audio_collections_16); + public static final Emoji EMOJI_ALBUM_FILES = new Emoji("\uD83D\uDCCE", R.drawable.ivanliana_baseline_file_collections_16); + public static final Emoji EMOJI_ALBUM_VIDEOS = new Emoji("\uD83C\uDFA5", R.drawable.ivanliana_baseline_video_collections_16); + public static final Emoji EMOJI_FORWARD = new Emoji("\u21A9", R.drawable.baseline_share_arrow_16); + public static final Emoji EMOJI_ABACUS = new Emoji("\uD83E\uDDEE", R.drawable.baseline_bar_chart_24); + public static final Emoji EMOJI_DART = new Emoji("\uD83C\uDFAF", R.drawable.baseline_gps_fixed_16); + public static final Emoji EMOJI_DICE = new Emoji("\uD83C\uDFB2", R.drawable.baseline_casino_16); + public static final Emoji EMOJI_DICE_1 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_1_16); + public static final Emoji EMOJI_DICE_2 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_2_16); + public static final Emoji EMOJI_DICE_3 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_3_16); + public static final Emoji EMOJI_DICE_4 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_4_16); + public static final Emoji EMOJI_DICE_5 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_5_16); + public static final Emoji EMOJI_DICE_6 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_6_16); + public static final Emoji EMOJI_CALL = new Emoji("\uD83D\uDCDE", R.drawable.baseline_call_16); + public static final Emoji EMOJI_TIMER = new Emoji("\u23F2", R.drawable.baseline_timer_16); + public static final Emoji EMOJI_TIMER_OFF = new Emoji("\u23F2", R.drawable.baseline_timer_off_16); + public static final Emoji EMOJI_CALL_END = new Emoji("\uD83D\uDCDE", R.drawable.baseline_call_end_16); + public static final Emoji EMOJI_CALL_MISSED = new Emoji("\u260E", R.drawable.baseline_call_missed_18); + public static final Emoji EMOJI_CALL_DECLINED = new Emoji("\u260E", R.drawable.baseline_call_received_18); + public static final Emoji EMOJI_WARN = new Emoji("\u26A0", R.drawable.baseline_warning_18); + public static final Emoji EMOJI_INFO = new Emoji("\u2139", R.drawable.baseline_info_18); + public static final Emoji EMOJI_ERROR = new Emoji("\u2139", R.drawable.baseline_error_18); + public static final Emoji EMOJI_LOCK = new Emoji("\uD83D\uDD12", R.drawable.baseline_lock_16) +; + private static final int ARG_NONE = 0; + private static final int ARG_TRUE = 1; + private static final int ARG_POLL_QUIZ = 1; + private static final int ARG_CALL_DECLINED = -1; + private static final int ARG_CALL_MISSED = -2; + private static final int ARG_RECURRING_PAYMENT = -3; + private static final long ADDITIONAL_MESSAGE_UI_LOAD_TIMEOUT_MS = -1; // Always async + private static final long ADDITIONAL_MESSAGE_LOAD_TIMEOUT_MS = 0; + public final @Nullable Emoji emoji, parentEmoji; + public final @StringRes int placeholderText; + public final @Nullable TdApi.FormattedText formattedText; + public final boolean isTranslatable; + public final boolean hideAuthor; + public @Nullable TdApi.Message relatedMessage; + private @Nullable MessageContentBuilder relatedMessageBuilder; + + public ContentPreview (ContentPreview copy, TdApi.FormattedText editedFormattedText) { + this.emoji = copy.emoji; + this.parentEmoji = copy.parentEmoji; + this.placeholderText = copy.placeholderText; + this.formattedText = editedFormattedText != null ? editedFormattedText : copy.formattedText; + this.isTranslatable = copy.isTranslatable; + this.hideAuthor = copy.hideAuthor; + this.relatedMessage = copy.relatedMessage; + this.relatedMessageBuilder = copy.relatedMessageBuilder; + this.refresher = copy.refresher; + this.isMediaGroup = copy.isMediaGroup; + this.album = copy.album; + } + + public ContentPreview (@Nullable Emoji emoji, @StringRes int placeholderTextRes) { + this(emoji, placeholderTextRes, (TdApi.FormattedText) null); + } + + public ContentPreview (@Nullable Emoji emoji, @StringRes int placeholderTextRes, @Nullable String text) { + this(emoji, placeholderTextRes, StringUtils.isEmpty(text) ? null : new TdApi.FormattedText(text, null), false); + } + + public ContentPreview (@Nullable Emoji emoji, @StringRes int placeholderTextRes, @Nullable TdApi.FormattedText text) { + this(emoji, placeholderTextRes, text, false); + } + + public ContentPreview (@Nullable String text, boolean textTranslatable) { + this(null, 0, text, textTranslatable); + } + + public ContentPreview (@Nullable TdApi.FormattedText text, boolean textTranslatable) { + this(null, 0, text, textTranslatable); + } + + public ContentPreview (@Nullable Emoji emoji, @StringRes int placeholderTextRes, @Nullable TdApi.FormattedText text, boolean textTranslatable) { + this(emoji, placeholderTextRes, text, textTranslatable, false, null); + } + + public ContentPreview (@Nullable Emoji emoji, @StringRes int placeholderTextRes, @Nullable String text, boolean textTranslatable) { + this(emoji, placeholderTextRes, StringUtils.isEmpty(text) ? null : new TdApi.FormattedText(text, null), textTranslatable, false, null); + } + + public ContentPreview (@Nullable Emoji emoji, ContentPreview copy) { + this(copy.emoji, copy.placeholderText, copy.formattedText, copy.isTranslatable, copy.hideAuthor, emoji); + } + + public ContentPreview (@Nullable TdApi.FormattedText text, ContentPreview copy) { + this(copy.emoji, copy.placeholderText, text != null ? text : copy.formattedText, copy.isTranslatable, copy.hideAuthor, copy.parentEmoji); + } + + public ContentPreview (@Nullable Emoji emoji, int placeholderText, @Nullable TdApi.FormattedText formattedText, boolean isTranslatable, boolean hideAuthor, @Nullable Emoji parentEmoji) { + this.emoji = emoji; + this.placeholderText = placeholderText; + this.formattedText = formattedText; + this.isTranslatable = isTranslatable; + this.hideAuthor = hideAuthor; + this.parentEmoji = parentEmoji; + } + + private interface MessageContentBuilder { + ContentPreview runBuilder (TdApi.Message message); + } + + public boolean belongsToRelatedMessage (long chatId, long[] messageIds) { + return relatedMessage != null && relatedMessage.chatId == chatId && ArrayUtils.contains(messageIds, relatedMessage.id); + } + + private ContentPreview setRelatedMessage (@NonNull TdApi.Message message, @NonNull MessageContentBuilder refresher) { + this.relatedMessage = message; + this.relatedMessageBuilder = refresher; + return this; + } + + public boolean updateRelatedMessage (long chatId, long messageId, TdApi.MessageContent content, RefreshCallback callback) { + if (relatedMessage != null && relatedMessage.chatId == chatId && relatedMessage.id == messageId) { + relatedMessage.content = content; + if (callback != null) { + ContentPreview newPreview; + if (relatedMessageBuilder != null) { + newPreview = relatedMessageBuilder.runBuilder(relatedMessage); + } else { + newPreview = null; + } + if (newPreview != null) { + callback.onContentPreviewChanged(chatId, messageId, newPreview, this); + } else { + callback.onContentPreviewNotChanged(chatId, messageId, this); + } + } + return true; + } + return false; + } + + @NonNull + public static ContentPreview getChatListPreview (Tdlib tdlib, long chatId, TdApi.Message message, boolean checkChatRestrictions) { + return getContentPreview(tdlib, chatId, message, true, true, checkChatRestrictions); + } + + @NonNull + public static ContentPreview getNotificationPreview (Tdlib tdlib, long chatId, TdApi.Message message, boolean allowContent) { + return getContentPreview(tdlib, chatId, message, allowContent, false, true); + } + + private static long additionalMessageLoadTimeoutMs () { + if (UI.inUiThread()) { + return ADDITIONAL_MESSAGE_UI_LOAD_TIMEOUT_MS; + } else { + return ADDITIONAL_MESSAGE_LOAD_TIMEOUT_MS; + } + } + + @NonNull + private static ContentPreview getContentPreview (Tdlib tdlib, long chatId, @Nullable TdApi.Message message, boolean allowContent, boolean isChatList, boolean checkChatRestrictions) { + if (message == null) { + return new ContentPreview(EMOJI_ERROR, 0, Lang.getString(R.string.DeletedMessage), false); + } + if (Settings.instance().needRestrictContent()) { + if (!StringUtils.isEmpty(message.restrictionReason)) { + return new ContentPreview(EMOJI_ERROR, 0, message.restrictionReason, false); + } + if (checkChatRestrictions && chatId != 0) { // Otherwise lookup is handled by the caller + String restrictionReason = tdlib.chatRestrictionReason(chatId); + if (restrictionReason != null) { + return new ContentPreview(EMOJI_ERROR, 0, restrictionReason, false); + } + } + } + @TdApi.MessageContent.Constructors int type = message.content.getConstructor(); + TdApi.FormattedText formattedText; + if (allowContent) { + formattedText = Td.textOrCaption(message.content); + if (message.isOutgoing) { + TdApi.FormattedText pendingText = tdlib.getPendingFormattedText(message.chatId, message.id); + if (pendingText != null) { + formattedText = pendingText; + } + } + } else { + formattedText = null; + } + String alternativeText = null; + boolean alternativeTextTranslatable = false; + int arg1 = ARG_NONE; + int arg2 = ARG_NONE; + switch (type) { + case TdApi.MessageText.CONSTRUCTOR: { + TdApi.MessageText messageText = (TdApi.MessageText) message.content; + if (!Td.isEmpty(messageText.text) && messageText.text.entities != null) { + boolean isUrl = false; + for (TdApi.TextEntity entity : messageText.text.entities) { + //noinspection SwitchIntDef + switch (entity.type.getConstructor()) { + case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: + case TdApi.TextEntityTypeUrl.CONSTRUCTOR: { + if (entity.offset == 0 && ( + entity.length == messageText.text.text.length() || + !Td.isTextUrl(entity.type) || + !StringUtils.isEmptyOrInvisible(Td.substring(messageText.text.text, entity + ))) + ) { + isUrl = true; + break; + } + break; + } + } + } + if (isUrl) { + arg1 = ARG_TRUE; + } + } + break; + } + case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: { + TdApi.MessageAnimatedEmoji animatedEmoji = (TdApi.MessageAnimatedEmoji) message.content; + alternativeText = animatedEmoji.emoji; + break; + } + case TdApi.MessageDocument.CONSTRUCTOR: + alternativeText = ((TdApi.MessageDocument) message.content).document.fileName; + break; + case TdApi.MessageVoiceNote.CONSTRUCTOR: { + int duration = ((TdApi.MessageVoiceNote) message.content).voiceNote.duration; + if (duration > 0) { + alternativeText = Lang.getString(R.string.ChatContentVoiceDuration, Lang.getString(R.string.ChatContentVoice), Strings.buildDuration(duration)); + alternativeTextTranslatable = true; + } + break; + } + case TdApi.MessageVideoNote.CONSTRUCTOR: { + int duration = ((TdApi.MessageVideoNote) message.content).videoNote.duration; + if (duration > 0) { + alternativeText = Lang.getString(R.string.ChatContentVoiceDuration, Lang.getString(R.string.ChatContentRoundVideo), Strings.buildDuration(duration)); + alternativeTextTranslatable = true; + } + break; + } + case TdApi.MessageAudio.CONSTRUCTOR: + TdApi.Audio audio = ((TdApi.MessageAudio) message.content).audio; + alternativeText = Lang.getString(R.string.ChatContentSong, TD.getTitle(audio), TD.getSubtitle(audio)); + alternativeTextTranslatable = !TD.hasTitle(audio) || !TD.hasSubtitle(audio); + break; + case TdApi.MessageContact.CONSTRUCTOR: { + TdApi.Contact contact = ((TdApi.MessageContact) message.content).contact; + String name = TD.getUserName(contact.firstName, contact.lastName); + if (!StringUtils.isEmpty(name)) + alternativeText = name; + break; + } + case TdApi.MessagePoll.CONSTRUCTOR: + alternativeText = ((TdApi.MessagePoll) message.content).poll.question; + arg1 = ((TdApi.MessagePoll) message.content).poll.type.getConstructor() == TdApi.PollTypeRegular.CONSTRUCTOR ? ARG_NONE : ARG_POLL_QUIZ; + break; + case TdApi.MessageDice.CONSTRUCTOR: + alternativeText = ((TdApi.MessageDice) message.content).emoji; + arg1 = ((TdApi.MessageDice) message.content).value; + break; + case TdApi.MessageCall.CONSTRUCTOR: { + switch (((TdApi.MessageCall) message.content).discardReason.getConstructor()) { + case TdApi.CallDiscardReasonDeclined.CONSTRUCTOR: + arg1 = ARG_CALL_DECLINED; + break; + case TdApi.CallDiscardReasonMissed.CONSTRUCTOR: + arg1 = ARG_CALL_MISSED; + break; + default: + arg1 = ((TdApi.MessageCall) message.content).duration; + break; + } + break; + } + case TdApi.MessageLocation.CONSTRUCTOR: { + TdApi.MessageLocation location = ((TdApi.MessageLocation) message.content); + alternativeText = location.livePeriod == 0 || location.expiresIn == 0 ? null : "live"; + break; + } + case TdApi.MessageGame.CONSTRUCTOR: + alternativeText = ((TdApi.MessageGame) message.content).game.title; + break; + case TdApi.MessageSticker.CONSTRUCTOR: + TdApi.Sticker sticker = ((TdApi.MessageSticker) message.content).sticker; + alternativeText = Td.isAnimated(sticker.format) ? "animated" + sticker.emoji : sticker.emoji; + break; + case TdApi.MessageInvoice.CONSTRUCTOR: { + TdApi.MessageInvoice invoice = (TdApi.MessageInvoice) message.content; + alternativeText = CurrencyUtils.buildAmount(invoice.currency, invoice.totalAmount); + break; + } + case TdApi.MessagePhoto.CONSTRUCTOR: + if (((TdApi.MessagePhoto) message.content).isSecret) + return new ContentPreview(EMOJI_SECRET_PHOTO, R.string.SelfDestructPhoto, formattedText); + break; + case TdApi.MessageVideo.CONSTRUCTOR: + if (((TdApi.MessageVideo) message.content).isSecret) + return new ContentPreview(EMOJI_SECRET_VIDEO, R.string.SelfDestructVideo, formattedText); + break; + case TdApi.MessageAnimation.CONSTRUCTOR: + // alternativeText = ((TdApi.MessageAnimation) message.content).animation.fileName; + break; + case TdApi.MessageStory.CONSTRUCTOR: { + TdApi.MessageStory story = (TdApi.MessageStory) message.content; + arg1 = story.viaMention ? ARG_TRUE : ARG_NONE; + break; + } + case TdApi.MessagePinMessage.CONSTRUCTOR: { + long pinnedMessageId = ((TdApi.MessagePinMessage) message.content).messageId; + TdApi.Message pinnedMessage; + long loadTimeoutMs = additionalMessageLoadTimeoutMs(); + if (pinnedMessageId != 0 && loadTimeoutMs >= 0) { + pinnedMessage = tdlib.getMessageLocally( + message.chatId, pinnedMessageId, loadTimeoutMs + ); + } else { + pinnedMessage = null; + } + if (pinnedMessage != null) { + return new ContentPreview(EMOJI_PIN, getContentPreview(tdlib, chatId, pinnedMessage, allowContent, isChatList, checkChatRestrictions)); + } else { + return new ContentPreview(EMOJI_PIN, R.string.ChatContentPinned) + .setRefresher((oldPreview, callback) -> tdlib.getMessage(chatId, pinnedMessageId, remotePinnedMessage -> { + if (remotePinnedMessage != null) { + MessageContentBuilder builder = new MessageContentBuilder() { + @Override + public ContentPreview runBuilder (TdApi.Message message) { + return new ContentPreview(EMOJI_PIN, getContentPreview(tdlib, chatId, message, allowContent, isChatList, checkChatRestrictions)) + .setRelatedMessage(message, this); + } + }; + ContentPreview newPreview = builder.runBuilder(remotePinnedMessage); + callback.onContentPreviewChanged(chatId, message.id, newPreview, oldPreview); + } else { + callback.onContentPreviewNotChanged(chatId, message.id, oldPreview); + } + }), false); + } + } + case TdApi.MessageGameScore.CONSTRUCTOR: { + TdApi.MessageGameScore score = (TdApi.MessageGameScore) message.content; + long timeoutMs = additionalMessageLoadTimeoutMs(); + TdApi.Message gameMessage = timeoutMs >= 0 ? + tdlib.getMessageLocally( + message.chatId, score.gameMessageId, + timeoutMs + ) : null; + String gameTitle = gameMessage != null && Td.isGame(gameMessage.content) ? TD.getGameName(((TdApi.MessageGame) gameMessage.content).game, false) : null; + if (!StringUtils.isEmpty(gameTitle)) { + return new ContentPreview(EMOJI_GAME, 0, Lang.plural(message.isOutgoing ? R.string.game_ActionYouScoredInGame : R.string.game_ActionScoredInGame, score.score, gameTitle), true); + } else { + return new ContentPreview(EMOJI_GAME, 0, Lang.plural(message.isOutgoing ? R.string.game_ActionYouScored : R.string.game_ActionScored, score.score), true) + .setRefresher(gameMessage != null ? null : + (oldPreview, callback) -> tdlib.getMessage(message.chatId, score.gameMessageId, remoteGameMessage -> { + if (remoteGameMessage != null && Td.isGame(remoteGameMessage.content)) { + String newGameTitle = TD.getGameName(((TdApi.MessageGame) remoteGameMessage.content).game, false); + if (!StringUtils.isEmpty(newGameTitle)) { + MessageContentBuilder builder = new MessageContentBuilder() { + @Override + public ContentPreview runBuilder (TdApi.Message updatedMessage) { + String newGameTitle = TD.getGameName(((TdApi.MessageGame) updatedMessage.content).game, false); + return new ContentPreview(EMOJI_GAME, 0, Lang.plural(message.isOutgoing ? R.string.game_ActionYouScoredInGame : R.string.game_ActionScoredInGame, score.score, newGameTitle), true) + .setRelatedMessage(updatedMessage, this); + } + }; + ContentPreview newContent = builder.runBuilder(remoteGameMessage); + callback.onContentPreviewChanged(message.chatId, message.id, newContent, oldPreview); + return; + } + } + callback.onContentPreviewNotChanged(message.chatId, message.id, oldPreview); + }), false); + } + } + case TdApi.MessageProximityAlertTriggered.CONSTRUCTOR: { + TdApi.MessageProximityAlertTriggered alert = (TdApi.MessageProximityAlertTriggered) message.content; + if (tdlib.isSelfSender(alert.travelerId)) { + return new ContentPreview(EMOJI_LOCATION, 0, Lang.plural(alert.distance >= 1000 ? R.string.ChatContentProximityYouKm : R.string.ChatContentProximityYouM, alert.distance >= 1000 ? alert.distance / 1000 : alert.distance, tdlib.senderName(alert.watcherId, true)), true); + } else if (tdlib.isSelfSender(alert.watcherId)) { + return new ContentPreview(EMOJI_LOCATION, 0, Lang.plural(alert.distance >= 1000 ? R.string.ChatContentProximityFromYouKm : R.string.ChatContentProximityFromYouM, alert.distance >= 1000 ? alert.distance / 1000 : alert.distance, tdlib.senderName(alert.travelerId, true)), true); + } else { + return new ContentPreview(EMOJI_LOCATION, 0, Lang.plural(alert.distance >= 1000 ? R.string.ChatContentProximityKm : R.string.ChatContentProximityM, alert.distance >= 1000 ? alert.distance / 1000 : alert.distance, tdlib.senderName(alert.travelerId, true), tdlib.senderName(alert.watcherId, true)), true); + } + } + case TdApi.MessageVideoChatStarted.CONSTRUCTOR: { + if (message.isChannelPost) { + return new ContentPreview(EMOJI_CALL, message.isOutgoing ? R.string.ChatContentLiveStreamStarted_outgoing : R.string.ChatContentLiveStreamStarted); + } else { + return new ContentPreview(EMOJI_CALL, message.isOutgoing ? R.string.ChatContentVoiceChatStarted_outgoing : R.string.ChatContentVoiceChatStarted); + } + } + case TdApi.MessageVideoChatEnded.CONSTRUCTOR: { + TdApi.MessageVideoChatEnded videoChatOrLiveStream = (TdApi.MessageVideoChatEnded) message.content; + if (message.isChannelPost) { + return new ContentPreview(EMOJI_CALL_END, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentLiveStreamFinished_outgoing : R.string.ChatContentLiveStreamFinished, Lang.getCallDuration(videoChatOrLiveStream.duration)), true); + } else { + return new ContentPreview(EMOJI_CALL_END, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentVoiceChatFinished_outgoing : R.string.ChatContentVoiceChatFinished, Lang.getCallDuration(videoChatOrLiveStream.duration)), true); + } + } + case TdApi.MessageVideoChatScheduled.CONSTRUCTOR: { + TdApi.MessageVideoChatScheduled event = (TdApi.MessageVideoChatScheduled) message.content; + return new ContentPreview(EMOJI_CALL, 0, Lang.getString(message.isChannelPost ? R.string.LiveStreamScheduledOn : R.string.VideoChatScheduledFor, Lang.getMessageTimestamp(event.startDate, TimeUnit.SECONDS)), true); + } + case TdApi.MessageInviteVideoChatParticipants.CONSTRUCTOR: { + TdApi.MessageInviteVideoChatParticipants info = (TdApi.MessageInviteVideoChatParticipants) message.content; + if (message.isChannelPost) { + if (info.userIds.length == 1) { + long userId = info.userIds[0]; + if (tdlib.isSelfUserId(userId)) { + return new ContentPreview(EMOJI_GROUP_INVITE, R.string.ChatContentLiveStreamInviteYou); + } else { + return new ContentPreview(EMOJI_GROUP_INVITE, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentLiveStreamInvite_outgoing : R.string.ChatContentLiveStreamInvite, tdlib.cache().userName(userId)), true); + } + } else { + return new ContentPreview(EMOJI_GROUP_INVITE, 0, Lang.plural(message.isOutgoing ? R.string.ChatContentLiveStreamInviteMulti_outgoing : R.string.ChatContentLiveStreamInviteMulti, info.userIds.length), true); + } + } else { + if (info.userIds.length == 1) { + long userId = info.userIds[0]; + if (tdlib.isSelfUserId(userId)) { + return new ContentPreview(EMOJI_GROUP_INVITE, R.string.ChatContentVoiceChatInviteYou); + } else { + return new ContentPreview(EMOJI_GROUP_INVITE, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentVoiceChatInvite_outgoing : R.string.ChatContentVoiceChatInvite, tdlib.cache().userName(userId)), true); + } + } else { + return new ContentPreview(EMOJI_GROUP_INVITE, 0, Lang.plural(message.isOutgoing ? R.string.ChatContentVoiceChatInviteMulti_outgoing : R.string.ChatContentVoiceChatInviteMulti, info.userIds.length), true); + } + } + } + case TdApi.MessageChatAddMembers.CONSTRUCTOR: { + TdApi.MessageChatAddMembers info = (TdApi.MessageChatAddMembers) message.content; + if (info.memberUserIds.length == 1) { + long userId = info.memberUserIds[0]; + if (userId == Td.getSenderUserId(message)) { + if (ChatId.isSupergroup(message.chatId)) { + return new ContentPreview(EMOJI_GROUP, message.isOutgoing ? R.string.ChatContentGroupJoinPublic_outgoing : R.string.ChatContentGroupJoinPublic); + } else { // isReturned + return new ContentPreview(EMOJI_GROUP, message.isOutgoing ? R.string.ChatContentGroupReturn_outgoing : R.string.ChatContentGroupReturn); + } + } else if (tdlib.isSelfUserId(userId)) { + return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupAddYou); + } else { + return new ContentPreview(EMOJI_GROUP, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentGroupAdd_outgoing : R.string.ChatContentGroupAdd, tdlib.cache().userName(userId)), true); + } + } else { + return new ContentPreview(EMOJI_GROUP, 0, Lang.plural(message.isOutgoing ? R.string.ChatContentGroupAddMembers_outgoing : R.string.ChatContentGroupAddMembers, info.memberUserIds.length), true); + } + } + case TdApi.MessageChatDeleteMember.CONSTRUCTOR: { + long userId = ((TdApi.MessageChatDeleteMember) message.content).userId; + if (userId == Td.getSenderUserId(message)) { + return new ContentPreview(EMOJI_GROUP, message.isOutgoing ? R.string.ChatContentGroupLeft_outgoing : R.string.ChatContentGroupLeft); + } else if (tdlib.isSelfUserId(userId)) { + return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupKickYou); + } else { + return new ContentPreview(EMOJI_GROUP, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentGroupKick_outgoing : R.string.ChatContentGroupKick, tdlib.cache().userFirstName(userId)), true); + } + } + case TdApi.MessageChatChangeTitle.CONSTRUCTOR: + alternativeText = ((TdApi.MessageChatChangeTitle) message.content).title; + break; + case TdApi.MessageChatSetMessageAutoDeleteTime.CONSTRUCTOR: + arg1 = ((TdApi.MessageChatSetMessageAutoDeleteTime) message.content).messageAutoDeleteTime; + break; + case TdApi.MessageChatSetTheme.CONSTRUCTOR: + alternativeText = ((TdApi.MessageChatSetTheme) message.content).themeName; + break; + case TdApi.MessageGiftedPremium.CONSTRUCTOR: { + // TODO: R.string.ChatContent* + TdApi.MessageGiftedPremium giftedPremium = (TdApi.MessageGiftedPremium) message.content; + CharSequence text; + if (message.isOutgoing) { + text = Lang.pluralBold(R.string.YouGiftedPremium, giftedPremium.monthCount, CurrencyUtils.buildAmount(giftedPremium.currency, giftedPremium.amount)); + } else { + text = Lang.pluralBold(R.string.GiftedPremium, giftedPremium.monthCount, tdlib.senderName(message.senderId, true), CurrencyUtils.buildAmount(giftedPremium.currency, giftedPremium.amount)); + } + TdApi.FormattedText formatted = TD.toFormattedText(text, false); + return new ContentPreview(EMOJI_GIFT, 0, formatted, true); + } + case TdApi.MessagePremiumGiftCode.CONSTRUCTOR: { + // TODO: R.string.ChatContent* + TdApi.MessagePremiumGiftCode giftedPremium = (TdApi.MessagePremiumGiftCode) message.content; + CharSequence text; + if (message.isOutgoing) { + text = Lang.pluralBold(R.string.YouGiftedPremiumCode, giftedPremium.monthCount); + } else { + text = Lang.pluralBold(R.string.GiftedPremiumCode, giftedPremium.monthCount, tdlib.senderName(giftedPremium.creatorId, true)); + } + TdApi.FormattedText formatted = TD.toFormattedText(text, false); + return new ContentPreview(EMOJI_GIFT, 0, formatted, true); + } + case TdApi.MessagePremiumGiveaway.CONSTRUCTOR: { + TdApi.MessagePremiumGiveaway premiumGiveaway = (TdApi.MessagePremiumGiveaway) message.content; + String text; + if (premiumGiveaway.winnerCount > 0) { + text = Lang.getString(R.string.format_giveawayInfo, + Lang.getString(R.string.Giveaway), + Lang.plural(R.string.xFutureWinnersOn, premiumGiveaway.winnerCount, Lang.getDate(premiumGiveaway.parameters.winnersSelectionDate, TimeUnit.SECONDS)) + ); + } else { + text = Lang.getString(R.string.Giveaway); + } + return new ContentPreview(EMOJI_GIFT, 0, text, true); + } + case TdApi.MessagePremiumGiveawayCompleted.CONSTRUCTOR: { + TdApi.MessagePremiumGiveawayCompleted giveawayCompleted = (TdApi.MessagePremiumGiveawayCompleted) message.content; + arg1 = giveawayCompleted.winnerCount; + arg2 = giveawayCompleted.unclaimedPrizeCount; + break; + } + + case TdApi.MessageCustomServiceAction.CONSTRUCTOR: { + TdApi.MessageCustomServiceAction serviceAction = (TdApi.MessageCustomServiceAction) message.content; + return new ContentPreview(EMOJI_INFO, 0, serviceAction.text); + } + case TdApi.MessageBotWriteAccessAllowed.CONSTRUCTOR: { + TdApi.MessageBotWriteAccessAllowed writeAccessAllowed = (TdApi.MessageBotWriteAccessAllowed) message.content; + TdApi.BotWriteAccessAllowReason reason = writeAccessAllowed.reason; + CharSequence text; + switch (reason.getConstructor()) { + case TdApi.BotWriteAccessAllowReasonConnectedWebsite.CONSTRUCTOR: { + TdApi.BotWriteAccessAllowReasonConnectedWebsite connectedWebsite = (TdApi.BotWriteAccessAllowReasonConnectedWebsite) reason; + text = Lang.getStringBold(R.string.BotWebappAllowed, connectedWebsite.domainName); + break; + } + case TdApi.BotWriteAccessAllowReasonAddedToAttachmentMenu.CONSTRUCTOR: { + text = Lang.getString(R.string.BotAttachAllowed); + break; + } + case TdApi.BotWriteAccessAllowReasonLaunchedWebApp.CONSTRUCTOR: { + TdApi.BotWriteAccessAllowReasonLaunchedWebApp launchedWebApp = (TdApi.BotWriteAccessAllowReasonLaunchedWebApp) reason; + text = Lang.getStringBold(R.string.BotWebappAllowed, launchedWebApp.webApp.title); + break; + } + case TdApi.BotWriteAccessAllowReasonAcceptedRequest.CONSTRUCTOR: { + text = Lang.getString(R.string.BotAppAllowed); + break; + } + default: { + Td.assertBotWriteAccessAllowReason_d7597302(); + throw Td.unsupported(reason); + } + } + formattedText = TD.toFormattedText(text, false); + return new ContentPreview(EMOJI_INFO, 0, formattedText, true); + } + case TdApi.MessageWebAppDataSent.CONSTRUCTOR: { + TdApi.MessageWebAppDataSent webAppDataSent = (TdApi.MessageWebAppDataSent) message.content; + return new ContentPreview(EMOJI_INFO, 0, Lang.getString(R.string.BotDataSent, webAppDataSent.buttonText), true); + } + case TdApi.MessagePaymentSuccessful.CONSTRUCTOR: { + TdApi.MessagePaymentSuccessful successful = (TdApi.MessagePaymentSuccessful) message.content; + return new ContentPreview(EMOJI_INVOICE, 0, Lang.getString(R.string.PaymentSuccessfullyPaidNoItem, CurrencyUtils.buildAmount(successful.currency, successful.totalAmount), tdlib.chatTitle(message.chatId)), true); + } + + // Handled by getSimpleContentPreview + case TdApi.MessageVenue.CONSTRUCTOR: + case TdApi.MessageScreenshotTaken.CONSTRUCTOR: + case TdApi.MessageExpiredPhoto.CONSTRUCTOR: + case TdApi.MessageExpiredVideo.CONSTRUCTOR: + case TdApi.MessageContactRegistered.CONSTRUCTOR: + + case TdApi.MessageChatUpgradeFrom.CONSTRUCTOR: + case TdApi.MessageChatUpgradeTo.CONSTRUCTOR: + case TdApi.MessageBasicGroupChatCreate.CONSTRUCTOR: + case TdApi.MessageSupergroupChatCreate.CONSTRUCTOR: + case TdApi.MessageChatJoinByRequest.CONSTRUCTOR: + case TdApi.MessageChatJoinByLink.CONSTRUCTOR: + case TdApi.MessageChatChangePhoto.CONSTRUCTOR: + case TdApi.MessageChatDeletePhoto.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayCreated.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayWinners.CONSTRUCTOR: + + // Handled by getSimpleContentPreview, but unsupported + case TdApi.MessageUnsupported.CONSTRUCTOR: + case TdApi.MessageUsersShared.CONSTRUCTOR: + case TdApi.MessageChatShared.CONSTRUCTOR: + case TdApi.MessageSuggestProfilePhoto.CONSTRUCTOR: + case TdApi.MessageForumTopicCreated.CONSTRUCTOR: + case TdApi.MessageForumTopicEdited.CONSTRUCTOR: + case TdApi.MessageForumTopicIsClosedToggled.CONSTRUCTOR: + case TdApi.MessageForumTopicIsHiddenToggled.CONSTRUCTOR: + case TdApi.MessagePassportDataSent.CONSTRUCTOR: + case TdApi.MessageChatSetBackground.CONSTRUCTOR: + break; + + // Bots only. Unused + case TdApi.MessagePassportDataReceived.CONSTRUCTOR: + case TdApi.MessagePaymentSuccessfulBot.CONSTRUCTOR: + case TdApi.MessageWebAppDataReceived.CONSTRUCTOR: + default: + Td.assertMessageContent_d40af239(); + throw Td.unsupported(message.content); + } + Refresher refresher = null; + if (message.mediaAlbumId != 0 && TD.getCombineMode(message) != TD.COMBINE_MODE_NONE) { + refresher = (oldContent, callback) -> tdlib.getAlbum(message, true, null, localAlbum -> { + if (localAlbum.messages.size() == 1 && !localAlbum.mayHaveMoreItems()) { + callback.onContentPreviewNotChanged(message.chatId, message.id, oldContent); + } else { + ContentPreview newPreview = getAlbumPreview(tdlib, message, localAlbum, allowContent); + if (localAlbum.messages.size() == 1) { + if (newPreview.hasRefresher()) { + newPreview.refreshContent(callback); + } else { + callback.onContentPreviewNotChanged(message.chatId, message.id, oldContent); + } + } else { + callback.onContentPreviewChanged(message.chatId, message.id, newPreview, oldContent); + } + } + }); + } + TdApi.FormattedText argument; + boolean argumentTranslatable; + if (Td.isEmpty(formattedText)) { + argument = new TdApi.FormattedText(alternativeText, null); + argumentTranslatable = alternativeTextTranslatable; + } else { + argument = formattedText; + argumentTranslatable = false; + } + ContentPreview preview = getSimpleContentPreview(message.content.getConstructor(), tdlib, chatId, message.senderId, null, !message.isChannelPost && message.isOutgoing, isChatList, argument, argumentTranslatable, arg1, arg2); + if (refresher != null) { + preview.setRefresher(refresher, true); + } + return preview; + } + + public static ContentPreview getAlbumPreview (Tdlib tdlib, TdApi.Message message, Tdlib.Album album, boolean allowContent) { + SparseIntArray counters = new SparseIntArray(); + for (TdApi.Message m : album.messages) { + ArrayUtils.increment(counters, m.content.getConstructor()); + } + int textRes; + Emoji emoji; + switch (counters.size() == 1 ? counters.keyAt(0) : 0) { + case TdApi.MessagePhoto.CONSTRUCTOR: + textRes = R.string.xPhotos; + emoji = EMOJI_ALBUM_PHOTOS; + break; + case TdApi.MessageVideo.CONSTRUCTOR: + textRes = R.string.xVideos; + emoji = EMOJI_ALBUM_VIDEOS; + break; + case TdApi.MessageDocument.CONSTRUCTOR: + textRes = R.string.xFiles; + emoji = EMOJI_ALBUM_FILES; + break; + case TdApi.MessageAudio.CONSTRUCTOR: + textRes = R.string.xAudios; + emoji = EMOJI_ALBUM_AUDIO; + break; + default: + textRes = R.string.xMedia; + emoji = EMOJI_ALBUM_MEDIA; + break; + } + TdApi.Message captionMessage = allowContent ? getAlbumCaptionMessage(tdlib, album.messages) : null; + TdApi.FormattedText formattedCaption = captionMessage != null ? Td.textOrCaption(captionMessage.content) : null; + ContentPreview preview = new ContentPreview(emoji, 0, Td.isEmpty(formattedCaption) ? new TdApi.FormattedText(Lang.plural(textRes, album.messages.size()), null) : formattedCaption, Td.isEmpty(formattedCaption)); + preview.album = album; + if (album.mayHaveMoreItems()) { + preview.setRefresher((oldPreview, callback) -> + tdlib.getAlbum(message, false, album, remoteAlbum -> { + if (remoteAlbum.messages.size() > album.messages.size()) { + callback.onContentPreviewChanged(message.chatId, message.id, getAlbumPreview(tdlib, message, remoteAlbum, allowContent), oldPreview); + } else { + callback.onContentPreviewNotChanged(message.chatId, message.id, oldPreview); + } + }), true + ); + } + return preview; + } + + public static TdApi.Message getAlbumCaptionMessage (Tdlib tdlib, List messages) { + TdApi.Message captionMessage = null; + for (TdApi.Message message : messages) { + TdApi.FormattedText currentCaption = tdlib.getFormattedText(message); + if (!Td.isEmpty(currentCaption)) { + if (captionMessage != null) { + captionMessage = null; + break; + } else { + captionMessage = message; + } + } + } + return captionMessage; + } + + private static ContentPreview getNotificationPinned(int res, int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument) { + return getNotificationPinned(res, type, tdlib, chatId, sender, argument, senderName, false, 0, 0); + } + + private static ContentPreview getNotificationPinned(int res, int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument, boolean argumentTranslatable) { + return getNotificationPinned(res, type, tdlib, chatId, sender, argument, senderName, argumentTranslatable, 0, 0); + } + + private static ContentPreview getNotificationPinned(int res, int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument, int arg1, int arg2) { + return getNotificationPinned(res, type, tdlib, chatId, sender, argument, senderName, false, arg1, arg2); + } + + private static ContentPreview getNotificationPinned (int res, int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument, boolean argumentTranslatable, int arg1, int arg2) { + String text; + if (StringUtils.isEmpty(argument)) { + try { + text = Lang.formatString(Strings.replaceBoldTokens(Lang.getString(res)).toString(), null, getSenderName(tdlib, sender, senderName)).toString(); + } catch (Throwable t) { + text = Lang.getString(res); + } + } else { + ContentPreview contentPreview = getNotificationPreview(type, tdlib, chatId, sender, senderName, argument, argumentTranslatable, arg1, arg2); + String preview = contentPreview != null ? contentPreview.toString() : null; + if (StringUtils.isEmpty(preview)) { + preview = argument; + } + try { + text = Lang.formatString(Strings.replaceBoldTokens(Lang.getString(R.string.ActionPinnedText)).toString(), null, getSenderName(tdlib, sender, senderName), preview).toString(); + } catch (Throwable t) { + text = Lang.getString(R.string.ActionPinnedText); + } + } + // TODO icon? + return new ContentPreview(null, 0, new TdApi.FormattedText(text, null), true, true, EMOJI_PIN); + } + + public static @NonNull ContentPreview getNotificationPreview (Tdlib tdlib, long chatId, TdApi.NotificationTypeNewPushMessage push, boolean allowContent) { + switch (push.content.getConstructor()) { + case TdApi.PushMessageContentHidden.CONSTRUCTOR: + if (((TdApi.PushMessageContentHidden) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedNoText, TdApi.MessageText.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + else + return new ContentPreview(Lang.plural(R.string.xNewMessages, 1), true); + + case TdApi.PushMessageContentText.CONSTRUCTOR: + if (((TdApi.PushMessageContentText) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedNoText, TdApi.MessageText.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentText) push.content).text); + else + return getNotificationPreview(TdApi.MessageText.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentText) push.content).text); + + case TdApi.PushMessageContentMessageForwards.CONSTRUCTOR: + return new ContentPreview(Lang.plural(R.string.xForwards, ((TdApi.PushMessageContentMessageForwards) push.content).totalCount), true); + + case TdApi.PushMessageContentPhoto.CONSTRUCTOR: { + String caption = ((TdApi.PushMessageContentPhoto) push.content).caption; + if (((TdApi.PushMessageContentPhoto) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedPhoto, TdApi.MessagePhoto.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption); + else if (((TdApi.PushMessageContentPhoto) push.content).isSecret) + return new ContentPreview(EMOJI_SECRET_PHOTO, R.string.SelfDestructPhoto, caption, false); + else + return getNotificationPreview(TdApi.MessagePhoto.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption); + } + + case TdApi.PushMessageContentVideo.CONSTRUCTOR: { + String caption = ((TdApi.PushMessageContentVideo) push.content).caption; + if (((TdApi.PushMessageContentVideo) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedVideo, TdApi.MessageVideo.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption); + else if (((TdApi.PushMessageContentVideo) push.content).isSecret) + return new ContentPreview(EMOJI_SECRET_VIDEO, R.string.SelfDestructVideo, caption); + else + return getNotificationPreview(TdApi.MessageVideo.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption); + } + + case TdApi.PushMessageContentAnimation.CONSTRUCTOR: { + String caption = ((TdApi.PushMessageContentAnimation) push.content).caption; + if (((TdApi.PushMessageContentAnimation) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedGif, TdApi.MessageAnimation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption); + else + return getNotificationPreview(TdApi.MessageAnimation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption); + } + + case TdApi.PushMessageContentDocument.CONSTRUCTOR: { + TdApi.Document media = ((TdApi.PushMessageContentDocument) push.content).document; + String caption = null; // FIXME server ((TdApi.PushMessageContentDocument) push.content).caption; + if (StringUtils.isEmpty(caption) && media != null) { + caption = media.fileName; + } + if (((TdApi.PushMessageContentDocument) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedFile, TdApi.MessageDocument.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption); + else + return getNotificationPreview(TdApi.MessageDocument.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption); + } + + case TdApi.PushMessageContentSticker.CONSTRUCTOR: + if (((TdApi.PushMessageContentSticker) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedSticker, TdApi.MessageSticker.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentSticker) push.content).emoji); + else if (((TdApi.PushMessageContentSticker) push.content).sticker != null && Td.isAnimated(((TdApi.PushMessageContentSticker) push.content).sticker.format)) + return getNotificationPreview(TdApi.MessageSticker.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, "animated" + ((TdApi.PushMessageContentSticker) push.content).emoji); + else + return getNotificationPreview(TdApi.MessageSticker.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentSticker) push.content).emoji); + + case TdApi.PushMessageContentLocation.CONSTRUCTOR: + if (((TdApi.PushMessageContentLocation) push.content).isLive) { + if (((TdApi.PushMessageContentLocation) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedGeoLive, TdApi.MessageLocation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + else + return getNotificationPreview(TdApi.MessageLocation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, "live"); + } else { + if (((TdApi.PushMessageContentLocation) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedGeo, TdApi.MessageLocation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + else + return getNotificationPreview(TdApi.MessageLocation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + } + + case TdApi.PushMessageContentPoll.CONSTRUCTOR: + if (((TdApi.PushMessageContentPoll) push.content).isPinned) + return getNotificationPinned(((TdApi.PushMessageContentPoll) push.content).isRegular ? R.string.ActionPinnedPoll : R.string.ActionPinnedQuiz, TdApi.MessagePoll.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentPoll) push.content).question, ((TdApi.PushMessageContentPoll) push.content).isRegular ? ARG_NONE : ARG_POLL_QUIZ, 0); + else + return getNotificationPreview(TdApi.MessagePoll.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentPoll) push.content).question, ((TdApi.PushMessageContentPoll) push.content).isRegular ? ARG_NONE : ARG_POLL_QUIZ, 0); + + case TdApi.PushMessageContentAudio.CONSTRUCTOR: { + TdApi.Audio audio = ((TdApi.PushMessageContentAudio) push.content).audio; + String caption = null; // FIXME server ((TdApi.PushMessageContentAudio) push.content).caption; + boolean translatable = false; + if (StringUtils.isEmpty(caption) && audio != null) { + caption = Lang.getString(R.string.ChatContentSong, TD.getTitle(audio), TD.getSubtitle(audio)); + translatable = !TD.hasTitle(audio) || !TD.hasSubtitle(audio); + } + if (((TdApi.PushMessageContentAudio) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedMusic, TdApi.MessageAudio.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, translatable); + else + return getNotificationPreview(TdApi.MessageAudio.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, translatable); + } + + case TdApi.PushMessageContentVideoNote.CONSTRUCTOR: { + String argument = null; + boolean argumentTranslatable = false; + TdApi.VideoNote videoNote = ((TdApi.PushMessageContentVideoNote) push.content).videoNote; + if (videoNote != null && videoNote.duration > 0) { + argument = Lang.getString(R.string.ChatContentVoiceDuration, Lang.getString(R.string.ChatContentRoundVideo), Strings.buildDuration(videoNote.duration)); + argumentTranslatable = true; + } + if (((TdApi.PushMessageContentVideoNote) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedRound, TdApi.MessageVideoNote.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, argument, argumentTranslatable); + else + return getNotificationPreview(TdApi.MessageVideoNote.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, argument, argumentTranslatable); + } + + case TdApi.PushMessageContentStory.CONSTRUCTOR: { + if (((TdApi.PushMessageContentStory) push.content).isPinned) { + return getNotificationPinned(R.string.ActionPinnedStory, TdApi.MessageStory.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + } else { + return getNotificationPreview(TdApi.MessageStory.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + } + } + + case TdApi.PushMessageContentVoiceNote.CONSTRUCTOR: { + String argument = null; // FIXME server ((TdApi.PushMessageContentVoiceNote) push.content).caption; + boolean argumentTranslatable = false; + if (StringUtils.isEmpty(argument)) { + TdApi.VoiceNote voiceNote = ((TdApi.PushMessageContentVoiceNote) push.content).voiceNote; + if (voiceNote != null && voiceNote.duration > 0) { + argument = Lang.getString(R.string.ChatContentVoiceDuration, Lang.getString(R.string.ChatContentVoice), Strings.buildDuration(voiceNote.duration)); + argumentTranslatable = true; + } + } + if (((TdApi.PushMessageContentVoiceNote) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedVoice, TdApi.MessageVoiceNote.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, argument, argumentTranslatable); + else + return getNotificationPreview(TdApi.MessageVoiceNote.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, argument, argumentTranslatable); + } + + case TdApi.PushMessageContentGame.CONSTRUCTOR: + if (((TdApi.PushMessageContentGame) push.content).isPinned) { + String gameTitle = ((TdApi.PushMessageContentGame) push.content).title; + return getNotificationPinned(StringUtils.isEmpty(gameTitle) ? R.string.ActionPinnedGameNoName : R.string.ActionPinnedGame, TdApi.MessageGame.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, StringUtils.isEmpty(gameTitle) ? null : gameTitle); + } else + return getNotificationPreview(TdApi.MessageGame.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentGame) push.content).title); + + case TdApi.PushMessageContentContact.CONSTRUCTOR: + if (((TdApi.PushMessageContentContact) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedContact, TdApi.MessageContact.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentContact) push.content).name); + else + return getNotificationPreview(TdApi.MessageContact.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentContact) push.content).name); + + case TdApi.PushMessageContentInvoice.CONSTRUCTOR: + if (((TdApi.PushMessageContentInvoice) push.content).isPinned) + return getNotificationPinned(R.string.ActionPinnedNoText, TdApi.MessageInvoice.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); // TODO + else + return getNotificationPreview(TdApi.MessageInvoice.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentInvoice) push.content).price); + + case TdApi.PushMessageContentScreenshotTaken.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageScreenshotTaken.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + + case TdApi.PushMessageContentGameScore.CONSTRUCTOR: { + TdApi.PushMessageContentGameScore score = (TdApi.PushMessageContentGameScore) push.content; + if (score.isPinned) { + return getNotificationPinned(R.string.ActionPinnedNoText, TdApi.MessageGameScore.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); // TODO + } else { + String gameTitle = score.title; + if (!StringUtils.isEmpty(gameTitle)) + return new ContentPreview(EMOJI_GAME, 0, Lang.plural(R.string.game_ActionScoredInGame, score.score, gameTitle), true); + else + return new ContentPreview(EMOJI_GAME, 0, Lang.plural(R.string.game_ActionScored, score.score), true); + } + } + + case TdApi.PushMessageContentContactRegistered.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageContactRegistered.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + + case TdApi.PushMessageContentMediaAlbum.CONSTRUCTOR: { + TdApi.PushMessageContentMediaAlbum album = ((TdApi.PushMessageContentMediaAlbum) push.content); + int mediaTypeCount = 0; + if (album.hasPhotos) + mediaTypeCount++; + if (album.hasVideos) + mediaTypeCount++; + if (album.hasAudios) + mediaTypeCount++; + if (album.hasDocuments) + mediaTypeCount++; + if (mediaTypeCount > 1 || mediaTypeCount == 0) { + return new ContentPreview(EMOJI_ALBUM_MEDIA, 0, Lang.plural(R.string.xMedia, album.totalCount), true); + } else if (album.hasDocuments) { + return new ContentPreview(EMOJI_ALBUM_FILES, 0, Lang.plural(R.string.xFiles, album.totalCount), true); + } else if (album.hasAudios) { + return new ContentPreview(EMOJI_ALBUM_AUDIO, 0, Lang.plural(R.string.xAudios, album.totalCount), true); + } else if (album.hasVideos) { + return new ContentPreview(EMOJI_ALBUM_VIDEOS, 0, Lang.plural(R.string.xVideos, album.totalCount), true); + } else { + return new ContentPreview(EMOJI_ALBUM_PHOTOS, 0, Lang.plural(R.string.xPhotos, album.totalCount), true); + } + } + + case TdApi.PushMessageContentBasicGroupChatCreate.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageBasicGroupChatCreate.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + + case TdApi.PushMessageContentChatAddMembers.CONSTRUCTOR: { + TdApi.PushMessageContentChatAddMembers info = (TdApi.PushMessageContentChatAddMembers) push.content; + if (info.isReturned) { + return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupReturn); + } else if (info.isCurrentUser) { + return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupAddYou); + } else { + return new ContentPreview(EMOJI_GROUP, 0, Lang.getString(R.string.ChatContentGroupAdd, info.memberName), true); + } + } + + case TdApi.PushMessageContentChatDeleteMember.CONSTRUCTOR: { + TdApi.PushMessageContentChatDeleteMember info = (TdApi.PushMessageContentChatDeleteMember) push.content; + if (info.isLeft) { + return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupLeft); + } else if (info.isCurrentUser) { + return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupKickYou); + } else { + return new ContentPreview(EMOJI_GROUP, 0, Lang.getString(R.string.ChatContentGroupKick, info.memberName), true); + } + } + + case TdApi.PushMessageContentChatJoinByLink.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageChatJoinByLink.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + case TdApi.PushMessageContentChatJoinByRequest.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageChatJoinByRequest.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + case TdApi.PushMessageContentRecurringPayment.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageInvoice.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentRecurringPayment) push.content).amount, ARG_RECURRING_PAYMENT, 0); + + case TdApi.PushMessageContentChatChangePhoto.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageChatChangePhoto.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); // FIXME Server: Missing isRemoved + + case TdApi.PushMessageContentChatChangeTitle.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageChatChangeTitle.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentChatChangeTitle) push.content).title); + + case TdApi.PushMessageContentChatSetTheme.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageChatSetTheme.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentChatSetTheme) push.content).themeName); + case TdApi.PushMessageContentChatSetBackground.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageChatSetBackground.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, ((TdApi.PushMessageContentChatSetBackground) push.content).isSame ? ARG_TRUE : ARG_NONE, 0); + case TdApi.PushMessageContentSuggestProfilePhoto.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessageSuggestProfilePhoto.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null); + + case TdApi.PushMessageContentPremiumGiftCode.CONSTRUCTOR: + return getNotificationPreview(TdApi.MessagePremiumGiftCode.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, ((TdApi.PushMessageContentPremiumGiftCode) push.content).monthCount, 0); + case TdApi.PushMessageContentPremiumGiveaway.CONSTRUCTOR: { + TdApi.PushMessageContentPremiumGiveaway giveaway = (TdApi.PushMessageContentPremiumGiveaway) push.content; + if (giveaway.isPinned) { + return getNotificationPinned(R.string.ActionPinnedGiveaway, TdApi.MessagePremiumGiveaway.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, giveaway.winnerCount, giveaway.monthCount); + } else { + return getNotificationPreview(TdApi.MessagePremiumGiveaway.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, giveaway.winnerCount, giveaway.monthCount); + } + } + default: + Td.assertPushMessageContent_b17e0a62(); + throw Td.unsupported(push.content); + } + } + + private static ContentPreview getNotificationPreview (@TdApi.MessageContent.Constructors int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument, boolean argumentTranslatable, int arg1, int arg2) { + return getSimpleContentPreview(type, tdlib, chatId, sender, senderName, tdlib.isSelfSender(sender), false, new TdApi.FormattedText(argument, null), argumentTranslatable, arg1, arg2); + } + + private static ContentPreview getNotificationPreview (@TdApi.MessageContent.Constructors int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument, boolean argumentTranslatable) { + return getSimpleContentPreview(type, tdlib, chatId, sender, senderName, tdlib.isSelfSender(sender), false, new TdApi.FormattedText(argument, null), argumentTranslatable, 0, 0); + } + + private static ContentPreview getNotificationPreview (@TdApi.MessageContent.Constructors int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument) { + return getSimpleContentPreview(type, tdlib, chatId, sender, senderName, tdlib.isSelfSender(sender), false, new TdApi.FormattedText(argument, null), false, 0, 0); + } + + private static ContentPreview getNotificationPreview (@TdApi.MessageContent.Constructors int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument, int arg1, int arg2) { + return getSimpleContentPreview(type, tdlib, chatId, sender, senderName, tdlib.isSelfSender(sender), false, new TdApi.FormattedText(argument, null), false, arg1, arg2); + } + + private static String getSenderName (Tdlib tdlib, TdApi.MessageSender sender, String senderName) { + return StringUtils.isEmpty(senderName) ? tdlib.senderName(sender, true) : senderName; + } + + private static @NonNull ContentPreview getSimpleContentPreview (@TdApi.MessageContent.Constructors int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, boolean isOutgoing, boolean isChatsList, TdApi.FormattedText formattedArgument, boolean argumentTranslatable, int arg1, int arg2) { + switch (type) { + case TdApi.MessageText.CONSTRUCTOR: + return new ContentPreview(arg1 == ARG_TRUE ? EMOJI_LINK : null, R.string.YouHaveNewMessage, formattedArgument, argumentTranslatable); + case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: + return new ContentPreview(null, R.string.YouHaveNewMessage, formattedArgument, argumentTranslatable); + case TdApi.MessagePhoto.CONSTRUCTOR: + return new ContentPreview(EMOJI_PHOTO, R.string.ChatContentPhoto, formattedArgument, argumentTranslatable); + case TdApi.MessageVideo.CONSTRUCTOR: + return new ContentPreview(EMOJI_VIDEO, R.string.ChatContentVideo, formattedArgument, argumentTranslatable); + case TdApi.MessageDocument.CONSTRUCTOR: + return new ContentPreview(EMOJI_FILE, R.string.ChatContentFile, formattedArgument, argumentTranslatable); + case TdApi.MessageAudio.CONSTRUCTOR: + return new ContentPreview(EMOJI_AUDIO, 0, formattedArgument, argumentTranslatable); // FIXME: does it need a placeholder or argument is always non-null? + case TdApi.MessageContact.CONSTRUCTOR: + return new ContentPreview(EMOJI_CONTACT, R.string.AttachContact, formattedArgument, argumentTranslatable); + case TdApi.MessagePoll.CONSTRUCTOR: + if (arg1 == ARG_POLL_QUIZ) + return new ContentPreview(EMOJI_QUIZ, R.string.Quiz, formattedArgument, argumentTranslatable); + else + return new ContentPreview(EMOJI_POLL, R.string.Poll, formattedArgument, argumentTranslatable); + case TdApi.MessageVoiceNote.CONSTRUCTOR: + return new ContentPreview(EMOJI_VOICE, R.string.ChatContentVoice, formattedArgument, argumentTranslatable); + case TdApi.MessageVideoNote.CONSTRUCTOR: + return new ContentPreview(EMOJI_ROUND_VIDEO, R.string.ChatContentRoundVideo, formattedArgument, argumentTranslatable); + case TdApi.MessageAnimation.CONSTRUCTOR: + return new ContentPreview(EMOJI_GIF, R.string.ChatContentAnimation, formattedArgument, argumentTranslatable); + case TdApi.MessageLocation.CONSTRUCTOR: + return new ContentPreview(EMOJI_LOCATION, "live".equals(Td.getText(formattedArgument)) ? R.string.AttachLiveLocation : R.string.Location); + case TdApi.MessageVenue.CONSTRUCTOR: + return new ContentPreview(EMOJI_LOCATION, R.string.Location); + case TdApi.MessageSticker.CONSTRUCTOR: { + String emoji = Td.getText(formattedArgument); + boolean isAnimated = false; + if (emoji != null && emoji.startsWith("animated")) { + emoji = emoji.substring("animated".length()); + isAnimated = true; + } + return new ContentPreview(StringUtils.isEmpty(emoji) ? null : new Emoji(emoji, 0), isAnimated && !isChatsList ? R.string.AnimatedSticker : R.string.Sticker); + } + case TdApi.MessageScreenshotTaken.CONSTRUCTOR: + if (isOutgoing) + return new ContentPreview(EMOJI_SCREENSHOT, R.string.YouTookAScreenshot); + else if (isChatsList) + return new ContentPreview(EMOJI_SCREENSHOT, R.string.ChatContentScreenshot); + else + return new ContentPreview(EMOJI_SCREENSHOT, 0, Lang.getString(R.string.XTookAScreenshot, getSenderName(tdlib, sender, senderName)), true); + case TdApi.MessageGame.CONSTRUCTOR: + return new ContentPreview(EMOJI_GAME, 0, Lang.getString(ChatId.isMultiChat(chatId) ? (isOutgoing ? R.string.NotificationGame_group_outgoing : R.string.NotificationGame_group) : (isOutgoing ? R.string.NotificationGame_outgoing : R.string.NotificationGame), Td.getText(formattedArgument)), true); + case TdApi.MessageInvoice.CONSTRUCTOR: + if (arg1 == ARG_RECURRING_PAYMENT) { + return new ContentPreview(EMOJI_INVOICE, R.string.RecurringPayment, Td.isEmpty(formattedArgument) ? null : Lang.getString(R.string.PaidX, Td.getText(formattedArgument)), true); + } else { + return new ContentPreview(EMOJI_INVOICE, R.string.Invoice, Td.isEmpty(formattedArgument) ? null : Lang.getString(R.string.InvoiceFor, Td.getText(formattedArgument)), true); + } + case TdApi.MessageContactRegistered.CONSTRUCTOR: + return new ContentPreview(EMOJI_USER_JOINED, 0, Lang.getString(R.string.NotificationContactJoined, getSenderName(tdlib, sender, senderName)), true); + case TdApi.MessageSupergroupChatCreate.CONSTRUCTOR: + if (tdlib.isChannel(chatId)) + return new ContentPreview(EMOJI_CHANNEL, R.string.ActionCreateChannel); + else + return new ContentPreview(EMOJI_GROUP, isOutgoing ? R.string.ChatContentGroupCreate_outgoing : R.string.ChatContentGroupCreate); + case TdApi.MessageBasicGroupChatCreate.CONSTRUCTOR: + return new ContentPreview(EMOJI_GROUP, isOutgoing ? R.string.ChatContentGroupCreate_outgoing : R.string.ChatContentGroupCreate); + case TdApi.MessageChatJoinByLink.CONSTRUCTOR: + return new ContentPreview(EMOJI_GROUP, isOutgoing ? R.string.ChatContentGroupJoin_outgoing : R.string.ChatContentGroupJoin); + case TdApi.MessageChatJoinByRequest.CONSTRUCTOR: + return new ContentPreview(EMOJI_GROUP, isOutgoing ? R.string.ChatContentGroupAccept_outgoing : R.string.ChatContentGroupAccept); + case TdApi.MessageChatChangePhoto.CONSTRUCTOR: + if (tdlib.isChannel(chatId)) + return new ContentPreview(EMOJI_PHOTO, R.string.ActionChannelChangedPhoto); + else + return new ContentPreview(EMOJI_PHOTO, isOutgoing ? R.string.ChatContentGroupPhoto_outgoing : R.string.ChatContentGroupPhoto); + case TdApi.MessageChatDeletePhoto.CONSTRUCTOR: + if (tdlib.isChannel(chatId)) + return new ContentPreview(EMOJI_CHANNEL, R.string.ActionChannelRemovedPhoto); + else + return new ContentPreview(EMOJI_GROUP, isOutgoing ? R.string.ChatContentGroupPhotoRemove_outgoing : R.string.ChatContentGroupPhotoRemove); + case TdApi.MessageChatChangeTitle.CONSTRUCTOR: + if (tdlib.isChannel(chatId)) + return new ContentPreview(EMOJI_CHANNEL, 0, Lang.getString(R.string.ActionChannelChangedTitleTo, Td.getText(formattedArgument)), true); + else + return new ContentPreview(EMOJI_GROUP, 0, Lang.getString(isOutgoing ? R.string.ChatContentGroupName_outgoing : R.string.ChatContentGroupName, Td.getText(formattedArgument)), true); + case TdApi.MessageChatSetTheme.CONSTRUCTOR: + if (StringUtils.isEmpty(formattedArgument.text)) { + if (isOutgoing) + return new ContentPreview(EMOJI_THEME, R.string.ChatContentThemeDisabled_outgoing); + else + return new ContentPreview(EMOJI_THEME, R.string.ChatContentThemeDisabled); + } else { + if (isOutgoing) + return new ContentPreview(EMOJI_THEME, 0, TD.toFormattedText(Lang.getStringBold(R.string.ChatContentThemeSet_outgoing, formattedArgument.text), true)); + else + return new ContentPreview(EMOJI_THEME, 0, TD.toFormattedText(Lang.getStringBold(R.string.ChatContentThemeSet, formattedArgument.text), true)); + } + case TdApi.MessageChatSetMessageAutoDeleteTime.CONSTRUCTOR: { + if (arg1 > 0) { + final int secondsRes, minutesRes, hoursRes, daysRes, weeksRes, monthsRes; + if (ChatId.isUserChat(chatId)) { + secondsRes = R.string.ChatContentTtlSeconds; + minutesRes = R.string.ChatContentTtlMinutes; + hoursRes = R.string.ChatContentTtlHours; + daysRes = R.string.ChatContentTtlDays; + weeksRes = R.string.ChatContentTtlWeeks; + monthsRes = R.string.ChatContentTtlMonths; + } else if (tdlib.isChannel(chatId)) { + secondsRes = R.string.ChatContentChannelTtlSeconds; + minutesRes = R.string.ChatContentChannelTtlMinutes; + hoursRes = R.string.ChatContentChannelTtlHours; + daysRes = R.string.ChatContentChannelTtlDays; + weeksRes = R.string.ChatContentChannelTtlWeeks; + monthsRes = R.string.ChatContentChannelTtlMonths; + } else { + secondsRes = R.string.ChatContentGroupTtlSeconds; + minutesRes = R.string.ChatContentGroupTtlMinutes; + hoursRes = R.string.ChatContentGroupTtlHours; + daysRes = R.string.ChatContentGroupTtlDays; + weeksRes = R.string.ChatContentGroupTtlWeeks; + monthsRes = R.string.ChatContentGroupTtlMonths; + } + final CharSequence text = Lang.pluralDuration(arg1, TimeUnit.SECONDS, secondsRes, minutesRes, hoursRes, daysRes, weeksRes, monthsRes); + return new ContentPreview(EMOJI_TIMER, 0, TD.toFormattedText(text, false), true); + } else { + final int stringRes; + if (ChatId.isUserChat(chatId)) { + stringRes = R.string.ChatContentTtlOff; + } else if (tdlib.isChannel(chatId)) { + stringRes = R.string.ChatContentChannelTtlOff; + } else { + stringRes = R.string.ChatContentGroupTtlOff; + } + return new ContentPreview(EMOJI_TIMER_OFF, stringRes); + } + } + case TdApi.MessageDice.CONSTRUCTOR: { + String diceEmoji = !Td.isEmpty(formattedArgument) && tdlib.isDiceEmoji(formattedArgument.text) ? formattedArgument.text : EMOJI_DICE.textRepresentation; + if (EMOJI_DART.textRepresentation.equals(diceEmoji)) { + return new ContentPreview(EMOJI_DART, getDartRes(arg1)); + } + if (EMOJI_DICE.textRepresentation.equals(diceEmoji)) { + if (arg1 >= 1 && arg1 <= 6) { + switch (arg1) { + case 1: + return new ContentPreview(EMOJI_DICE_1, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); + case 2: + return new ContentPreview(EMOJI_DICE_2, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); + case 3: + return new ContentPreview(EMOJI_DICE_3, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); + case 4: + return new ContentPreview(EMOJI_DICE_4, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); + case 5: + return new ContentPreview(EMOJI_DICE_5, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); + case 6: + return new ContentPreview(EMOJI_DICE_6, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); + } + } + return new ContentPreview(EMOJI_DICE, R.string.ChatContentDice); + } + return new ContentPreview(new Emoji(diceEmoji, 0), 0); + } + case TdApi.MessageExpiredPhoto.CONSTRUCTOR: + return new ContentPreview(EMOJI_SECRET_PHOTO, R.string.AttachPhotoExpired); + case TdApi.MessageExpiredVideo.CONSTRUCTOR: + return new ContentPreview(EMOJI_SECRET_VIDEO, R.string.AttachVideoExpired); + case TdApi.MessageCall.CONSTRUCTOR: + switch (arg1) { + case ARG_CALL_DECLINED: + return new ContentPreview(EMOJI_CALL_DECLINED, isOutgoing ? R.string.OutgoingCall : R.string.CallMessageIncomingDeclined); + case ARG_CALL_MISSED: + return new ContentPreview(EMOJI_CALL_MISSED, isOutgoing ? R.string.CallMessageOutgoingMissed : R.string.MissedCall); + default: + if (arg1 > 0) { + return new ContentPreview(EMOJI_CALL, 0, Lang.getString(R.string.ChatContentCallWithDuration, Lang.getString(isOutgoing ? R.string.OutgoingCall : R.string.IncomingCall), Lang.getDurationFull(arg1)), true); + } else { + return new ContentPreview(EMOJI_CALL, isOutgoing ? R.string.OutgoingCall : R.string.IncomingCall); + } + } + case TdApi.MessageChatUpgradeFrom.CONSTRUCTOR: + case TdApi.MessageChatUpgradeTo.CONSTRUCTOR: + return new ContentPreview(EMOJI_GROUP, R.string.GroupUpgraded); + + case TdApi.MessagePremiumGiveawayCreated.CONSTRUCTOR: + return new ContentPreview(EMOJI_GIFT, R.string.BoostingGiveawayJustStarted); + case TdApi.MessagePremiumGiveawayCompleted.CONSTRUCTOR: + return new ContentPreview(EMOJI_GIFT, 0, Lang.plural(R.string.BoostingGiveawayServiceWinnersSelected, arg1), true); + + case TdApi.MessagePremiumGiveaway.CONSTRUCTOR: { + int winnerCount = arg1; + int monthCount = arg2; + String text; + if (winnerCount > 0) { + text = Lang.getString(R.string.format_giveawayInfo, + Lang.getString(R.string.Giveaway), + Lang.plural(R.string.xFutureWinners, winnerCount) + ); + } else { + text = Lang.getString(R.string.Giveaway); + } + return new ContentPreview(EMOJI_GIFT, 0, text, true); + } + + // Must be supported by the caller and never passed to this method. + case TdApi.MessageGiftedPremium.CONSTRUCTOR: + case TdApi.MessageGameScore.CONSTRUCTOR: + case TdApi.MessageProximityAlertTriggered.CONSTRUCTOR: + case TdApi.MessageChatAddMembers.CONSTRUCTOR: + case TdApi.MessageChatDeleteMember.CONSTRUCTOR: + case TdApi.MessageCustomServiceAction.CONSTRUCTOR: + case TdApi.MessageBotWriteAccessAllowed.CONSTRUCTOR: + case TdApi.MessageWebAppDataSent.CONSTRUCTOR: + case TdApi.MessageInviteVideoChatParticipants.CONSTRUCTOR: + case TdApi.MessageVideoChatStarted.CONSTRUCTOR: + case TdApi.MessageVideoChatEnded.CONSTRUCTOR: + case TdApi.MessageVideoChatScheduled.CONSTRUCTOR: + case TdApi.MessagePinMessage.CONSTRUCTOR: + case TdApi.MessagePaymentSuccessful.CONSTRUCTOR: + throw new IllegalArgumentException(Integer.toString(type)); + + case TdApi.MessageStory.CONSTRUCTOR: + case TdApi.MessageUsersShared.CONSTRUCTOR: + case TdApi.MessageChatShared.CONSTRUCTOR: + case TdApi.MessageSuggestProfilePhoto.CONSTRUCTOR: + case TdApi.MessageForumTopicCreated.CONSTRUCTOR: + case TdApi.MessageForumTopicEdited.CONSTRUCTOR: + case TdApi.MessageForumTopicIsClosedToggled.CONSTRUCTOR: + case TdApi.MessageForumTopicIsHiddenToggled.CONSTRUCTOR: + case TdApi.MessagePassportDataSent.CONSTRUCTOR: + case TdApi.MessageChatSetBackground.CONSTRUCTOR: + case TdApi.MessagePremiumGiftCode.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayWinners.CONSTRUCTOR: + // TODO support these previews + return new ContentPreview(EMOJI_QUIZ, R.string.UnsupportedMessage); + + case TdApi.MessageUnsupported.CONSTRUCTOR: + return new ContentPreview(EMOJI_QUIZ, R.string.UnsupportedMessageType); + + // Bots only. Unused + case TdApi.MessagePassportDataReceived.CONSTRUCTOR: + case TdApi.MessagePaymentSuccessfulBot.CONSTRUCTOR: + case TdApi.MessageWebAppDataReceived.CONSTRUCTOR: + default: + Td.assertMessageContent_d40af239(); + throw new UnsupportedOperationException(Integer.toString(type)); + } + } + + static int getDartRes (int value) { + switch (value) { + case 0: + return R.string.ChatContentDart; + case 1: + return R.string.ChatContentDart1; + case 2: + return R.string.ChatContentDart2; + case 3: + return R.string.ChatContentDart3; + case 4: + return R.string.ChatContentDart4; + case 6: + return R.string.ChatContentDart6; + case 5: + default: + return R.string.ChatContentDart5; + } + } + + @NonNull + public String buildText (boolean allowIcon) { + if (emoji == null || (allowIcon && emoji.iconRepresentation != 0)) { + return Td.isEmpty(formattedText) ? (placeholderText != 0 ? Lang.getString(placeholderText) : "") : formattedText.text; + } else if (Td.isEmpty(formattedText)) { + return placeholderText != 0 ? emoji.textRepresentation + " " + Lang.getString(placeholderText) : emoji.textRepresentation; + } else if (formattedText.text.startsWith(emoji.textRepresentation)) { + return formattedText.text; + } else { + return emoji.textRepresentation + " " + formattedText.text; + } + } + + public TdApi.FormattedText buildFormattedText (boolean allowIcon) { + if (emoji == null || (allowIcon && emoji.iconRepresentation != 0)) { + return Td.isEmpty(formattedText) ? new TdApi.FormattedText(placeholderText != 0 ? Lang.getString(placeholderText) : "", null) : formattedText; + } else if (Td.isEmpty(formattedText)) { + return new TdApi.FormattedText(placeholderText != 0 ? emoji.textRepresentation + " " + Lang.getString(placeholderText) : emoji.textRepresentation, null); + } else if (formattedText.text.startsWith(emoji.textRepresentation)) { + return formattedText; + } else { + return TD.withPrefix(emoji.textRepresentation + " ", formattedText); + } + } + + @Override + @NonNull + public String toString () { + return buildText(false); + } + + private Refresher refresher; + private boolean isMediaGroup; + + public ContentPreview setRefresher (Refresher refresher, boolean isMediaGroup) { + this.refresher = refresher; + this.isMediaGroup = isMediaGroup; + return this; + } + + public boolean hasRefresher () { + return refresher != null; + } + + public boolean isMediaGroup () { + return isMediaGroup; + } + + public void refreshContent (@NonNull RefreshCallback callback) { + if (refresher != null) { + refresher.runRefresher(this, callback); + } + } + + private Tdlib.Album album; + + @Nullable + public Tdlib.Album getAlbum () { + return album; + } + + public interface Refresher { + void runRefresher (ContentPreview oldPreview, RefreshCallback callback); + } + + public interface RefreshCallback { + void onContentPreviewChanged (long chatId, long messageId, ContentPreview newPreview, ContentPreview oldPreview); + + default void onContentPreviewNotChanged (long chatId, long messageId, ContentPreview oldContent) {} + } + + public static final class Emoji { + public final @NonNull String textRepresentation; + public final @DrawableRes int iconRepresentation; + + public Emoji (@NonNull String textRepresentation, @DrawableRes int iconRepresentation) { + this.textRepresentation = textRepresentation; + this.iconRepresentation = iconRepresentation; + } + + @NonNull + @Override + public String toString () { + return textRepresentation; + } + + public Emoji toNewEmoji (String newEmoji) { + return StringUtils.equalsOrBothEmpty(this.textRepresentation, newEmoji) ? this : new Emoji(newEmoji, iconRepresentation); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/data/DoubleTextWrapper.java b/app/src/main/java/org/thunderdog/challegram/data/DoubleTextWrapper.java index 6fdaea1d87..63b9e452ba 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/DoubleTextWrapper.java +++ b/app/src/main/java/org/thunderdog/challegram/data/DoubleTextWrapper.java @@ -299,7 +299,7 @@ public boolean isPremiumLocked () { public void setForcedSubtitle (CharSequence newSubtitle) { this.forcedSubtitle = newSubtitle; setIgnoreOnline(true); - setSubtitle(!StringUtils.isEmpty(forcedSubtitle) ? forcedSubtitle: subtitle); + setSubtitle(!StringUtils.isEmpty(forcedSubtitle) ? forcedSubtitle : subtitle); } public void setSubtitle (CharSequence newSubtitle) { @@ -421,8 +421,8 @@ private void buildTitle () { emojiStatusDrawable = EmojiStatusHelper.makeDrawable(null, tdlib, user, new TextColorSetOverride(TextColorSets.Regular.NORMAL) { @Override - public int emojiStatusColor () { - return Theme.getColor(ColorId.iconActive); + public long mediaTextComplexColor () { + return Theme.newComplexColor(true, ColorId.iconActive); } }, this::invalidateEmojiStatusReceiver); emojiStatusDrawable.invalidateTextMedia(); @@ -534,7 +534,7 @@ public void draw (T view, Receiver receiver, Drawable incognitoIcon = view.getSparseDrawable(R.drawable.baseline_lock_16, ColorId.text); float x = currentWidth - Screen.dp(18 + 16); float y = receiver.centerY(); - Drawables.draw(c, incognitoIcon, x, y - incognitoIcon.getMinimumHeight() / 2f, Paints.getPorterDuffPaint(Theme.getColor(ColorId.text))); + Drawables.draw(c, incognitoIcon, x, y - incognitoIcon.getMinimumHeight() / 2f, PorterDuffPaint.get(ColorId.text)); } int offset = 0; if (trimmedTitle != null) { diff --git a/app/src/main/java/org/thunderdog/challegram/data/EmojiMessageContentType.java b/app/src/main/java/org/thunderdog/challegram/data/EmojiMessageContentType.java new file mode 100644 index 0000000000..978200913f --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/data/EmojiMessageContentType.java @@ -0,0 +1,28 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 24/10/2023 at 02:14 + */ +package org.thunderdog.challegram.data; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +@IntDef({ + EmojiMessageContentType.NOT_EMOJI, EmojiMessageContentType.ANIMATED_EMOJI, EmojiMessageContentType.NON_BUBBLE_EMOJI +}) +public @interface EmojiMessageContentType { + int NOT_EMOJI = 0, ANIMATED_EMOJI = 1, NON_BUBBLE_EMOJI = 2; +} diff --git a/app/src/main/java/org/thunderdog/challegram/data/FileComponent.java b/app/src/main/java/org/thunderdog/challegram/data/FileComponent.java index efa8c48fef..a6d0ecabb9 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/FileComponent.java +++ b/app/src/main/java/org/thunderdog/challegram/data/FileComponent.java @@ -43,6 +43,7 @@ import org.thunderdog.challegram.player.TGPlayerController; import org.thunderdog.challegram.telegram.TGLegacyAudioManager; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibFilesManager; import org.thunderdog.challegram.telegram.TdlibManager; import org.thunderdog.challegram.theme.ColorId; @@ -163,7 +164,7 @@ public void setDoc (@NonNull TdApi.Document doc) { if (hasPreview) { this.progress.setBackgroundColor(0x44000000); } else { - this.progress.setBackgroundColorId(TD.getFileColorId(doc, context.isOutgoingBubble())); + this.progress.setBackgroundColorId(TdlibAccentColor.getFileColorId(doc, context.isOutgoingBubble())); } this.progress.setFile(doc.document, context.getMessage()); if (viewProvider != null) { diff --git a/app/src/main/java/org/thunderdog/challegram/data/InlineResult.java b/app/src/main/java/org/thunderdog/challegram/data/InlineResult.java index 1e9311b243..8640c444b0 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/InlineResult.java +++ b/app/src/main/java/org/thunderdog/challegram/data/InlineResult.java @@ -400,6 +400,7 @@ public boolean setThumbLocation (MediaViewThumbLocation location, View view, int // Static stuff public static InlineResult valueOf (BaseActivity context, Tdlib tdlib, TdApi.Message message) { + //noinspection SwitchIntDef switch (message.content.getConstructor()) { case TdApi.MessageAudio.CONSTRUCTOR: { return new InlineResultCommon(context, tdlib, message, (TdApi.MessageAudio) message.content, null).setMessage(message); diff --git a/app/src/main/java/org/thunderdog/challegram/data/InlineResultCommand.java b/app/src/main/java/org/thunderdog/challegram/data/InlineResultCommand.java index 2cfb1ecdc2..d1e8d21b0b 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/InlineResultCommand.java +++ b/app/src/main/java/org/thunderdog/challegram/data/InlineResultCommand.java @@ -94,7 +94,7 @@ protected int getContentHeight () { @Override public void requestContent (ComplexReceiver receiver, boolean isInvalidate) { - receiver.clearReceivers((receiverType, receiver1, key) -> receiverType == ComplexReceiver.RECEIVER_TYPE_IMAGE && key == 0); + receiver.clearReceivers((receiverType, receiver1, key) -> receiverType == ComplexReceiver.ReceiverType.IMAGE && key == 0); receiver.getImageReceiver(0).requestFile(userContext.getImageFile()); } diff --git a/app/src/main/java/org/thunderdog/challegram/data/InlineResultCommon.java b/app/src/main/java/org/thunderdog/challegram/data/InlineResultCommon.java index aa49fa45b3..df837b535c 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/InlineResultCommon.java +++ b/app/src/main/java/org/thunderdog/challegram/data/InlineResultCommon.java @@ -42,6 +42,7 @@ import org.thunderdog.challegram.mediaview.data.MediaItem; import org.thunderdog.challegram.player.TGPlayerController; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibFilesManager; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; @@ -90,8 +91,8 @@ public InlineResultCommon (BaseActivity context, Tdlib tdlib, TdApi.InlineQueryR setMediaPreview(MediaPreview.valueOf(tdlib, data.video, Screen.dp(50f), Screen.dp(3f), false)); if (getMediaPreview() == null) { - int placeholderColorId = TD.getColorIdForString(data.video.fileName.isEmpty() ? data.id : data.video.fileName); - avatarPlaceholder = new AvatarPlaceholder(AVATAR_PLACEHOLDER_RADIUS, new AvatarPlaceholder.Metadata(placeholderColorId, TD.getLetters(title)), null); + TdlibAccentColor accentColor = tdlib.accentColorForString(data.video.fileName.isEmpty() ? data.id : data.video.fileName); + avatarPlaceholder = new AvatarPlaceholder(AVATAR_PLACEHOLDER_RADIUS, new AvatarPlaceholder.Metadata(accentColor, TD.getLetters(title)), null); } } @@ -123,8 +124,8 @@ public InlineResultCommon (BaseActivity context, Tdlib tdlib, TdApi.InlineQueryR if (getMediaPreview() == null) { Letters letters = TD.getLetters(data.contact.firstName, data.contact.lastName); - int placeholderColorId = data.contact.userId != 0 ? TD.getAvatarColorId(data.contact.userId, tdlib.myUserId()) : ColorId.avatarInactive; //TD.getColorIdForString(TD.getUserName(data.contact.firstName, data.contact.lastName)); - avatarPlaceholder = new AvatarPlaceholder(AVATAR_PLACEHOLDER_RADIUS, new AvatarPlaceholder.Metadata(placeholderColorId, letters), null); + TdlibAccentColor accentColor = data.contact.userId != 0 ? tdlib.cache().userAccentColor(data.contact.userId) : tdlib.accentColor(TdlibAccentColor.InternalId.INACTIVE); + avatarPlaceholder = new AvatarPlaceholder(AVATAR_PLACEHOLDER_RADIUS, new AvatarPlaceholder.Metadata(accentColor, letters), null); } } @@ -356,7 +357,7 @@ public InlineResultCommon (BaseActivity context, Tdlib tdlib, File file, String this.fileProgress.setSimpleListener(this); if (this.getMediaPreview() == null) { this.fileProgress.setDownloadedIconRes(isFolder ? R.drawable.baseline_folder_24 : R.drawable.baseline_insert_drive_file_24); - this.fileProgress.setBackgroundColorId(TD.getFileColorId(file.getName(), mimeType, false)); + this.fileProgress.setBackgroundColorId(TdlibAccentColor.getFileColorId(file.getName(), mimeType, false)); } else { if (isFolder) { this.fileProgress.setBackgroundColor(0x66000000); @@ -456,7 +457,7 @@ public InlineResultCommon (BaseActivity context, Tdlib tdlib, TdApi.Message mess this.fileProgress.setSimpleListener(this); this.fileProgress.setDocumentMetadata(document, this.getMediaPreview() == null); if (this.getMediaPreview() == null) { - this.fileProgress.setBackgroundColorId(TD.getFileColorId(document, false)); + this.fileProgress.setBackgroundColorId(TdlibAccentColor.getFileColorId(document, false)); } else { this.fileProgress.setBackgroundColor(0x44000000); } @@ -487,7 +488,7 @@ public InlineResultCommon (BaseActivity context, Tdlib tdlib, TdApi.InlineQueryR this.fileProgress.setPausedIconRes(R.drawable.baseline_insert_drive_file_24); this.fileProgress.setDocumentMetadata(data.document, this.getMediaPreview() == null); if (this.getMediaPreview() == null) { - this.fileProgress.setBackgroundColorId(TD.getFileColorId(data.document, false)); + this.fileProgress.setBackgroundColorId(TdlibAccentColor.getFileColorId(data.document, false)); } else { this.fileProgress.setBackgroundColor(0x44000000); } @@ -565,13 +566,13 @@ protected void drawInternal (CustomResultView view, Canvas c, ComplexReceiver re c.drawCircle(cx, cy, Screen.dp(25f), Paints.fillingPaint(Theme.getColor(customColorId))); } if (customIcon != null) { - Drawables.draw(c, customIcon, cx - customIcon.getMinimumWidth() / 2f, cy - customIcon.getMinimumHeight() / 2f, Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, customIcon, cx - customIcon.getMinimumWidth() / 2f, cy - customIcon.getMinimumHeight() / 2f, Paints.whitePorterDuffPaint()); } } else if (fileProgress == null || getMediaPreview() != null) { if (getMediaPreview() != null) { getMediaPreview().draw(view, c, receiver, (int) rectF.left, (int) rectF.top, (int) rectF.width(), (int) rectF.height(), getMediaPreview().getCornerRadius(), 1f); } else { - c.drawRoundRect(rectF, rectF.width() / 2f, rectF.height() / 2f, Paints.fillingPaint(Theme.getColor(avatarPlaceholder.metadata.colorId))); + c.drawRoundRect(rectF, rectF.width() / 2f, rectF.height() / 2f, Paints.fillingPaint(avatarPlaceholder.metadata.accentColor.getPrimaryColor())); avatarPlaceholder.draw(c, rectF.centerX(), rectF.centerY(), 1f, avatarPlaceholder.getRadius(), false); } } @@ -583,7 +584,7 @@ protected void drawInternal (CustomResultView view, Canvas c, ComplexReceiver re } else { if (getMediaPreview() == null || getMediaPreview().needPlaceholder(receiver)) { c.drawRoundRect(rectF, radius, radius, Paints.fillingPaint(Theme.getColor(ColorId.playerCoverPlaceholder))); - Drawable drawable = view.getSparseDrawable(R.drawable.baseline_music_note_24, 0); + Drawable drawable = view.getSparseDrawable(R.drawable.baseline_music_note_24, ColorId.NONE); Drawables.draw(c, drawable, rectF.centerX() - drawable.getMinimumWidth() / 2f, rectF.centerY() - drawable.getMinimumHeight() / 2f, Paints.getNotePorterDuffPaint()); } if (getMediaPreview() != null) { diff --git a/app/src/main/java/org/thunderdog/challegram/data/InlineResultMention.java b/app/src/main/java/org/thunderdog/challegram/data/InlineResultMention.java index 0d9ec5dd05..a1442b759d 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/InlineResultMention.java +++ b/app/src/main/java/org/thunderdog/challegram/data/InlineResultMention.java @@ -110,7 +110,7 @@ protected int getContentHeight () { @Override public void requestContent (ComplexReceiver receiver, boolean isInvalidate) { - receiver.clearReceivers((receiverType, receiver1, key) -> receiverType == ComplexReceiver.RECEIVER_TYPE_IMAGE && key == 0); + receiver.clearReceivers((receiverType, receiver1, key) -> receiverType == ComplexReceiver.ReceiverType.IMAGE && key == 0); receiver.getAvatarReceiver(0).requestUser(tdlib, userContext.getId(), AvatarReceiver.Options.NONE); } diff --git a/app/src/main/java/org/thunderdog/challegram/data/InlineResultMultiline.java b/app/src/main/java/org/thunderdog/challegram/data/InlineResultMultiline.java index 1b95828e0b..ac86ec4fb3 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/InlineResultMultiline.java +++ b/app/src/main/java/org/thunderdog/challegram/data/InlineResultMultiline.java @@ -29,6 +29,7 @@ import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.loader.ComplexReceiver; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; @@ -63,8 +64,8 @@ public InlineResultMultiline (BaseActivity context, Tdlib tdlib, TdApi.InlineQue this.description = article.description; this.url = article.hideUrl || article.url.isEmpty() ? null : article.url; // ? null : article.url; - int placeholderColorId = TD.getColorIdForString(article.url.isEmpty() ? article.id : article.url); - avatarPlaceholder = new AvatarPlaceholder(AVATAR_PLACEHOLDER_RADIUS, new AvatarPlaceholder.Metadata(placeholderColorId, TD.getLetters(title)), null); + TdlibAccentColor accentColor = tdlib.accentColorForString(article.url.isEmpty() ? article.id : article.url); + avatarPlaceholder = new AvatarPlaceholder(AVATAR_PLACEHOLDER_RADIUS, new AvatarPlaceholder.Metadata(accentColor, TD.getLetters(title)), null); setMediaPreview(MediaPreview.valueOf(tdlib, article.thumbnail, null, Screen.dp(50f), Screen.dp(3f))); layoutInternal(Screen.currentWidth()); @@ -76,8 +77,8 @@ public InlineResultMultiline (BaseActivity context, Tdlib tdlib, TdApi.InlineQue this.title = game.game.title; this.description = game.game.description; - int placeholderColorId = TD.getColorIdForString(game.game.shortName); - avatarPlaceholder = new AvatarPlaceholder(AVATAR_PLACEHOLDER_RADIUS, new AvatarPlaceholder.Metadata(placeholderColorId, TD.getLetters(title)), null); + TdlibAccentColor accentColor = tdlib.accentColorForString(game.game.shortName); + avatarPlaceholder = new AvatarPlaceholder(AVATAR_PLACEHOLDER_RADIUS, new AvatarPlaceholder.Metadata(accentColor, TD.getLetters(title)), null); setMediaPreview(MediaPreview.valueOf(tdlib, game.game, Screen.dp(50f), Screen.dp(3f))); @@ -90,7 +91,7 @@ public InlineResultMultiline (BaseActivity context, Tdlib tdlib, TdApi.Message m setMessage(message); TdApi.FormattedText text = Td.textOrCaption(message.content); - TdApi.WebPage webPage = message.content.getConstructor() == TdApi.MessageText.CONSTRUCTOR ? ((TdApi.MessageText) message.content).webPage : null; + TdApi.WebPage webPage = Td.isText(message.content) ? ((TdApi.MessageText) message.content).webPage : null; if (webPage != null) { this.title = Strings.any(webPage.title, webPage.document != null ? webPage.document.fileName : null, webPage.audio != null ? webPage.audio.title : null, webPage.siteName); @@ -102,6 +103,7 @@ public InlineResultMultiline (BaseActivity context, Tdlib tdlib, TdApi.Message m } else if (text != null) { TdApi.TextEntity effectiveEntity = null; main: for (TdApi.TextEntity entity : text.entities) { + //noinspection SwitchIntDef switch (entity.type.getConstructor()) { case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: { if (effectiveEntity == null) { @@ -138,7 +140,7 @@ public InlineResultMultiline (BaseActivity context, Tdlib tdlib, TdApi.Message m } } if (effectiveEntity != null) { - if (effectiveEntity.type.getConstructor() == TdApi.TextEntityTypeUrl.CONSTRUCTOR) { + if (Td.isUrl(effectiveEntity.type)) { TdApi.FormattedText part1 = effectiveEntity.offset > 0 ? Td.substring(text, 0, effectiveEntity.offset) : null; TdApi.FormattedText part2 = effectiveEntity.offset + effectiveEntity.length < text.text.length() ? Td.substring(text, effectiveEntity.offset + effectiveEntity.length) : null; TdApi.FormattedText finalText = Td.trim(part1 != null && part2 != null ? Td.concat(part1, new TdApi.FormattedText("…", new TdApi.TextEntity[]{new TdApi.TextEntity(0, 1, new TdApi.TextEntityTypeTextUrl(url))}), part2) : part1 != null ? part1 : part2); @@ -164,8 +166,8 @@ public InlineResultMultiline (BaseActivity context, Tdlib tdlib, TdApi.Message m this.url = ""; } - int placeholderColorId = TD.getColorIdForString(url); - avatarPlaceholder = new AvatarPlaceholder(AVATAR_PLACEHOLDER_RADIUS, new AvatarPlaceholder.Metadata(placeholderColorId, TD.getLetters(title)), null); + TdlibAccentColor accentColor = tdlib.accentColorForString(url); + avatarPlaceholder = new AvatarPlaceholder(AVATAR_PLACEHOLDER_RADIUS, new AvatarPlaceholder.Metadata(accentColor, TD.getLetters(title)), null); layoutInternal(Screen.currentWidth()); } @@ -292,7 +294,7 @@ protected void drawInternal (CustomResultView view, Canvas c, ComplexReceiver re } else if (avatarPlaceholder != null) { RectF rectF = Paints.getRectF(); rectF.set(Screen.dp(11f), Screen.dp(11f), Screen.dp(11f) + Screen.dp(50f), Screen.dp(11f) + Screen.dp(50f)); - c.drawRoundRect(rectF, Screen.dp(3f), Screen.dp(3f), Paints.fillingPaint(Theme.getColor(avatarPlaceholder.metadata.colorId))); + c.drawRoundRect(rectF, Screen.dp(3f), Screen.dp(3f), Paints.fillingPaint(avatarPlaceholder.metadata.accentColor.getPrimaryColor())); avatarPlaceholder.draw(c, rectF.centerX(), rectF.centerY(), 1f, avatarPlaceholder.getRadius(), false); } diff --git a/app/src/main/java/org/thunderdog/challegram/data/PageBlockMedia.java b/app/src/main/java/org/thunderdog/challegram/data/PageBlockMedia.java index e3b2910984..7115c5abcd 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/PageBlockMedia.java +++ b/app/src/main/java/org/thunderdog/challegram/data/PageBlockMedia.java @@ -453,7 +453,7 @@ protected void drawInternal (T view, Canvas final int x = ((view.getMeasuredWidth() - getMinimumContentPadding(true) - getMinimumContentPadding(false)) / 2 - wrapper.getCellWidth() / 2) + getMinimumContentPadding(true); wrapper.draw(view, c, x, getContentTop(), preview, receiver, 1f); if (!StringUtils.isEmpty(url)) { - Drawables.draw(c, linkIcon, receiver.getRight() - linkIcon.getMinimumWidth() - Screen.dp(9f), receiver.getTop() + Screen.dp(9f), Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, linkIcon, receiver.getRight() - linkIcon.getMinimumWidth() - Screen.dp(9f), receiver.getTop() + Screen.dp(9f), Paints.whitePorterDuffPaint()); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/data/PageBlockRichText.java b/app/src/main/java/org/thunderdog/challegram/data/PageBlockRichText.java index d034e9181f..d6b82552c6 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/PageBlockRichText.java +++ b/app/src/main/java/org/thunderdog/challegram/data/PageBlockRichText.java @@ -35,6 +35,7 @@ import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; @@ -289,7 +290,7 @@ public void onClickAt (View view, float x, float y) { showChatLinkSubtitle(publicChat, time, openParameters); } else if (publicChat.type.getConstructor() == TdApi.ChatTypeSupergroup.CONSTRUCTOR) { context.tdlib().cache().supergroupFull(ChatId.toSupergroupId(publicChat.id), fullInfo -> { - int fullMemberCount = fullInfo.memberCount; + int fullMemberCount = fullInfo != null ? fullInfo.memberCount : 0; if (fullMemberCount > 1) { showChatLinkSubtitle(publicChat, time, openParameters); } @@ -335,7 +336,7 @@ public PageBlockRichText (ViewController context, TdApi.PageBlockFooter foote super(context, footer); setText(footer.footer, getFooterProvider(), TextColorSets.InstantView.FOOTER, openParameters); if (!isPost) { - this.backgroundColorId = 0; + this.backgroundColorId = ColorId.NONE; } else { this.paddingTop = 3f; } @@ -400,8 +401,7 @@ public PageBlockRichText (ViewController context, TdApi.PageBlock mediaBlock, private ImageFile avatarMiniThumbnail, avatarPreview, avatarFull; private boolean needAvatar; - private @ColorId - int avatarPlaceholderColorId; + private TdlibAccentColor accentColor; public PageBlockRichText (ViewController context, TdApi.PageBlockEmbeddedPost embeddedPost, @Nullable TdlibUi.UrlOpenParameters openParameters) { super(context, embeddedPost); @@ -413,6 +413,7 @@ public PageBlockRichText (ViewController context, TdApi.PageBlockEmbeddedPost this.needAvatar = true; TdApi.PhotoSize size = Td.findSmallest(embeddedPost.authorPhoto); + accentColor = context.tdlib().accentColorForString(embeddedPost.author); if (size != null) { if (embeddedPost.authorPhoto.minithumbnail != null) { avatarMiniThumbnail = new ImageFileLocal(embeddedPost.authorPhoto.minithumbnail); @@ -431,8 +432,6 @@ public PageBlockRichText (ViewController context, TdApi.PageBlockEmbeddedPost } else { avatarPreview.setNoBlur(); } - } else { - avatarPlaceholderColorId = TD.getAvatarColorId(embeddedPost.author.hashCode(), 0); } } @@ -581,7 +580,7 @@ public void drawInternal (View view, Canvas c, Receiver preview, Receiver receiv int textLeft = getTextPaddingLeft(); int textTop = getContentTop(); - if (forceBackground && backgroundColorId != 0) { + if (forceBackground && backgroundColorId != ColorId.NONE) { c.drawRect(0, 0, viewWidth, getComputedHeight(), Paints.fillingPaint(Theme.getColor(backgroundColorId))); } @@ -592,7 +591,7 @@ public void drawInternal (View view, Canvas c, Receiver preview, Receiver receiv if (avatarPreview != null) { if (receiver.needPlaceholder()) { if (preview.needPlaceholder()) { - c.drawCircle(avatarLeft + avatarSize / 2, avatarTop + avatarSize / 2, avatarSize / 2, Paints.fillingPaint(Theme.placeholderColor())); + c.drawCircle(avatarLeft + avatarSize / 2f, avatarTop + avatarSize / 2f, avatarSize / 2f, Paints.fillingPaint(Theme.placeholderColor())); } preview.setBounds(avatarLeft, avatarTop, avatarLeft + avatarSize, avatarTop + avatarSize); preview.draw(c); @@ -600,7 +599,7 @@ public void drawInternal (View view, Canvas c, Receiver preview, Receiver receiv receiver.setBounds(avatarLeft, avatarTop, avatarLeft + avatarSize, avatarTop + avatarSize); receiver.draw(c); } else { - c.drawCircle(avatarLeft + avatarSize / 2, avatarTop + avatarSize / 2, avatarSize / 2, Paints.fillingPaint(Theme.getColor(avatarPlaceholderColorId))); + c.drawCircle(avatarLeft + avatarSize / 2f, avatarTop + avatarSize / 2f, avatarSize / 2f, Paints.fillingPaint(accentColor.getPrimaryColor())); // TODO letters } } @@ -664,7 +663,7 @@ public void drawInternal (View view, Canvas c, Receiver preview, Receiver receiv if (avatarFile != null && iconReceiver != null) { ImageReceiver r = iconReceiver.getImageReceiver(Integer.MAX_VALUE); r.setBounds(cx - avatarSize / 2, cy - avatarSize / 2, cx + avatarSize / 2, cy + avatarSize / 2); - r.setRadius(avatarSize / 2); + r.setRadius(avatarSize / 2f); r.setPaintAlpha(r.getPaintAlpha() * subtitleFactor); r.draw(c); r.restorePaintAlpha(); diff --git a/app/src/main/java/org/thunderdog/challegram/data/SponsoredMessageUtils.java b/app/src/main/java/org/thunderdog/challegram/data/SponsoredMessageUtils.java index 4aed9200e0..22b1aebf14 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/SponsoredMessageUtils.java +++ b/app/src/main/java/org/thunderdog/challegram/data/SponsoredMessageUtils.java @@ -21,22 +21,21 @@ public class SponsoredMessageUtils { public static final String TELEGRAM_AD_TYPE = "telegram_adx"; - public static TGMessage sponsoredToTgx (MessagesManager manager, long chatId, int date, TdApi.SponsoredMessage sMsg) { - return new TGMessageText(manager, sponsoredToTd(chatId, date, sMsg, manager.controller().tdlib()), sMsg); + public static TGMessage sponsoredToTgx (MessagesManager manager, long inChatId, TdApi.SponsoredMessage sMsg) { + return new TGMessageText(manager, inChatId, sMsg); } - private static TdApi.Message sponsoredToTd (long chatId, int date, TdApi.SponsoredMessage sMsg, Tdlib tdlib) { - TdApi.MessageText fMsgContent = (TdApi.MessageText) sMsg.content; + public static TdApi.Message sponsoredToTd (long chatId, TdApi.SponsoredMessage sponsoredMessage, Tdlib tdlib) { + TdApi.MessageText fMsgContent = (TdApi.MessageText) sponsoredMessage.content; fMsgContent.webPage = new TdApi.WebPage(); fMsgContent.webPage.type = TELEGRAM_AD_TYPE; fMsgContent.webPage.url = ""; TdApi.Message fMsg = new TdApi.Message(); - fMsg.senderId = tdlib.sender(sMsg.sponsorChatId); + fMsg.senderId = null; fMsg.content = fMsgContent; - fMsg.authorSignature = Lang.getString(R.string.SponsoredSign); - fMsg.id = sMsg.messageId; - fMsg.date = date; + fMsg.authorSignature = Lang.getString(sponsoredMessage.isRecommended ? R.string.RecommendedSign : R.string.SponsoredSign); + fMsg.id = sponsoredMessage.messageId; fMsg.isOutgoing = false; fMsg.canBeSaved = true; fMsg.chatId = chatId; @@ -44,16 +43,4 @@ private static TdApi.Message sponsoredToTd (long chatId, int date, TdApi.Sponsor return fMsg; } - - public static TdApi.SponsoredMessages generateSponsoredMessages (Tdlib tdlib) { - return generateUserSponsoredMessages(tdlib); - } - - private static TdApi.SponsoredMessages generateUserSponsoredMessages (Tdlib tdlib) { - TdApi.SponsoredMessage msg = new TdApi.SponsoredMessage(); - msg.sponsorChatId = tdlib.myUserId(); - msg.messageId = 1; - msg.content = new TdApi.MessageText(new TdApi.FormattedText("Test ad message (from user/channel)", null), null); - return new TdApi.SponsoredMessages(new TdApi.SponsoredMessage[] {msg}, 0); - } } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TD.java b/app/src/main/java/org/thunderdog/challegram/data/TD.java index 7558d28ccb..8067931a7d 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TD.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TD.java @@ -14,8 +14,6 @@ */ package org.thunderdog.challegram.data; -import static androidx.core.util.ObjectsCompat.requireNonNull; - import android.app.DownloadManager; import android.content.Context; import android.database.Cursor; @@ -33,8 +31,8 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.BackgroundColorSpan; -import android.text.style.CharacterStyle; import android.text.style.ClickableSpan; +import android.text.style.QuoteSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; @@ -45,6 +43,7 @@ import android.widget.Toast; import androidx.annotation.DrawableRes; +import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; @@ -71,7 +70,9 @@ import org.thunderdog.challegram.telegram.PrivacySettings; import org.thunderdog.challegram.telegram.RightId; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibDelegate; +import org.thunderdog.challegram.telegram.TdlibEntitySpan; import org.thunderdog.challegram.telegram.TdlibManager; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; @@ -83,7 +84,6 @@ import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.ui.HashtagController; import org.thunderdog.challegram.ui.ShareController; -import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.util.CustomTypefaceSpan; import org.thunderdog.challegram.util.Permissions; import org.thunderdog.challegram.util.text.Letters; @@ -94,26 +94,27 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import me.vkryl.android.html.HtmlEncoder; import me.vkryl.android.html.HtmlParser; import me.vkryl.android.html.HtmlTag; import me.vkryl.android.text.AcceptFilter; import me.vkryl.core.ArrayUtils; -import me.vkryl.core.CurrencyUtils; import me.vkryl.core.DateUtils; import me.vkryl.core.FileUtils; -import me.vkryl.core.MathUtils; +import me.vkryl.core.ObjectUtils; import me.vkryl.core.StringUtils; +import me.vkryl.core.collection.IntList; import me.vkryl.core.collection.LongList; +import me.vkryl.core.collection.LongSet; +import me.vkryl.core.lambda.Filter; import me.vkryl.core.lambda.Future; -import me.vkryl.core.lambda.RunnableBool; import me.vkryl.core.unit.ByteUnit; import me.vkryl.td.ChatId; import me.vkryl.td.MessageId; @@ -147,6 +148,10 @@ public static boolean isValidRight (@RightId int rightId) { case RightId.INVITE_USERS: case RightId.PIN_MESSAGES: case RightId.MANAGE_VIDEO_CHATS: + case RightId.MANAGE_TOPICS: + case RightId.POST_STORIES: + case RightId.EDIT_STORIES: + case RightId.DELETE_STORIES: case RightId.ADD_NEW_ADMINS: case RightId.REMAIN_ANONYMOUS: return true; @@ -206,74 +211,56 @@ public static boolean checkRight (TdApi.ChatPermissions permissions, @RightId in case RightId.DELETE_MESSAGES: case RightId.EDIT_MESSAGES: case RightId.MANAGE_VIDEO_CHATS: + case RightId.MANAGE_TOPICS: + case RightId.POST_STORIES: + case RightId.EDIT_STORIES: + case RightId.DELETE_STORIES: case RightId.REMAIN_ANONYMOUS: break; } throw new IllegalArgumentException(Lang.getResourceEntryName(rightId)); } - private static final int[] color_ids = { - ColorId.avatarRed /* red 0 */, - ColorId.avatarOrange /* orange 1 */, - ColorId.avatarYellow /* yellow 2 */, - ColorId.avatarGreen /* green 3 */, - ColorId.avatarCyan /* cyan 4 */, - ColorId.avatarBlue /* blue 5 */, - ColorId.avatarViolet /* violet 6 */, - ColorId.avatarPink /* pink 7 */ - }; - public static boolean isSameSource (TdApi.Message a, TdApi.Message b, boolean splitAuthors) { if (a == null || b == null) return false; if ((a.forwardInfo == null) == (b.forwardInfo != null)) return false; - if (a.forwardInfo == null){ + if (!Td.equalsTo(a.importInfo, b.importInfo, false)) { + return false; + } + if (a.forwardInfo == null) { return a.chatId == b.chatId; } if (a.forwardInfo.origin.getConstructor() != b.forwardInfo.origin.getConstructor() || a.forwardInfo.fromChatId != b.forwardInfo.fromChatId) return false; if (splitAuthors) { switch (a.forwardInfo.origin.getConstructor()) { - case TdApi.MessageForwardOriginUser.CONSTRUCTOR: - return ((TdApi.MessageForwardOriginUser) a.forwardInfo.origin).senderUserId == ((TdApi.MessageForwardOriginUser) b.forwardInfo.origin).senderUserId; - - case TdApi.MessageForwardOriginChannel.CONSTRUCTOR: - return ((TdApi.MessageForwardOriginChannel) a.forwardInfo.origin).chatId == ((TdApi.MessageForwardOriginChannel) b.forwardInfo.origin).chatId && - StringUtils.equalsOrBothEmpty(((TdApi.MessageForwardOriginChannel) a.forwardInfo.origin).authorSignature, ((TdApi.MessageForwardOriginChannel) b.forwardInfo.origin).authorSignature); - case TdApi.MessageForwardOriginChat.CONSTRUCTOR: - return ((TdApi.MessageForwardOriginChat) a.forwardInfo.origin).senderChatId == ((TdApi.MessageForwardOriginChat) b.forwardInfo.origin).senderChatId && - StringUtils.equalsOrBothEmpty(((TdApi.MessageForwardOriginChat) a.forwardInfo.origin).authorSignature, ((TdApi.MessageForwardOriginChat) b.forwardInfo.origin).authorSignature); - - case TdApi.MessageForwardOriginHiddenUser.CONSTRUCTOR: - return ((TdApi.MessageForwardOriginHiddenUser) a.forwardInfo.origin).senderName.equals(((TdApi.MessageForwardOriginHiddenUser) b.forwardInfo.origin).senderName); - case TdApi.MessageForwardOriginMessageImport.CONSTRUCTOR: - return StringUtils.equalsOrBothEmpty(((TdApi.MessageForwardOriginMessageImport) a.forwardInfo.origin).senderName, ((TdApi.MessageForwardOriginMessageImport) b.forwardInfo.origin).senderName); + case TdApi.MessageOriginUser.CONSTRUCTOR: + return ((TdApi.MessageOriginUser) a.forwardInfo.origin).senderUserId == ((TdApi.MessageOriginUser) b.forwardInfo.origin).senderUserId; + case TdApi.MessageOriginChannel.CONSTRUCTOR: + return ((TdApi.MessageOriginChannel) a.forwardInfo.origin).chatId == ((TdApi.MessageOriginChannel) b.forwardInfo.origin).chatId && + StringUtils.equalsOrBothEmpty(((TdApi.MessageOriginChannel) a.forwardInfo.origin).authorSignature, ((TdApi.MessageOriginChannel) b.forwardInfo.origin).authorSignature); + case TdApi.MessageOriginChat.CONSTRUCTOR: + return ((TdApi.MessageOriginChat) a.forwardInfo.origin).senderChatId == ((TdApi.MessageOriginChat) b.forwardInfo.origin).senderChatId && + StringUtils.equalsOrBothEmpty(((TdApi.MessageOriginChat) a.forwardInfo.origin).authorSignature, ((TdApi.MessageOriginChat) b.forwardInfo.origin).authorSignature); + case TdApi.MessageOriginHiddenUser.CONSTRUCTOR: + return ((TdApi.MessageOriginHiddenUser) a.forwardInfo.origin).senderName.equals(((TdApi.MessageOriginHiddenUser) b.forwardInfo.origin).senderName); + default: + Td.assertMessageOrigin_f2224a59(); + throw Td.unsupported(a.forwardInfo.origin); } } return false; } - public static boolean isSecret (TdApi.InputMessageContent content) { - int selfDestructTime = 0; - switch (content.getConstructor()) { - case TdApi.InputMessagePhoto.CONSTRUCTOR: - selfDestructTime = ((TdApi.InputMessagePhoto) content).selfDestructTime; - break; - case TdApi.InputMessageVideo.CONSTRUCTOR: - selfDestructTime = ((TdApi.InputMessageVideo) content).selfDestructTime; - break; - } - return selfDestructTime != 0 && selfDestructTime <= 60; - } - public static CharSequence formatString (@Nullable TdlibDelegate context, String text, TdApi.TextEntity[] entities, @Nullable Typeface defaultTypeface, @Nullable CustomTypefaceSpan.OnClickListener onClickListener) { if (entities == null || entities.length == 0) return text; SpannableStringBuilder b = null; for (TdApi.TextEntity entity : entities) { - CustomTypefaceSpan span; + Object span; switch (entity.type.getConstructor()) { case TdApi.TextEntityTypeBotCommand.CONSTRUCTOR: span = null; // nothing to do? @@ -375,16 +362,20 @@ public static CharSequence formatString (@Nullable TdlibDelegate context, String if (span != null) { if (b == null) b = new SpannableStringBuilder(text); - if (span.getOnClickListener() != null) { - final String entityText = Td.substring(text, entity); - b.setSpan(new ClickableSpan() { - @Override - public void onClick (@NonNull View widget) { - span.getOnClickListener().onClick(widget, span, entityText); - } - }, entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (span instanceof CustomTypefaceSpan) { + CustomTypefaceSpan customSpan = (CustomTypefaceSpan) span; + customSpan.setTextEntityType(entity.type); + customSpan.setRemoveUnderline(true); + if (customSpan.getOnClickListener() != null) { + final String entityText = Td.substring(text, entity); + b.setSpan(new ClickableSpan() { + @Override + public void onClick (@NonNull View widget) { + customSpan.getOnClickListener().onClick(widget, customSpan, entityText); + } + }, entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } } - span.setEntityType(entity.type).setRemoveUnderline(true); b.setSpan(span, entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } @@ -399,7 +390,7 @@ private static List findUrls (String text, @Nullable List urls) TdApi.TextEntity[] entities = Td.findEntities(text); if (entities != null) { for (TdApi.TextEntity entity : entities) { - if (entity.type.getConstructor() == TdApi.TextEntityTypeUrl.CONSTRUCTOR) { + if (Td.isUrl(entity.type)) { if (urls == null) urls = new ArrayList<>(); urls.add(text.substring(entity.offset, entity.offset + entity.length)); @@ -487,7 +478,7 @@ public static List findUrls (TdApi.FormattedText text) { } // add textUrl - if (existingEntity.type.getConstructor() == TdApi.TextEntityTypeTextUrl.CONSTRUCTOR) { + if (Td.isTextUrl(existingEntity.type)) { if (links == null) links = new ArrayList<>(); links.add(((TdApi.TextEntityTypeTextUrl) existingEntity.type).url); @@ -522,6 +513,9 @@ public static boolean isVisual (TdApi.TextEntityType type, boolean allowInternal case TdApi.TextEntityTypeEmailAddress.CONSTRUCTOR: case TdApi.TextEntityTypePhoneNumber.CONSTRUCTOR: case TdApi.TextEntityTypeBankCardNumber.CONSTRUCTOR: + case TdApi.TextEntityTypeMediaTimestamp.CONSTRUCTOR: + // Only because custom emoji aren't displayed in the notification + case TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR: return false; case TdApi.TextEntityTypeBold.CONSTRUCTOR: @@ -533,13 +527,16 @@ public static boolean isVisual (TdApi.TextEntityType type, boolean allowInternal case TdApi.TextEntityTypePre.CONSTRUCTOR: case TdApi.TextEntityTypePreCode.CONSTRUCTOR: case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: return true; case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: return allowInternal; - } - return false; + default: + Td.assertTextEntityType_91234a79(); + throw Td.unsupported(type); + } } public static GifFile toGifFile (Tdlib tdlib, TdApi.Thumbnail thumbnail) { @@ -656,171 +653,10 @@ public static TextEntity[] collectAllEntities (ViewController context, Tdlib return null; } - public static int getColorIdForString (String string) { - switch (Math.abs(string.hashCode()) % 3) { - case 0: return ColorId.fileYellow; - case 1: return ColorId.fileRed; - case 2: return ColorId.fileGreen; - } - return ColorId.file; - } - - public static int getColorIdForName (String name) { - return color_ids[MathUtils.pickNumber(color_ids.length, name)]; - } - - public static int getFileColorId (TdApi.Document doc, boolean isOutBubble) { - return getFileColorId(doc.fileName, doc.mimeType, isOutBubble); - } - - public static int getFileColorId (String fileName, @Nullable String mimeType, boolean isOutBubble) { - String mime = mimeType != null ? mimeType.toLowerCase() : null; - int i = fileName.lastIndexOf('.'); - String ext = i != -1 ? fileName.substring(i + 1).toLowerCase() : ""; - - // Android APKs - if ("application/vnd.android.package-archive".equals(mime) || "apk".equals(ext)) { - return ColorId.fileGreen; - } - - if ( - "7z".equals(ext) || "application/x-7z-compressed".equals(mime) || - "zip".equals(ext) || "application/zip".equals(mime) || - "rar".equals(ext) || "application/x-rar-compressed".equals(mime) - ) { - return ColorId.fileYellow; - } - - if ( - "pdf".equals(ext) || "application/pdf".equals(mime) - ) { - return ColorId.fileRed; - } - - return isOutBubble ? ColorId.bubbleOut_file : ColorId.file; - } - public static String getPhoneNumber (String in) { return StringUtils.isEmpty(in) || in.startsWith("+") ? in : "+" + in; } - public static void saveSender (Bundle bundle, String prefix, TdApi.MessageSender sender) { - if (sender == null) - return; - switch (sender.getConstructor()) { - case TdApi.MessageSenderChat.CONSTRUCTOR: - bundle.putLong(prefix + "chat_id", ((TdApi.MessageSenderChat) sender).chatId); - break; - case TdApi.MessageSenderUser.CONSTRUCTOR: - bundle.putLong(prefix + "user_id", ((TdApi.MessageSenderUser) sender).userId); - break; - default: - throw new RuntimeException(sender.toString()); - } - } - - public static TdApi.MessageSender restoreSender (Bundle bundle, String prefix) { - long chatId = bundle.getLong(prefix + "chat_id"); - if (chatId != 0) - return new TdApi.MessageSenderChat(chatId); - long userId = bundle.getLong(prefix + "user_id"); - if (userId != 0) - return new TdApi.MessageSenderUser(userId); - return null; - } - - public static void saveFilter (Bundle bundle, String prefix, TdApi.SearchMessagesFilter filter) { - if (filter == null) - return; - int type; - switch (filter.getConstructor()) { - case TdApi.SearchMessagesFilterAnimation.CONSTRUCTOR: - type = 1; - break; - case TdApi.SearchMessagesFilterAudio.CONSTRUCTOR: - type = 2; - break; - case TdApi.SearchMessagesFilterDocument.CONSTRUCTOR: - type = 3; - break; - case TdApi.SearchMessagesFilterPhoto.CONSTRUCTOR: - type = 4; - break; - case TdApi.SearchMessagesFilterVideo.CONSTRUCTOR: - type = 5; - break; - case TdApi.SearchMessagesFilterVoiceNote.CONSTRUCTOR: - type = 6; - break; - case TdApi.SearchMessagesFilterPhotoAndVideo.CONSTRUCTOR: - type = 7; - break; - case TdApi.SearchMessagesFilterUrl.CONSTRUCTOR: - type = 8; - break; - case TdApi.SearchMessagesFilterChatPhoto.CONSTRUCTOR: - type = 9; - break; - /*case TdApi.SearchMessagesFilterCall.CONSTRUCTOR: - type = 10; - break; - case TdApi.SearchMessagesFilterMissedCall.CONSTRUCTOR: - type = 11; - break;*/ - case TdApi.SearchMessagesFilterVideoNote.CONSTRUCTOR: - type = 12; - break; - case TdApi.SearchMessagesFilterVoiceAndVideoNote.CONSTRUCTOR: - type = 13; - break; - case TdApi.SearchMessagesFilterMention.CONSTRUCTOR: - type = 14; - break; - case TdApi.SearchMessagesFilterUnreadMention.CONSTRUCTOR: - type = 15; - break; - case TdApi.SearchMessagesFilterFailedToSend.CONSTRUCTOR: - type = 16; - break; - case TdApi.SearchMessagesFilterPinned.CONSTRUCTOR: - type = 17; - break; - case TdApi.SearchMessagesFilterEmpty.CONSTRUCTOR: - default: - type = 0; - break; - } - if (type != 0) { - bundle.putInt(prefix + "type", type); - } - } - - public static TdApi.SearchMessagesFilter restoreFilter (Bundle bundle, String prefix) { - int type = bundle.getInt(prefix + "type", 0); - if (type != 0) { - switch (type) { - case 1: return new TdApi.SearchMessagesFilterAnimation(); - case 2: return new TdApi.SearchMessagesFilterAudio(); - case 3: return new TdApi.SearchMessagesFilterDocument(); - case 4: return new TdApi.SearchMessagesFilterPhoto(); - case 5: return new TdApi.SearchMessagesFilterVideo(); - case 6: return new TdApi.SearchMessagesFilterVoiceNote(); - case 7: return new TdApi.SearchMessagesFilterPhotoAndVideo(); - case 8: return new TdApi.SearchMessagesFilterUrl(); - case 9: return new TdApi.SearchMessagesFilterChatPhoto(); - /*case 10: return new TdApi.SearchMessagesFilterCall(); - case 11: return new TdApi.SearchMessagesFilterMissedCall();*/ - case 12: return new TdApi.SearchMessagesFilterVideoNote(); - case 13: return new TdApi.SearchMessagesFilterVoiceAndVideoNote(); - case 14: return new TdApi.SearchMessagesFilterMention(); - case 15: return new TdApi.SearchMessagesFilterUnreadMention(); - case 16: return new TdApi.SearchMessagesFilterFailedToSend(); - case 17: return new TdApi.SearchMessagesFilterPinned(); - } - } - return null; - } - public static void saveMessageThreadInfo (Bundle bundle, String prefix, @Nullable TdApi.MessageThreadInfo threadInfo) { if (threadInfo == null) { return; @@ -828,8 +664,8 @@ public static void saveMessageThreadInfo (Bundle bundle, String prefix, @Nullabl bundle.putLong(prefix + "_chatId", threadInfo.chatId); bundle.putLong(prefix + "_messageThreadId", threadInfo.messageThreadId); bundle.putInt(prefix + "_unreadMessageCount", threadInfo.unreadMessageCount); - saveReplyInfo(bundle, prefix + "_replyInfo", threadInfo.replyInfo); - saveDraftMessage(bundle, prefix + "_draftMessage", threadInfo.draftMessage); + Td.put(bundle, prefix + "_replyInfo", threadInfo.replyInfo); + Td.put(bundle, prefix + "_draftMessage", threadInfo.draftMessage); bundle.putInt(prefix + "_messagesLength", threadInfo.messages.length); for (int index = 0; index < threadInfo.messages.length; index++) { bundle.putLong(prefix + "_messageId_" + index, threadInfo.messages[index].id); @@ -842,7 +678,7 @@ public static void saveMessageThreadInfo (Bundle bundle, String prefix, @Nullabl if (chatId == 0 || messageThreadId == 0) { return null; } - TdApi.MessageReplyInfo replyInfo = restoreMessageReplyInfo(bundle, prefix + "_replyInfo"); + TdApi.MessageReplyInfo replyInfo = Td.restoreMessageReplyInfo(bundle, prefix + "_replyInfo"); if (replyInfo == null) { return null; } @@ -858,234 +694,10 @@ public static void saveMessageThreadInfo (Bundle bundle, String prefix, @Nullabl return null; } } - TdApi.DraftMessage draftMessage = restoreDraftMessage(bundle, prefix + "_draftMessage"); + TdApi.DraftMessage draftMessage = Td.restoreDraftMessage(bundle, prefix + "_draftMessage"); return new TdApi.MessageThreadInfo(chatId, messageThreadId, replyInfo, unreadMessageCount, messages.toArray(new TdApi.Message[0]), draftMessage); } - public static void saveDraftMessage (Bundle bundle, String prefix, @Nullable TdApi.DraftMessage draftMessage) { - if (draftMessage == null) { - return; - } - if (!(draftMessage.inputMessageText instanceof TdApi.InputMessageText)) { - throw new UnsupportedOperationException(draftMessage.inputMessageText.toString()); - } - bundle.putLong(prefix + "_replyToMessageId", draftMessage.replyToMessageId); - bundle.putInt(prefix + "_date", draftMessage.date); - saveInputMessageText(bundle, prefix + "_inputMessageText", (TdApi.InputMessageText) draftMessage.inputMessageText); - } - - public static @Nullable TdApi.DraftMessage restoreDraftMessage (Bundle bundle, String prefix) { - long replyToMessageId = bundle.getLong(prefix + "_replyToMessageId"); - int date = bundle.getInt(prefix + "_date"); - TdApi.InputMessageText inputMessageText = restoreInputMessageText(bundle, prefix + "_inputMessageText"); - if (inputMessageText == null) - return null; - return new TdApi.DraftMessage(replyToMessageId, date, inputMessageText); - } - - public static void saveInputMessageText (Bundle bundle, String prefix, @Nullable TdApi.InputMessageText inputMessageText) { - if (inputMessageText == null) { - return; - } - saveFormattedText(bundle, prefix + "_text", inputMessageText.text); - bundle.putBoolean(prefix + "_disableWebPagePreview", inputMessageText.disableWebPagePreview); - bundle.putBoolean(prefix + "_clearDraft", inputMessageText.clearDraft); - } - - public static @Nullable TdApi.InputMessageText restoreInputMessageText (Bundle bundle, String prefix) { - TdApi.FormattedText text = restoreFormattedText(bundle, prefix + "_text"); - if (text == null) { - return null; - } - boolean disableWebPagePreview = bundle.getBoolean(prefix + "_disableWebPagePreview"); - boolean clearDraft = bundle.getBoolean(prefix + "_clearDraft"); - return new TdApi.InputMessageText(text, disableWebPagePreview, clearDraft); - } - - public static void saveFormattedText (Bundle bundle, String prefix, @Nullable TdApi.FormattedText formattedText) { - if (formattedText == null) { - return; - } - bundle.putString(prefix + "_text", formattedText.text); - if (formattedText.entities != null) { - bundle.putInt(prefix + "_entityCount", formattedText.entities.length); - for (int i = 0; i < formattedText.entities.length; i++) { - saveTextEntity(bundle, prefix + "_entity_" + i, formattedText.entities[i]); - } - } - } - - public static @Nullable TdApi.FormattedText restoreFormattedText (Bundle bundle, String prefix) { - String text = bundle.getString(prefix + "_text"); - if (text == null) { - return null; - } - int entityCount = bundle.getInt(prefix + "_entityCount"); - TdApi.TextEntity[] entities = new TdApi.TextEntity[entityCount]; - for (int i = 0; i < entityCount; i++) { - TdApi.TextEntity entity = restoreTextEntity(bundle, prefix + "_entity_" + i); - if (entity != null) { - entities[i] = entity; - } else { - return null; - } - } - return new TdApi.FormattedText(text, entities); - } - - public static void saveTextEntity (Bundle bundle, String prefix, @Nullable TdApi.TextEntity textEntity) { - if (textEntity == null) { - return; - } - bundle.putInt(prefix + "_offset", textEntity.offset); - bundle.putInt(prefix + "_length", textEntity.length); - saveTextEntityType(bundle, prefix + "_type", textEntity.type); - } - - public static @Nullable TdApi.TextEntity restoreTextEntity (Bundle bundle, String prefix) { - if (!bundle.containsKey(prefix + "_offset")) { - return null; - } - int offset = bundle.getInt(prefix + "_offset"); - int length = bundle.getInt(prefix + "_length"); - TdApi.TextEntityType type = restoreTextEntityType(bundle, prefix + "_type"); - if (type == null) { - return null; - } - return new TdApi.TextEntity(offset, length, type); - } - - public static void saveTextEntityType (Bundle bundle, String prefix, @Nullable TdApi.TextEntityType type) { - if (type == null) { - return; - } - bundle.putInt(prefix + "_constructor", type.getConstructor()); - switch (type.getConstructor()) { - case TdApi.TextEntityTypePreCode.CONSTRUCTOR: - bundle.putString(prefix + "_language", ((TdApi.TextEntityTypePreCode) type).language); - break; - case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: - bundle.putString(prefix + "_url", ((TdApi.TextEntityTypeTextUrl) type).url); - break; - case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: - bundle.putLong(prefix + "_userId", ((TdApi.TextEntityTypeMentionName) type).userId); - break; - case TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR: - bundle.putLong(prefix + "_customEmojiId", ((TdApi.TextEntityTypeCustomEmoji) type).customEmojiId); - break; - case TdApi.TextEntityTypeMediaTimestamp.CONSTRUCTOR: - bundle.putInt(prefix + "_mediaTimestamp", ((TdApi.TextEntityTypeMediaTimestamp) type).mediaTimestamp); - break; - case TdApi.TextEntityTypeBankCardNumber.CONSTRUCTOR: - case TdApi.TextEntityTypeBold.CONSTRUCTOR: - case TdApi.TextEntityTypeBotCommand.CONSTRUCTOR: - case TdApi.TextEntityTypeCashtag.CONSTRUCTOR: - case TdApi.TextEntityTypeCode.CONSTRUCTOR: - case TdApi.TextEntityTypeEmailAddress.CONSTRUCTOR: - case TdApi.TextEntityTypeHashtag.CONSTRUCTOR: - case TdApi.TextEntityTypeItalic.CONSTRUCTOR: - case TdApi.TextEntityTypeMention.CONSTRUCTOR: - case TdApi.TextEntityTypePhoneNumber.CONSTRUCTOR: - case TdApi.TextEntityTypePre.CONSTRUCTOR: - case TdApi.TextEntityTypeSpoiler.CONSTRUCTOR: - case TdApi.TextEntityTypeStrikethrough.CONSTRUCTOR: - case TdApi.TextEntityTypeUnderline.CONSTRUCTOR: - case TdApi.TextEntityTypeUrl.CONSTRUCTOR: - break; - default: - throw new UnsupportedOperationException(type.toString()); - } - } - - public static @Nullable TdApi.TextEntityType restoreTextEntityType (Bundle bundle, String prefix) { - if (!bundle.containsKey(prefix + "_constructor")) { - return null; - } - @TdApi.TextEntityType.Constructors int constructor = bundle.getInt(prefix + "_constructor"); - switch (constructor) { - case TdApi.TextEntityTypeBankCardNumber.CONSTRUCTOR: - return new TdApi.TextEntityTypeBankCardNumber(); - case TdApi.TextEntityTypeBold.CONSTRUCTOR: - return new TdApi.TextEntityTypeBold(); - case TdApi.TextEntityTypeBotCommand.CONSTRUCTOR: - return new TdApi.TextEntityTypeBotCommand(); - case TdApi.TextEntityTypeCashtag.CONSTRUCTOR: - return new TdApi.TextEntityTypeCashtag(); - case TdApi.TextEntityTypeCode.CONSTRUCTOR: - return new TdApi.TextEntityTypeCode(); - case TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR: - long customEmoji = requireNonNull(bundle.getLong(prefix + "_customEmojiId")); - return new TdApi.TextEntityTypeCustomEmoji(customEmoji); - case TdApi.TextEntityTypeEmailAddress.CONSTRUCTOR: - return new TdApi.TextEntityTypeEmailAddress(); - case TdApi.TextEntityTypeHashtag.CONSTRUCTOR: - return new TdApi.TextEntityTypeHashtag(); - case TdApi.TextEntityTypeItalic.CONSTRUCTOR: - return new TdApi.TextEntityTypeItalic(); - case TdApi.TextEntityTypeMediaTimestamp.CONSTRUCTOR: - int mediaTimestamp = bundle.getInt(prefix + "_mediaTimestamp"); - return new TdApi.TextEntityTypeMediaTimestamp(mediaTimestamp); - case TdApi.TextEntityTypeMention.CONSTRUCTOR: - return new TdApi.TextEntityTypeMention(); - case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: - long userId = bundle.getLong(prefix + "_userId"); - return new TdApi.TextEntityTypeMentionName(userId); - case TdApi.TextEntityTypePhoneNumber.CONSTRUCTOR: - return new TdApi.TextEntityTypePhoneNumber(); - case TdApi.TextEntityTypePre.CONSTRUCTOR: - return new TdApi.TextEntityTypePre(); - case TdApi.TextEntityTypePreCode.CONSTRUCTOR: - String language = requireNonNull(bundle.getString(prefix + "_language")); - return new TdApi.TextEntityTypePreCode(language); - case TdApi.TextEntityTypeSpoiler.CONSTRUCTOR: - return new TdApi.TextEntityTypeSpoiler(); - case TdApi.TextEntityTypeStrikethrough.CONSTRUCTOR: - return new TdApi.TextEntityTypeStrikethrough(); - case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: - String url = requireNonNull(bundle.getString(prefix + "_url")); - return new TdApi.TextEntityTypeTextUrl(url); - case TdApi.TextEntityTypeUnderline.CONSTRUCTOR: - return new TdApi.TextEntityTypeUnderline(); - case TdApi.TextEntityTypeUrl.CONSTRUCTOR: - return new TdApi.TextEntityTypeUrl(); - default: - throw new UnsupportedOperationException("constructor=" + constructor); - } - } - - public static void saveReplyInfo (Bundle bundle, String prefix, @Nullable TdApi.MessageReplyInfo replyInfo) { - if (replyInfo == null) { - return; - } - bundle.putInt(prefix + "_replyCount", replyInfo.replyCount); - bundle.putLong(prefix + "_lastMessageId", replyInfo.lastMessageId); - bundle.putLong(prefix + "_lastReadInboxMessageId", replyInfo.lastReadInboxMessageId); - bundle.putLong(prefix + "_lastReadOutboxMessageId", replyInfo.lastReadOutboxMessageId); - if (replyInfo.recentReplierIds != null) { - bundle.putInt(prefix + "_recentReplierIdsLength", replyInfo.recentReplierIds.length); - for (int index = 0; index < replyInfo.recentReplierIds.length; index++) { - TdApi.MessageSender recentReplierId = replyInfo.recentReplierIds[index]; - saveSender(bundle, prefix + "_recentReplierId_" + index, recentReplierId); - } - } - } - - public static @Nullable TdApi.MessageReplyInfo restoreMessageReplyInfo (Bundle bundle, String prefix) { - if (!bundle.containsKey(prefix + "_replyCount")) { - return null; - } - int replyCount = bundle.getInt(prefix + "_replyCount"); - long lastMessageId = bundle.getLong(prefix + "_lastMessageId"); - long lastReadInboxMessageId = bundle.getLong(prefix + "_lastReadInboxMessageId"); - long lastReadOutboxMessageId = bundle.getLong(prefix + "_lastReadOutboxMessageId"); - int recentReplierIdsLength = bundle.getInt(prefix + "_recentReplierIdsLength"); - TdApi.MessageSender[] recentReplierIds = new TdApi.MessageSender[recentReplierIdsLength]; - for (int index = 0; index < recentReplierIdsLength; index++) { - recentReplierIds[index] = restoreSender(bundle, prefix + "_recentReplierId_" + index); - } - return new TdApi.MessageReplyInfo(replyCount, recentReplierIds, lastReadInboxMessageId, lastReadOutboxMessageId, lastMessageId); - } - public static final String KEY_PREFIX_FOLDER = "filter"; public static TdApi.ChatList chatListFromKey (String key) { if (StringUtils.isEmpty(key)) @@ -1123,8 +735,10 @@ public static String makeChatListKey (TdApi.ChatList chatList) { return "archive"; case TdApi.ChatListFolder.CONSTRUCTOR: return KEY_PREFIX_FOLDER + ((TdApi.ChatListFolder) chatList).chatFolderId; + default: + Td.assertChatList_db6c93ab(); + throw Td.unsupported(chatList); } - throw new UnsupportedOperationException(chatList.toString()); } public static TdApi.ReactionType toReactionType (String key) { @@ -1140,36 +754,10 @@ public static String makeReactionKey (TdApi.ReactionType reactionType) { return ((TdApi.ReactionTypeEmoji) reactionType).emoji; case TdApi.ReactionTypeCustomEmoji.CONSTRUCTOR: return "custom_" + ((TdApi.ReactionTypeCustomEmoji) reactionType).customEmojiId; + default: + Td.assertReactionType_7dcca074(); + throw Td.unsupported(reactionType); } - throw new UnsupportedOperationException(reactionType.toString()); - } - - public static int getColorIndex (long selfUserId, long id) { - if (id >= 0 && id < color_ids.length) { - return (int) id; - } - try { - String str; - if (id >= 0 && selfUserId != 0) { - str = String.format(Locale.US, "%d%d", id, selfUserId); - } else { - str = String.format(Locale.US, "%d", id); - } - if (str.length() > 15) { - str = str.substring(0, 15); - } - java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); - byte[] digest = md.digest(str.getBytes(StringUtils.UTF_8)); - int b = digest[(int) Math.abs(id % 16)]; - if (b < 0) { - b += 256; - } - return Math.abs(b) % color_ids.length; - } catch (Throwable t) { - Log.e("Cannot calculate user color", t); - } - - return (int) Math.abs(id % color_ids.length); } public static ImageFile getAvatar (Tdlib tdlib, TdApi.User user) { @@ -1315,16 +903,11 @@ public static TdApi.PhotoSize toThumbnailSize (TdApi.Thumbnail thumbnail) { } } - public static boolean needRepainting (TdApi.Sticker sticker) { + public static boolean needThemedColorFilter (TdApi.Sticker sticker) { return (sticker != null && sticker.fullType instanceof TdApi.StickerFullTypeCustomEmoji && ((TdApi.StickerFullTypeCustomEmoji) sticker.fullType).needsRepainting); } - public static long getStickerCustomEmojiId (TdApi.Sticker sticker) { - return (sticker != null && sticker.fullType instanceof TdApi.StickerFullTypeCustomEmoji) ? - ((TdApi.StickerFullTypeCustomEmoji) sticker.fullType).customEmojiId: 0; - } - public static class Size { public final int width, height; @@ -1411,36 +994,6 @@ public static TdApi.Photo convertToPhoto (TdApi.Sticker sticker) { return new TdApi.Photo(false, null, sizes); } - public static int getAvatarColorId (TdApi.User user, long selfUserId) { - return getAvatarColorId(isUserDeleted(user) ? -1 : user == null ? 0 : user.id, selfUserId); - } - - public static int getNameColorId (int avatarColorId) { - switch (avatarColorId) { - case ColorId.avatarRed: - return ColorId.nameRed; - case ColorId.avatarOrange: - return ColorId.nameOrange; - case ColorId.avatarYellow: - return ColorId.nameYellow; - case ColorId.avatarGreen: - return ColorId.nameGreen; - case ColorId.avatarCyan: - return ColorId.nameCyan; - case ColorId.avatarBlue: - return ColorId.nameBlue; - case ColorId.avatarViolet: - return ColorId.nameViolet; - case ColorId.avatarPink: - return ColorId.namePink; - case ColorId.avatarSavedMessages: - return ColorId.messageAuthor; - case ColorId.avatarInactive: - return ColorId.nameInactive; - } - return ColorId.messageAuthor; - } - public static boolean isMultiChat (TdApi.Chat chat) { if (chat != null) { switch (chat.type.getConstructor()) { @@ -1453,18 +1006,6 @@ public static boolean isMultiChat (TdApi.Chat chat) { return false; } - /*public static int getAuthorColorId () { - return getAuthorColorId(true, 0); - } - - public static int getAuthorColorId (int selfUserId, int id) { - return selfUserId != 0 && selfUserId == id ? ColorId.chatAuthor : id == -1 ? ColorId.chatAuthorDead : color_ids[getColorIndex(selfUserId, id)]; - }*/ - - public static int getAvatarColorId (long id, long selfUserId) { - return id == -1 ? ColorId.avatarInactive : color_ids[getColorIndex(selfUserId, id)]; - } - public static boolean hasEncryptionKey (TdApi.SecretChat secretChat) { return secretChat != null && secretChat.state.getConstructor() == TdApi.SecretChatStateReady.CONSTRUCTOR && secretChat.keyHash != null && secretChat.keyHash.length > 0; } @@ -1499,16 +1040,24 @@ public static boolean isMutedForever (int muteForSeconds) { return TimeUnit.SECONDS.toDays(muteForSeconds) / 365 > 0; } - public static boolean isSecret (TdApi.Message message) { - return Td.isSecret(message.content); - } - public static boolean canSendToSecretChat (TdApi.MessageContent content) { + //noinspection SwitchIntDef switch (content.getConstructor()) { case TdApi.MessagePoll.CONSTRUCTOR: - case TdApi.MessageGame.CONSTRUCTOR: { + case TdApi.MessageGame.CONSTRUCTOR: + case TdApi.MessageStory.CONSTRUCTOR: + case TdApi.MessageInvoice.CONSTRUCTOR: + case TdApi.MessageDice.CONSTRUCTOR: + case TdApi.MessagePremiumGiveaway.CONSTRUCTOR: + case TdApi.MessagePremiumGiftCode.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayCreated.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayCompleted.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayWinners.CONSTRUCTOR: { return false; } + default: { + Td.assertMessageContent_d40af239(); + } } return true; } @@ -1575,7 +1124,7 @@ public static TdApi.InputMessageContent toInputMessageContent (String filePath, if (allowAnimation && durationSeconds < 30 && info.knownSize < ByteUnit.MB.toBytes(10) && !metadata.hasAudio) { return new TdApi.InputMessageAnimation(inputFile, null, null, durationSeconds, videoWidth, videoHeight, caption, hasSpoiler); } else if (allowVideo && durationSeconds > 0) { - return new TdApi.InputMessageVideo(inputFile, null, null, durationSeconds, videoWidth, videoHeight, U.canStreamVideo(inputFile), caption, 0, hasSpoiler); + return new TdApi.InputMessageVideo(inputFile, null, null, durationSeconds, videoWidth, videoHeight, U.canStreamVideo(inputFile), caption, null, hasSpoiler); } } } @@ -1616,16 +1165,19 @@ public static boolean hasRestrictions (TdApi.ChatPermissions a, TdApi.ChatPermis } public static int getCombineMode (TdApi.Message message) { - if (message != null && !isSecret(message)) { - switch (message.content.getConstructor()) { - case TdApi.MessagePhoto.CONSTRUCTOR: - case TdApi.MessageVideo.CONSTRUCTOR: - case TdApi.MessageAnimation.CONSTRUCTOR: - return COMBINE_MODE_MEDIA; - case TdApi.MessageDocument.CONSTRUCTOR: - return COMBINE_MODE_FILES; - case TdApi.MessageAudio.CONSTRUCTOR: - return COMBINE_MODE_AUDIO; + if (message != null) { + if (!Td.isSecret(message.content)) { + //noinspection SwitchIntDef + switch (message.content.getConstructor()) { + case TdApi.MessagePhoto.CONSTRUCTOR: + case TdApi.MessageVideo.CONSTRUCTOR: + case TdApi.MessageAnimation.CONSTRUCTOR: + return COMBINE_MODE_MEDIA; + case TdApi.MessageDocument.CONSTRUCTOR: + return COMBINE_MODE_FILES; + case TdApi.MessageAudio.CONSTRUCTOR: + return COMBINE_MODE_AUDIO; + } } } return COMBINE_MODE_NONE; @@ -1635,9 +1187,9 @@ public static int getCombineMode (TdApi.InputMessageContent content) { if (content != null) { switch (content.getConstructor()) { case TdApi.InputMessagePhoto.CONSTRUCTOR: - return ((TdApi.InputMessagePhoto) content).selfDestructTime == 0 ? COMBINE_MODE_MEDIA : COMBINE_MODE_NONE; + return ((TdApi.InputMessagePhoto) content).selfDestructType == null ? COMBINE_MODE_MEDIA : COMBINE_MODE_NONE; case TdApi.InputMessageVideo.CONSTRUCTOR: - return ((TdApi.InputMessageVideo) content).selfDestructTime == 0 ? COMBINE_MODE_MEDIA : COMBINE_MODE_NONE; + return ((TdApi.InputMessageVideo) content).selfDestructType == null ? COMBINE_MODE_MEDIA : COMBINE_MODE_NONE; case TdApi.InputMessageAnimation.CONSTRUCTOR: return COMBINE_MODE_MEDIA; case TdApi.InputMessageDocument.CONSTRUCTOR: @@ -1787,6 +1339,14 @@ public static String getPrivacyRulesString (Tdlib tdlib, @TdApi.UserPrivacySetti everybodyExceptRes = R.string.PrivacyAllowFindingEverybodyExcept; everybodyRes = R.string.PrivacyAllowFindingEverybody; break; + case TdApi.UserPrivacySettingShowBio.CONSTRUCTOR: + nobodyExceptRes = R.string.PrivacyShowBioNobodyExcept; + nobodyRes = R.string.PrivacyShowBioNobody; + contactsExceptRes = R.string.PrivacyShowBioContactsExcept; + contactsRes = R.string.PrivacyShowBioContacts; + everybodyExceptRes = R.string.PrivacyShowBioEverybodyExcept; + everybodyRes = R.string.PrivacyShowBioEverybody; + break; case TdApi.UserPrivacySettingAllowChatInvites.CONSTRUCTOR: nobodyExceptRes = R.string.PrivacyAddToGroupsNobodyExcept; nobodyRes = R.string.PrivacyAddToGroupsNobody; @@ -1849,7 +1409,8 @@ public static String getPrivacyRulesString (Tdlib tdlib, @TdApi.UserPrivacySetti everybodyRes = R.string.PrivacyVoiceVideoEverybody; break; default: - throw new IllegalArgumentException("privacyKey == " + privacyKey); + Td.assertUserPrivacySetting_21d3f4(); + throw new UnsupportedOperationException(Integer.toString(privacyKey)); } int res, exceptRes; @@ -1880,6 +1441,11 @@ public static String getPrivacyRulesString (Tdlib tdlib, @TdApi.UserPrivacySetti } } + public static TdApi.Function[] toArray (Collection> collection) { + //noinspection unchecked + return (TdApi.Function[]) collection.toArray(new TdApi.Function[0]); + } + public static boolean isPrivateChat (TdApi.ChatType info) { return info.getConstructor() == TdApi.ChatTypePrivate.CONSTRUCTOR; } @@ -1914,7 +1480,7 @@ public static boolean forwardMessages (long toChatId, long toMessageThreadId, Td for (TdApi.Message message : messages) { if (message.chatId != fromChatId) { if (size > 0) { - out.add(new TdApi.ForwardMessages(toChatId, toMessageThreadId, fromChatId, getMessageIds(messages, index, size), options, sendCopy, removeCaption, false)); + out.add(new TdApi.ForwardMessages(toChatId, toMessageThreadId, fromChatId, getMessageIds(messages, index, size), options, sendCopy, removeCaption)); } fromChatId = message.chatId; index += size; @@ -1923,7 +1489,7 @@ public static boolean forwardMessages (long toChatId, long toMessageThreadId, Td size++; } if (size > 0) { - out.add(new TdApi.ForwardMessages(toChatId, toMessageThreadId, fromChatId, getMessageIds(messages, index, size), options, sendCopy, removeCaption, false)); + out.add(new TdApi.ForwardMessages(toChatId, toMessageThreadId, fromChatId, getMessageIds(messages, index, size), options, sendCopy, removeCaption)); } return true; } @@ -2271,8 +1837,11 @@ public static boolean isFileLoaded (TdApi.DiceStickers stickers) { isFileLoaded(slotMachine.rightReel.sticker) && isFileLoaded(slotMachine.lever.sticker); } + default: { + Td.assertDiceStickers_bd2aa513(); + throw Td.unsupported(stickers); + } } - throw new UnsupportedOperationException(stickers.toString()); } return false; } @@ -2387,15 +1956,19 @@ public static TdApi.User newFakeUser (long userId, String firstName, String last "", new TdApi.UserStatusEmpty(), null, + TdlibAccentColor.defaultAccentColorIdForUserId(userId), 0, + 0, 0, null, false, false, false, false, false, + false, null, false, false, + false, false, true, new TdApi.UserTypeRegular(), null, @@ -2543,6 +2116,7 @@ public static String findLink (TdApi.FormattedText text) { } String url = null; for (TdApi.TextEntity entity : text.entities) { + //noinspection SwitchIntDef switch (entity.type.getConstructor()) { case TdApi.TextEntityTypeUrl.CONSTRUCTOR: { return text.text.substring(entity.offset, entity.offset + entity.length); @@ -2596,6 +2170,9 @@ public static String toErrorString (@Nullable TdApi.Object object) { return "Unknown error (null)"; if (object.getConstructor() == TdApi.Error.CONSTRUCTOR) { TdApi.Error error = (TdApi.Error) object; + if (StringUtils.isEmpty(error.message)) { + return "Empty error " + error.code; + } return translateError(error.code, error.message); } return "not an error"; @@ -2657,6 +2234,7 @@ public static String toErrorString (@Nullable TdApi.Object object) { case "Invalid chat identifier specified": res = R.string.error_ChatInfoNotFound; break; case "Message must be non-empty": res = R.string.MessageInputEmpty; break; case "Not Found": res = R.string.error_NotFound; break; + case "Can't access the chat": res = R.string.errorChatInaccessible; break; case "The maximum number of pinned chats exceeded": return Lang.plural(R.string.ErrorPinnedChatsLimit, TdlibManager.instance().current().pinnedChatsMaxCount()); default: { String lookup = StringUtils.toCamelCase(message); @@ -3081,10 +2659,6 @@ public static int canRestrictMember (TdApi.ChatMemberStatus me, TdApi.ChatMember return RESTRICT_MODE_NONE; } - public static String getTelegramMeHost () { - return TdConstants.TME_HOSTS[0]; - } - public static byte[] newRandomWaveform () { return new byte[] {0, 4, 17, -50, -93, 86, -103, -45, -12, -26, 63, -25, -3, 109, -114, -54, -4, -1, -1, -1, -1, -29, -1, -1, -25, -1, -1, -97, -43, 57, -57, -108, 1, -91, -4, -47, 21, 99, 10, 97, 43, @@ -3108,12 +2682,12 @@ public static void processAlbum (Tdlib tdlib, long chatId, TdApi.MessageSendOpti } else { TdApi.InputMessageContent[] array = new TdApi.InputMessageContent[album.size()]; album.toArray(array); - functions.add(new TdApi.SendMessageAlbum(chatId, 0, 0, options, array, false)); + functions.add(new TdApi.SendMessageAlbum(chatId, 0, null, options, array)); } album.clear(); } - public static List> toFunctions (long chatId, long messageThreadId, long replyToMessageId, TdApi.MessageSendOptions options, TdApi.InputMessageContent[] content, boolean needGroupMedia) { + public static List> toFunctions (long chatId, long messageThreadId, @Nullable TdApi.InputMessageReplyTo replyTo, TdApi.MessageSendOptions options, TdApi.InputMessageContent[] content, boolean needGroupMedia) { if (content.length == 0) return Collections.emptyList(); @@ -3148,14 +2722,14 @@ public static List> toFunctions (long chatId, long messageThre } if (sliceSize == 1) { - functions.add(new TdApi.SendMessage(chatId, messageThreadId, functions.isEmpty() ? replyToMessageId : 0, options, null, slice[0])); + functions.add(new TdApi.SendMessage(chatId, messageThreadId, functions.isEmpty() ? replyTo : null, options, null, slice[0])); } else { for (TdApi.InputMessageContent inputContent : slice) { if (inputContent.getConstructor() == TdApi.InputMessageDocument.CONSTRUCTOR) { ((TdApi.InputMessageDocument) inputContent).disableContentTypeDetection = true; } } - functions.add(new TdApi.SendMessageAlbum(chatId, messageThreadId, functions.isEmpty() ? replyToMessageId : 0, options, slice, false)); + functions.add(new TdApi.SendMessageAlbum(chatId, messageThreadId, functions.isEmpty() ? replyTo : null, options, slice)); } remaining -= sliceSize; @@ -3166,17 +2740,13 @@ public static List> toFunctions (long chatId, long messageThre } public static void processSingle (Tdlib tdlib, long chatId, TdApi.MessageSendOptions options, List> functions, TdApi.InputMessageContent content) { - functions.add(new TdApi.SendMessage(chatId, 0, 0, options, null, content)); + functions.add(new TdApi.SendMessage(chatId, 0, null, options, null, content)); } public static boolean withinDistance (TdApi.File file, long offset) { return offset >= file.local.downloadOffset && offset <= file.local.downloadOffset + file.local.downloadedPrefixSize + ByteUnit.KIB.toBytes(512); } - public static boolean canVote (TdApi.Poll poll) { - return !needShowResults(poll); - } - public static boolean isMultiChoice (TdApi.Poll poll) { return poll.type.getConstructor() == TdApi.PollTypeRegular.CONSTRUCTOR && ((TdApi.PollTypeRegular) poll.type).allowMultipleAnswers; } @@ -3201,16 +2771,6 @@ public static boolean hasSelectedOption (TdApi.Poll poll) { return false; } - public static boolean needShowResults (TdApi.Poll poll) { - if (poll.isClosed) - return true; - for (TdApi.PollOption option : poll.options) { - if (option.isChosen) - return true; - } - return false; - } - public static int getMaxVoterCount (TdApi.Poll poll) { if (poll.totalVoterCount == 0) return 0; @@ -3241,48 +2801,6 @@ public static boolean matchHashtag (char c) { return type != Character.SPACE_SEPARATOR && c != '#'; } - /*public static boolean hasWritePermission (TdApi.Chat chat) { - if (chat == null) { - return false; - } - switch (chat.type.getConstructor()) { - case TdApi.ChatTypeSupergroup.CONSTRUCTOR: { - TdApi.Supergroup channel = TdlibCache.instance().getSupergroup(TD.getChatSupergroupId(chat)); - return hasWritePermission(channel); - } - case TdApi.ChatTypeBasicGroup.CONSTRUCTOR: { - TdApi.BasicGroup group = TdlibCache.instance().getGroup(TD.getChatBasicGroupId(chat)); - return hasWritePermission(group); - } - case TdApi.ChatTypePrivate.CONSTRUCTOR: { - TdApi.User user = TD.getUser(chat); - return user != null && user.type.getConstructor() != TdApi.UserTypeDeleted.CONSTRUCTOR && user.type.getConstructor() != TdApi.UserTypeUnknown.CONSTRUCTOR; - } - case TdApi.ChatTypeSecret.CONSTRUCTOR: { - TdApi.SecretChat secretChat = TD.getSecretChat(chat); - return secretChat != null && secretChat.state.getConstructor() == TdApi.SecretChatStateReady.CONSTRUCTOR; - } - } - return false; - }*/ - - public static boolean hasWritePermission (TdApi.Supergroup supergroup) { - if (supergroup != null) { - if (supergroup.isChannel) { - switch (supergroup.status.getConstructor()) { - case TdApi.ChatMemberStatusCreator.CONSTRUCTOR: - return true; - case TdApi.ChatMemberStatusAdministrator.CONSTRUCTOR: - return ((TdApi.ChatMemberStatusAdministrator) supergroup.status).rights.canPostMessages; - } - return false; - } else { - return !isNotInChat(supergroup.status); - } - } - return false; - } - public static boolean isLocalLanguagePackId (String languagePackId) { return languagePackId.startsWith("X"); } @@ -3509,7 +3027,7 @@ public static boolean isMember (TdApi.ChatMemberStatus status) { public static boolean hasHashtag (TdApi.FormattedText text, String hashtag) { if (!Td.isEmpty(text) && text.entities != null) { for (TdApi.TextEntity entity : text.entities) { - if (entity.type.getConstructor() == TdApi.TextEntityTypeHashtag.CONSTRUCTOR) { + if (Td.isHashtag(entity.type)) { if (hashtag.equals(Td.substring(text.text, entity))) return true; } @@ -3526,7 +3044,7 @@ public static boolean matchesFilter (TdApi.SupergroupMembersFilter filter, TdApi switch (status.getConstructor()) { case TdApi.ChatMemberStatusAdministrator.CONSTRUCTOR: case TdApi.ChatMemberStatusCreator.CONSTRUCTOR: - return true; + return true; } return false; case TdApi.SupergroupMembersFilterRestricted.CONSTRUCTOR: @@ -3537,30 +3055,6 @@ public static boolean matchesFilter (TdApi.SupergroupMembersFilter filter, TdApi return false; } - public static String getLink (TdApi.Supergroup supergroup) { - return "https://" + getTelegramMeHost() + "/" + Td.primaryUsername(supergroup); - } - - public static String getStickerPackLink (String name) { - return "https://" + getTelegramMeHost() + "/addstickers/" + name; - } - - public static String getEmojiPackLink (String name) { - return "https://" + getTelegramMeHost() + "/addemoji/" + name; - } - - public static String getLink (TdApi.User user) { - return "https://" + getTelegramMeHost() + "/" + Td.primaryUsername(user); - } - - public static String getLink (String username) { - return "https://" + getTelegramMeHost() + "/" + username; - } - - public static String getLink (TdApi.LanguagePackInfo languagePackInfo) { - return "https://" + getTelegramMeHost() + "/setlanguage/" + languagePackInfo.id; - } - public static String getRoleName (@Nullable TdApi.User user, int role) { switch (role) { case TdApi.ChatMemberStatusCreator.CONSTRUCTOR: return Lang.getString(R.string.ChannelOwner); @@ -3611,6 +3105,10 @@ public static boolean isChannel (TdApi.ChatType type) { return type != null && type.getConstructor() == TdApi.ChatTypeSupergroup.CONSTRUCTOR && ((TdApi.ChatTypeSupergroup) type).isChannel; } + public static boolean isChannel (TdApi.InviteLinkChatType type) { + return type != null && type.getConstructor() == TdApi.InviteLinkChatTypeChannel.CONSTRUCTOR; + } + public static boolean isSupergroup (TdApi.ChatType type) { return type != null && type.getConstructor() == TdApi.ChatTypeSupergroup.CONSTRUCTOR && !((TdApi.ChatTypeSupergroup) type).isChannel; } @@ -3689,510 +3187,6 @@ public static TdApi.InputFile createInputFile (long chatId, TdApi.File file) { return ChatId.isSecret(chatId) ? TD.createFileCopy(file) : new TdApi.InputFileId(file.id); } - public static final int PREVIEW_FLAG_ALLOW_CAPTIONS = 1; - public static final int PREVIEW_FLAG_FORCE_MEDIA_TYPE = 1 << 1; - - @Deprecated - private static String buildShortPreview (Tdlib tdlib, @Nullable TdApi.Message m, boolean allowCaptions, boolean multiLine, @Nullable RunnableBool translatable) { - String str = buildShortPreviewImpl(tdlib, m, allowCaptions ? PREVIEW_FLAG_ALLOW_CAPTIONS : 0, translatable); - return StringUtils.isEmpty(str) || multiLine ? str : Strings.translateNewLinesToSpaces(str); - } - - @Deprecated - public static String buildShortPreview (Tdlib tdlib, @Nullable TdApi.Message m, boolean allowCaptions) { - return buildShortPreview(tdlib, m, allowCaptions, false, null); - } - - private static int getDartRes (int value) { - switch (value) { - case 0: - return R.string.ChatContentDart; - case 1: - return R.string.ChatContentDart1; - case 2: - return R.string.ChatContentDart2; - case 3: - return R.string.ChatContentDart3; - case 4: - return R.string.ChatContentDart4; - case 6: - return R.string.ChatContentDart6; - case 5: - default: - return R.string.ChatContentDart5; - } - } - - /** - * TODO Support properly all missing cases in {@link #getContentPreview(Tdlib, long, TdApi.Message, boolean, boolean)} and remove this method. - */ - @Deprecated - private static String buildShortPreviewImpl (Tdlib tdlib, @Nullable TdApi.Message m, int flags, @Nullable RunnableBool isTranslatable) { - if (m == null || m.content == null) { - U.set(isTranslatable, true); - return Lang.getString(R.string.DeletedMessage); - } - if (!StringUtils.isEmpty(m.restrictionReason) && Settings.instance().needRestrictContent()) { - return m.restrictionReason; - } - boolean allowCaptions = (flags & PREVIEW_FLAG_ALLOW_CAPTIONS) != 0; - boolean forceMediaType = (flags & PREVIEW_FLAG_FORCE_MEDIA_TYPE) != 0; - switch (m.content.getConstructor()) { - // Common - case TdApi.MessageText.CONSTRUCTOR: { - U.set(isTranslatable, false); - return ((TdApi.MessageText) m.content).text.text; - } - case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: { - U.set(isTranslatable, false); - return ((TdApi.MessageAnimatedEmoji) m.content).emoji; - } - case TdApi.MessageAnimation.CONSTRUCTOR: { - String caption = ((TdApi.MessageAnimation) m.content).caption.text; - if (!allowCaptions || StringUtils.isEmpty(caption)) { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentAnimation); - } else if (forceMediaType) { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentWithCaption, Lang.getString(R.string.ChatContentAnimation), caption); // TODO style - } else { - U.set(isTranslatable, false); - return caption; - } - } - case TdApi.MessagePhoto.CONSTRUCTOR: { - String caption = ((TdApi.MessagePhoto) m.content).caption.text; - if (!allowCaptions || StringUtils.isEmpty(caption)) { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentPhoto); - } else if (forceMediaType) { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentWithCaption, Lang.getString(R.string.ChatContentPhoto), caption); // TODO style - } else { - U.set(isTranslatable, false); - return caption; - } - } - case TdApi.MessageDice.CONSTRUCTOR: { - U.set(isTranslatable, true); - String emoji = ((TdApi.MessageDice) m.content).emoji; - int value = ((TdApi.MessageDice) m.content).value; - if (TD.EMOJI_DART.textRepresentation.equals(emoji)) { - return Lang.getString(getDartRes(value)); - } - if (TD.EMOJI_DICE.textRepresentation.equals(emoji)) { - if (value != 0) { - return Lang.plural(R.string.ChatContentDiceRolled, value); - } else { - return Lang.getString(R.string.ChatContentDice); - } - } - return emoji; - } - case TdApi.MessagePoll.CONSTRUCTOR: { - String question = ((TdApi.MessagePoll) m.content).poll.question; - U.set(isTranslatable, false); - return question; - } - case TdApi.MessageDocument.CONSTRUCTOR: { - TdApi.MessageDocument doc = (TdApi.MessageDocument) m.content; - String caption = doc.caption.text; - String mediaType = doc.document != null && !StringUtils.isEmpty(doc.document.fileName) ? doc.document.fileName : null; - if (!allowCaptions || StringUtils.isEmpty(caption)) { - if (mediaType != null) { - U.set(isTranslatable, false); - return mediaType; - } else { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentFile); - } - } else if (forceMediaType) { - if (mediaType == null) { - mediaType = Lang.getString(R.string.ChatContentFile); - } - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentWithCaption, mediaType, caption); // TODO style - } else { - U.set(isTranslatable, false); - return caption; - } - } - case TdApi.MessageVoiceNote.CONSTRUCTOR: { - String caption = ((TdApi.MessageVoiceNote) m.content).caption.text; - if (!allowCaptions || StringUtils.isEmpty(caption)) { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentVoice); - } else if (forceMediaType) { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentWithCaption, Lang.getString(R.string.ChatContentVoice), caption); // TODO style - } else { - U.set(isTranslatable, false); - return caption; - } - } - case TdApi.MessageAudio.CONSTRUCTOR: { - TdApi.Audio audio = ((TdApi.MessageAudio) m.content).audio; - String caption = ((TdApi.MessageAudio) m.content).caption.text; - String mediaType = Lang.getString(R.string.ChatContentSong, TD.getTitle(audio), TD.getSubtitle(audio)); - if (!allowCaptions || StringUtils.isEmpty(caption)) { - U.set(isTranslatable, true); - return mediaType; - } else if (forceMediaType) { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentWithCaption, mediaType, caption); // TODO style - } else { - U.set(isTranslatable, false); - return caption; - } - } - case TdApi.MessageVideo.CONSTRUCTOR: { - String caption = ((TdApi.MessageVideo) m.content).caption.text; - if (!allowCaptions || StringUtils.isEmpty(caption)) { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentVideo); - } else if (forceMediaType) { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentWithCaption, Lang.getString(R.string.ChatContentVideo), caption); // TODO style - } else { - U.set(isTranslatable, false); - return caption; - } - } - case TdApi.MessageExpiredPhoto.CONSTRUCTOR: { - U.set(isTranslatable, true); - return Lang.getString(R.string.AttachPhotoExpired); - } - case TdApi.MessageInvoice.CONSTRUCTOR: { - U.set(isTranslatable, false); - return ((TdApi.MessageInvoice) m.content).title; - } - case TdApi.MessageExpiredVideo.CONSTRUCTOR: { - U.set(isTranslatable, true); - return Lang.getString(R.string.AttachVideoExpired); - } - case TdApi.MessageCall.CONSTRUCTOR: { - U.set(isTranslatable, true); - TdApi.MessageCall call = (TdApi.MessageCall) m.content; - // StringBuilder b = new StringBuilder(/*UI.getString(TGUtils.getCallName(call, TGUtils.isOut(m), true))*/); - String content; - boolean isOut = TD.isOut(m); - switch (call.discardReason.getConstructor()) { - case TdApi.CallDiscardReasonDeclined.CONSTRUCTOR: - content = Lang.getString(isOut ? R.string.OutgoingCallBusy : R.string.CallMessageIncomingDeclined); - break; - case TdApi.CallDiscardReasonMissed.CONSTRUCTOR: - content = Lang.getString(isOut ? R.string.CallMessageOutgoingMissed : R.string.MissedCall); - break; - case TdApi.CallDiscardReasonDisconnected.CONSTRUCTOR: - case TdApi.CallDiscardReasonHungUp.CONSTRUCTOR: - case TdApi.CallDiscardReasonEmpty.CONSTRUCTOR: - default: - content = Lang.getString(isOut ? R.string.OutgoingCall : R.string.IncomingCall); - break; - } - - if (call.duration > 0) { - return Lang.getString(R.string.ChatContentCallWithDuration, content, Lang.getDurationFull(call.duration)); - } else { - return content; - } - } - case TdApi.MessageVideoNote.CONSTRUCTOR: { - U.set(isTranslatable, true); - return Lang.getString(R.string.ChatContentRoundVideo); - } - case TdApi.MessageWebsiteConnected.CONSTRUCTOR: { - U.set(isTranslatable, true); - return Lang.getString(R.string.BotWebsiteAllowed, ((TdApi.MessageWebsiteConnected) m.content).domainName); - } - case TdApi.MessageSticker.CONSTRUCTOR: { - U.set(isTranslatable, true); - TdApi.Sticker sticker = m.content != null ? ((TdApi.MessageSticker) m.content).sticker : null; - return sticker != null && !StringUtils.isEmpty(sticker.emoji) ? Lang.getString(R.string.sticker, sticker.emoji) : Lang.getString(R.string.Sticker); - } - case TdApi.MessageVenue.CONSTRUCTOR: { - U.set(isTranslatable, true); - return Lang.getString(R.string.Location); - } - case TdApi.MessageLocation.CONSTRUCTOR: { - U.set(isTranslatable, true); - int resource = ((TdApi.MessageLocation) m.content).livePeriod > 0 ? R.string.AttachLiveLocation : R.string.Location; - return Lang.getString(resource); - } - case TdApi.MessageContact.CONSTRUCTOR: { - U.set(isTranslatable, true); - return Lang.getString(R.string.AttachContact); - } - case TdApi.MessageGame.CONSTRUCTOR: { - U.set(isTranslatable, false); - TdApi.Game game = ((TdApi.MessageGame) m.content).game; - return TD.getGameName(game, false); - } - case TdApi.MessageContactRegistered.CONSTRUCTOR: { - U.set(isTranslatable, true); - return Lang.getString(R.string.NotificationContactJoined, tdlib.senderName(m.senderId, true)); - } - case TdApi.MessagePinMessage.CONSTRUCTOR: { - U.set(isTranslatable, true); - if (m.isChannelPost) { - if (StringUtils.isEmpty(m.authorSignature)) { - return Lang.getString(R.string.PinnedMessageAction); - } else { - return Lang.getString(R.string.NotificationActionPinnedNoTextChannel, m.authorSignature); - } - } else { - return Lang.getString(R.string.NotificationActionPinnedNoTextChannel, tdlib.senderName(m.senderId, true)); - } - } - case TdApi.MessageGameScore.CONSTRUCTOR: { - U.set(isTranslatable, true); - final int score = ((TdApi.MessageGameScore) m.content).score; - if (tdlib.isSelfSender(m)) { - return Lang.plural(R.string.game_ActionYouScored, score); - } else { - return Lang.plural(R.string.game_ActionUserScored, score, tdlib.senderName(m.senderId, true)); - } - } - // Secret chats - case TdApi.MessageScreenshotTaken.CONSTRUCTOR: { - U.set(isTranslatable, true); - if (TD.isOut(m)) { - return Lang.getString(R.string.YouTookAScreenshot); - } else { - return Lang.getString(R.string.XTookAScreenshot, tdlib.senderName(m.senderId, true)); - } - } - case TdApi.MessageChatSetMessageAutoDeleteTime.CONSTRUCTOR: { - U.set(isTranslatable, true); - TdApi.MessageChatSetMessageAutoDeleteTime ttl = (TdApi.MessageChatSetMessageAutoDeleteTime) m.content; - if (ChatId.isUserChat(m.chatId)) { - if (ttl.messageAutoDeleteTime == 0) { - if (TD.isOut(m)) { - return Lang.getString(R.string.YouDisabledTimer); - } else { - return Lang.getString(R.string.XDisabledTimer, tdlib.senderName(m.senderId, true)); - } - } else { - if (TD.isOut(m)) { - return Lang.pluralDuration(ttl.messageAutoDeleteTime, TimeUnit.SECONDS, R.string.YouSetTimerSeconds, R.string.YouSetTimerMinutes, R.string.YouSetTimerHours, R.string.YouSetTimerDays, R.string.YouSetTimerWeeks, R.string.YouSetTimerMonths).toString(); - } else { - return Lang.pluralDuration(ttl.messageAutoDeleteTime, TimeUnit.SECONDS, R.string.XSetTimerSeconds, R.string.XSetTimerMinutes, R.string.XSetTimerHours, R.string.XSetTimerDays, R.string.XSetTimerWeeks, R.string.XSetTimerMonths, tdlib.senderName(m.senderId, true)).toString(); - } - } - } else { - if (ttl.messageAutoDeleteTime == 0) { - if (TD.isOut(m)) { - return Lang.getString(R.string.YouDisabledAutoDelete); - } else { - return Lang.getString(m.isChannelPost ? R.string.XDisabledAutoDeletePosts : R.string.XDisabledAutoDelete, tdlib.senderName(m.senderId, true)); - } - } else if (m.isChannelPost) { - if (TD.isOut(m)) { - return Lang.pluralDuration(ttl.messageAutoDeleteTime, TimeUnit.SECONDS, R.string.YouSetAutoDeletePostsSeconds, R.string.YouSetAutoDeletePostsMinutes, R.string.YouSetAutoDeletePostsHours, R.string.YouSetAutoDeletePostsDays, R.string.YouSetAutoDeletePostsWeeks, R.string.YouSetAutoDeletePostsMonths).toString(); - } else { - return Lang.pluralDuration(ttl.messageAutoDeleteTime, TimeUnit.SECONDS, R.string.XSetAutoDeletePostsSeconds, R.string.XSetAutoDeletePostsMinutes, R.string.XSetAutoDeletePostsHours, R.string.XSetAutoDeletePostsDays, R.string.XSetAutoDeletePostsWeeks, R.string.XSetAutoDeletePostsMonths, tdlib.senderName(m.senderId, true)).toString(); - } - } else { - if (TD.isOut(m)) { - return Lang.pluralDuration(ttl.messageAutoDeleteTime, TimeUnit.SECONDS, R.string.YouSetAutoDeleteSeconds, R.string.YouSetAutoDeleteMinutes, R.string.YouSetAutoDeleteHours, R.string.YouSetAutoDeleteDays, R.string.YouSetAutoDeleteWeeks, R.string.YouSetAutoDeleteMonths).toString(); - } else { - return Lang.pluralDuration(ttl.messageAutoDeleteTime, TimeUnit.SECONDS, R.string.XSetAutoDeleteSeconds, R.string.XSetAutoDeleteMinutes, R.string.XSetAutoDeleteHours, R.string.XSetAutoDeleteDays, R.string.XSetAutoDeleteWeeks, R.string.XSetAutoDeleteMonths, tdlib.senderName(m.senderId, true)).toString(); - } - } - } - } - // Group - case TdApi.MessageBasicGroupChatCreate.CONSTRUCTOR: { - U.set(isTranslatable, true); - if (TD.isOut(m)) { - return Lang.getString(R.string.YouCreatedGroup); - } else { - return Lang.getString(R.string.XCreatedGroup, tdlib.senderName(m.senderId, true)); - } - } - case TdApi.MessageSupergroupChatCreate.CONSTRUCTOR: { - U.set(isTranslatable, true); - if (TD.isOut(m)) { - return Lang.getString(m.isChannelPost ? R.string.YouCreatedChannel : R.string.YouCreatedGroup); - } else if (m.isChannelPost) { - return Lang.getString(R.string.ActionCreateChannel); - } else { - return Lang.getString(R.string.XCreatedGroup, tdlib.senderName(m.senderId, true)); - } - } - case TdApi.MessageChatJoinByLink.CONSTRUCTOR: { - U.set(isTranslatable, true); - if (TD.isOut(m)) { - return Lang.getString(R.string.YouJoinedByLink); - } else { - return Lang.getString(R.string.XJoinedByLink, tdlib.senderName(m.senderId, true)); - } - } - case TdApi.MessageChatJoinByRequest.CONSTRUCTOR: { - U.set(isTranslatable, true); - if (TD.isOut(m)) { - return Lang.getString(m.isChannelPost ? R.string.YouAcceptedToChannel : R.string.YouAcceptedToGroup); - } else { - return Lang.getString(m.isChannelPost ? R.string.XAcceptedToChannel : R.string.XAcceptedToGroup, tdlib.senderName(m.senderId, true)); - } - } - // Supergroup migration - case TdApi.MessageChatUpgradeFrom.CONSTRUCTOR: - case TdApi.MessageChatUpgradeTo.CONSTRUCTOR: { - U.set(isTranslatable, true); - return Lang.getString(R.string.GroupUpgraded); - } - - case TdApi.MessageChatChangeTitle.CONSTRUCTOR: { - U.set(isTranslatable, true); - if (m.isChannelPost) - return Lang.getString(R.string.ActionChannelChangedTitle); - else - return Lang.getString(R.string.XChangedGroupTitle, tdlib.senderName(m.senderId, true)); - } - - case TdApi.MessageChatChangePhoto.CONSTRUCTOR: { - U.set(isTranslatable, true); - if (m.isChannelPost) - return Lang.getString(R.string.ActionChannelChangedPhoto); - else if (TD.isOut(m)) - return Lang.getString(R.string.group_photo_changed_you); - else - return Lang.getString(R.string.group_photo_changed, tdlib.senderName(m.senderId, true)); - } - - case TdApi.MessageChatDeletePhoto.CONSTRUCTOR: { - U.set(isTranslatable, true); - if (m.isChannelPost) { - return Lang.getString(R.string.ActionChannelRemovedPhoto); - } else if (TD.isOut(m)) { - return Lang.getString(R.string.group_photo_deleted_you); - } else { - return Lang.getString(R.string.group_photo_deleted, tdlib.senderName(m.senderId, true)); - } - } - - case TdApi.MessageChatAddMembers.CONSTRUCTOR: { - U.set(isTranslatable, true); - final TdApi.MessageChatAddMembers users = (TdApi.MessageChatAddMembers) m.content; - if (m.isChannelPost) { - if (users.memberUserIds.length == 1) { - if (tdlib.isSelfUserId(users.memberUserIds[0])) { - return Lang.getString(R.string.channel_user_add_self); - } else { - return Lang.getString(R.string.channel_user_add, tdlib.cache().userFirstName(users.memberUserIds[0])); - } - } else { - return Lang.plural(R.string.xPeopleJoinedChannel, users.memberUserIds.length); - } - } else { - if (users.memberUserIds.length == 1) { - long joinedUserId = users.memberUserIds[0]; - if (joinedUserId != Td.getSenderUserId(m)) { - if (tdlib.isSelfUserId(joinedUserId)) { - return Lang.getString(R.string.group_user_added_self, tdlib.senderName(m.senderId, true)); - } else if (tdlib.isSelfSender(m.senderId)) { - return Lang.getString(R.string.group_user_self_added, tdlib.cache().userFirstName(joinedUserId)); - } else { - return Lang.getString(R.string.group_user_added, tdlib.senderName(m.senderId, true), tdlib.cache().userFirstName(joinedUserId)); - } - } else { - if (tdlib.isSelfUserId(joinedUserId)) { - return Lang.getString(R.string.group_user_add_self); - } else { - return Lang.getString(R.string.group_user_add, tdlib.cache().userFirstName(joinedUserId)); - } - } - } else { - return Lang.plural(R.string.xPeopleJoinedGroup, users.memberUserIds.length); - } - } - } - - case TdApi.MessageChatDeleteMember.CONSTRUCTOR: { - U.set(isTranslatable, true); - final long deletedUserId = ((TdApi.MessageChatDeleteMember) m.content).userId; - if (m.isChannelPost && Td.getSenderUserId(m) == deletedUserId) { - if (tdlib.isSelfUserId(deletedUserId)) { - return Lang.getString(R.string.channel_user_remove_self); - } else { - return Lang.getString(R.string.channel_user_remove, tdlib.cache().userFirstName(deletedUserId)); - } - } else { - if (Td.getSenderUserId(m) == deletedUserId) { - if (tdlib.isSelfUserId(deletedUserId)) { - return Lang.getString(R.string.group_user_remove_self); - } else { - return Lang.getString(R.string.group_user_remove, tdlib.cache().userFirstName(deletedUserId)); - } - } else { - if (tdlib.isSelfSender(m)) { - return Lang.getString(R.string.group_user_self_removed, tdlib.cache().userFirstName(deletedUserId)); - } else if (tdlib.isSelfUserId(deletedUserId)) { - return Lang.getString(R.string.group_user_removed_self, tdlib.senderName(m.senderId, true)); - } else { - return Lang.getString(R.string.group_user_removed, tdlib.senderName(m.senderId, true), tdlib.cache().userFirstName(deletedUserId)); - } - } - } - } - - case TdApi.MessagePaymentSuccessful.CONSTRUCTOR: { - U.set(isTranslatable, true); - TdApi.MessagePaymentSuccessful successful = (TdApi.MessagePaymentSuccessful) m.content; - return Lang.getString(R.string.PaymentSuccessfullyPaidNoItem, CurrencyUtils.buildAmount(successful.currency, successful.totalAmount), tdlib.chatTitle(m.chatId)); - } - case TdApi.MessageGiftedPremium.CONSTRUCTOR: { - U.set(isTranslatable, true); - TdApi.MessageGiftedPremium giftedPremium = (TdApi.MessageGiftedPremium) m.content; - if (m.isOutgoing) { - return Lang.plural(R.string.YouGiftedPremium, giftedPremium.monthCount, CurrencyUtils.buildAmount(giftedPremium.currency, giftedPremium.amount)); - } else { - return Lang.plural(R.string.GiftedPremium, giftedPremium.monthCount, tdlib.senderName(m.senderId, true), CurrencyUtils.buildAmount(giftedPremium.currency, giftedPremium.amount)); - } - } - case TdApi.MessageWebAppDataSent.CONSTRUCTOR: { - U.set(isTranslatable, true); - TdApi.MessageWebAppDataSent webAppDataSent = (TdApi.MessageWebAppDataSent) m.content; - return Lang.getString(R.string.BotDataSent, webAppDataSent.buttonText); - } - case TdApi.MessageCustomServiceAction.CONSTRUCTOR: { - U.set(isTranslatable, false); - return ((TdApi.MessageCustomServiceAction) m.content).text; - } - case TdApi.MessageUnsupported.CONSTRUCTOR: { - U.set(isTranslatable, true); - return Lang.getString(R.string.UnsupportedMessageType); - } - // Unsupported in this method - case TdApi.MessageChatSetTheme.CONSTRUCTOR: - case TdApi.MessageInviteVideoChatParticipants.CONSTRUCTOR: - case TdApi.MessageProximityAlertTriggered.CONSTRUCTOR: - case TdApi.MessageVideoChatEnded.CONSTRUCTOR: - case TdApi.MessageVideoChatScheduled.CONSTRUCTOR: - case TdApi.MessageVideoChatStarted.CONSTRUCTOR: - throw new IllegalArgumentException(m.content.toString()); - // Bots only. Unused - case TdApi.MessagePassportDataReceived.CONSTRUCTOR: - case TdApi.MessagePaymentSuccessfulBot.CONSTRUCTOR: - case TdApi.MessageWebAppDataReceived.CONSTRUCTOR: - throw new IllegalStateException(m.content.toString()); - // TODO - case TdApi.MessagePassportDataSent.CONSTRUCTOR: - case TdApi.MessageForumTopicCreated.CONSTRUCTOR: - case TdApi.MessageForumTopicEdited.CONSTRUCTOR: - case TdApi.MessageForumTopicIsClosedToggled.CONSTRUCTOR: - case TdApi.MessageForumTopicIsHiddenToggled.CONSTRUCTOR: - case TdApi.MessageBotWriteAccessAllowed.CONSTRUCTOR: - case TdApi.MessageChatShared.CONSTRUCTOR: - case TdApi.MessageSuggestProfilePhoto.CONSTRUCTOR: - case TdApi.MessageUserShared.CONSTRUCTOR: - default: { - U.set(isTranslatable, true); - return Lang.getString(R.string.UnsupportedMessage); - } - } - } public static String getFirstName (TdApi.User user) { if (user == null) { @@ -4436,10 +3430,11 @@ public static TdApi.Message[] filterNulls (TdApi.Message[] messages) { } public static boolean canEditText (TdApi.MessageContent content) { - return canBeEdited(content) && content.getConstructor() != TdApi.MessageLocation.CONSTRUCTOR; + return canBeEdited(content) && !Td.isLocation(content); } public static boolean canBeEdited (TdApi.MessageContent content) { + //noinspection SwitchIntDef switch (content.getConstructor()) { case TdApi.MessageText.CONSTRUCTOR: case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: @@ -4538,29 +3533,6 @@ public static String getTextFromMessageSpoilerless (TdApi.MessageContent content return null; } - public static @Nullable String getTextOrCaption (TdApi.PushMessageContent content) { - if (content == null) - return null; - switch (content.getConstructor()) { - case TdApi.PushMessageContentText.CONSTRUCTOR: return ((TdApi.PushMessageContentText) content).text; - case TdApi.PushMessageContentAnimation.CONSTRUCTOR: return ((TdApi.PushMessageContentAnimation) content).caption; - case TdApi.PushMessageContentVideo.CONSTRUCTOR: return ((TdApi.PushMessageContentVideo) content).caption; - - case TdApi.PushMessageContentPhoto.CONSTRUCTOR: return ((TdApi.PushMessageContentPhoto) content).caption; - case TdApi.PushMessageContentPoll.CONSTRUCTOR: return ((TdApi.PushMessageContentPoll) content).question; - - // FIXME: server+TDLIB missing captions in these media kinds - /*case TdApi.PushMessageContentDocument.CONSTRUCTOR: return ((TdApi.PushMessageContentDocument) content).caption; - case TdApi.PushMessageContentMediaAlbum.CONSTRUCTOR: return ((TdApi.PushMessageContentMediaAlbum) content).caption; - case TdApi.PushMessageContentAudio.CONSTRUCTOR: return ((TdApi.PushMessageContentAudio) content).caption; - case TdApi.PushMessageContentVoiceNote.CONSTRUCTOR: return ((TdApi.PushMessageContentVoiceNote) content).caption;*/ - - case TdApi.PushMessageContentChatChangeTitle.CONSTRUCTOR: return ((TdApi.PushMessageContentChatChangeTitle) content).title; - // case TdApi.PushMessageContentChatSetTheme.CONSTRUCTOR: return ((TdApi.PushMessageContentChatSetTheme) content).themeName; - } - return null; - } - public static boolean canCopyText (TdApi.Message msg) { TdApi.FormattedText text = msg != null ? Td.textOrCaption(msg.content) : null; return !Td.isEmpty(Td.trim(text)); @@ -4576,23 +3548,25 @@ public static TdApi.Message removeWebPage (TdApi.Message message) { } public static TdApi.MessageContent removeWebPage (TdApi.MessageContent content) { - if (content == null || content.getConstructor() != TdApi.MessageText.CONSTRUCTOR) { + if (content == null || !Td.isText(content)) { return content; } TdApi.MessageText messageText = (TdApi.MessageText) content; - if (messageText.webPage == null) { + String linkPreviewUrl = Td.findLinkPreviewUrl(messageText); + if (StringUtils.isEmpty(linkPreviewUrl)) { return messageText; } String linkPreviewText = "[" + Lang.getString(R.string.LinkPreview) + "]"; + // TODO maybe: show changes in LinkPreviewOptions? TdApi.FormattedText newText = Td.concat( messageText.text, new TdApi.FormattedText("\n", null), new TdApi.FormattedText(linkPreviewText, new TdApi.TextEntity[] { new TdApi.TextEntity(0, linkPreviewText.length(), new TdApi.TextEntityTypeItalic()), - new TdApi.TextEntity(0, linkPreviewText.length(), new TdApi.TextEntityTypeTextUrl(messageText.webPage.url)) + new TdApi.TextEntity(0, linkPreviewText.length(), new TdApi.TextEntityTypeTextUrl(linkPreviewUrl)) }) ); - return new TdApi.MessageText(newText, null); + return new TdApi.MessageText(newText, null, null); } public static class DownloadedFile { @@ -4710,10 +3684,11 @@ public static int getFileId (TdApi.Message msg) { public static boolean isHeavyContent (TdApi.Message message) { if (message == null) return false; - int constructor = message.content.getConstructor(); + @TdApi.MessageContent.Constructors int constructor = message.content.getConstructor(); if (constructor == TdApi.MessageText.CONSTRUCTOR) { constructor = convertToMessageContent(((TdApi.MessageText) message.content).webPage); } + //noinspection SwitchIntDef switch (constructor) { case TdApi.MessagePhoto.CONSTRUCTOR: case TdApi.MessageVideo.CONSTRUCTOR: @@ -5192,16 +4167,6 @@ public static void saveToDownloads (final BaseActivity context, final Downloaded // other - public static boolean isMessageOpened (TdApi.Message message) { - switch (message.content.getConstructor()) { - case TdApi.MessageVoiceNote.CONSTRUCTOR: - return ((TdApi.MessageVoiceNote) message.content).isListened; - case TdApi.MessageVideoNote.CONSTRUCTOR: - return ((TdApi.MessageVideoNote) message.content).isViewed; - } - return false; - } - public static void setMessageOpened (TdApi.Message message) { // FIXME when TDLib will get a field switch (message.content.getConstructor()) { @@ -5215,12 +4180,16 @@ public static void setMessageOpened (TdApi.Message message) { } public static CustomTypefaceSpan newSpan (@NonNull TdApi.TextEntityType type) { - return new CustomTypefaceSpan(null, 0).setEntityType(type); + CustomTypefaceSpan span = new CustomTypefaceSpan(null, 0); + span.setTextEntityType(type); + return span; } - public static CustomTypefaceSpan newBoldSpan (@NonNull String text) { + public static Object newBoldSpan (@NonNull String text) { boolean needFakeBold = Text.needFakeBold(text); - return new CustomTypefaceSpan(needFakeBold ? null : Fonts.getRobotoMedium(), 0).setFakeBold(needFakeBold).setEntityType(new TdApi.TextEntityTypeBold()); + CustomTypefaceSpan span = new CustomTypefaceSpan(needFakeBold ? null : Fonts.getRobotoMedium(), 0).setFakeBold(needFakeBold); + span.setTextEntityType(new TdApi.TextEntityTypeBold()); + return span; } public static TdApi.FormattedText newText (@NonNull CharSequence text) { @@ -5238,7 +4207,7 @@ public static CharSequence toDisplayCharSequence (TdApi.FormattedText text, @Nul return text.text; SpannableStringBuilder b = null; for (TdApi.TextEntity entity : text.entities) { - CharacterStyle span = toDisplaySpan(entity.type, defaultTypeface, Text.needFakeBold(text.text, entity.offset, entity.offset + entity.length)); + Object span = toDisplaySpan(entity.type, defaultTypeface, Text.needFakeBold(text.text, entity.offset, entity.offset + entity.length)); if (span != null) { if (b == null) b = new SpannableStringBuilder(text.text); @@ -5253,12 +4222,14 @@ public static CharSequence toMarkdown (TdApi.FormattedText text) { return null; if (text.entities == null || text.entities.length == 0) return text.text; - TdApi.Object result = Client.execute(new TdApi.GetMarkdownText(text)); - if (!(result instanceof TdApi.FormattedText)) { - Log.w("getMarkdownText: %s", result); + TdApi.FormattedText formattedText; + try { + formattedText = Client.execute(new TdApi.GetMarkdownText(text)); + } catch (Client.ExecutionException error) { + Log.w("getMarkdownText: %s", TD.toErrorString(error.error)); return text.text; } - return toCharSequence((TdApi.FormattedText) result, true, true); + return toCharSequence(formattedText, true, true); } private static HtmlTag toHtmlTag (TdApi.TextEntityType entityType) { @@ -5277,6 +4248,8 @@ private static HtmlTag toHtmlTag (TdApi.TextEntityType entityType) { return new HtmlTag("s"); case TdApi.TextEntityTypeUnderline.CONSTRUCTOR: return new HtmlTag("u"); + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: + return new HtmlTag("blockquote"); case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: return new HtmlTag( "", @@ -5312,13 +4285,15 @@ private static HtmlTag toHtmlTag (TdApi.TextEntityType entityType) { case TdApi.TextEntityTypeMention.CONSTRUCTOR: case TdApi.TextEntityTypePhoneNumber.CONSTRUCTOR: case TdApi.TextEntityTypeUrl.CONSTRUCTOR: - break; + return null; + default: + Td.assertTextEntityType_91234a79(); + throw Td.unsupported(entityType); } - return null; } - private static HtmlTag[] toHtmlTag (CharacterStyle span) { - TdApi.TextEntityType[] entityTypes = toEntityType((CharacterStyle) span); + private static HtmlTag[] toHtmlTag (Object span) { + TdApi.TextEntityType[] entityTypes = toEntityType(span); if (entityTypes != null && entityTypes.length > 0) { List tags = new ArrayList<>(); for (TdApi.TextEntityType entityType : entityTypes) { @@ -5336,14 +4311,14 @@ private static HtmlTag[] toHtmlTag (CharacterStyle span) { @Nullable public static String toHtmlCopyText (Spanned spanned) { - HtmlEncoder.EncodeResult encodeResult = HtmlEncoder.toHtml(spanned, CharacterStyle.class, TD::toHtmlTag); + HtmlEncoder.EncodeResult encodeResult = HtmlEncoder.toHtml(spanned, Object.class, TD::toHtmlTag); return encodeResult.tagCount > 0 ? encodeResult.htmlText : null; } @Nullable public static CharSequence htmlToCharSequence (String htmlText) { HtmlParser.Replacer entityReplacer = (text, start, end, mark) -> { - CharacterStyle span = toSpan(mark); + Object span = toSpan(mark); if (span != null) { text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } @@ -5410,7 +4385,7 @@ public static CharSequence toCharSequence (TdApi.FormattedText text, boolean all SpannableStringBuilder b = null; boolean hasSpoilers = false; for (TdApi.TextEntity entity : text.entities) { - boolean isSpoiler = entity.type.getConstructor() == TdApi.TextEntityTypeSpoiler.CONSTRUCTOR; + boolean isSpoiler = Td.isSpoiler(entity.type); if (isSpoiler) { hasSpoilers = true; } @@ -5425,7 +4400,7 @@ public static CharSequence toCharSequence (TdApi.FormattedText text, boolean all StringBuilder builder = b != null ? null : new StringBuilder(text.text); for (int i = text.entities.length - 1; i >= 0; i--) { TdApi.TextEntity entity = text.entities[i]; - if (entity.type.getConstructor() == TdApi.TextEntityTypeSpoiler.CONSTRUCTOR) { + if (Td.isSpoiler(entity.type)) { String replacement = StringUtils.multiply(SPOILER_REPLACEMENT_CHAR, entity.length); if (b != null) { b.delete(entity.offset, entity.offset + entity.length); @@ -5443,14 +4418,18 @@ public static CharSequence toCharSequence (TdApi.FormattedText text, boolean all return b != null ? SpannableString.valueOf(b) : text.text; } - public static CustomTypefaceSpan toDisplaySpan (TdApi.TextEntityType type) { + public static Object toDisplaySpan (TdApi.TextEntityType type) { return toDisplaySpan(type, null, false); } - public static CustomTypefaceSpan toDisplaySpan (TdApi.TextEntityType type, @Nullable Typeface defaultTypeface, boolean needFakeBold) { + public static Object tempToBlockQuoteSpan (boolean isDisplay, boolean needFakeBold) { + return new QuoteSpan(); // TODO + } + + public static Object toDisplaySpan (TdApi.TextEntityType type, @Nullable Typeface defaultTypeface, boolean needFakeBold) { if (type == null) return null; - CustomTypefaceSpan span; + Object span; switch (type.getConstructor()) { case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: @@ -5465,6 +4444,9 @@ public static CustomTypefaceSpan toDisplaySpan (TdApi.TextEntityType type, @Null case TdApi.TextEntityTypeItalic.CONSTRUCTOR: span = new CustomTypefaceSpan(Fonts.getRobotoItalic(), 0); break; + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: + span = tempToBlockQuoteSpan(true, needFakeBold); + break; case TdApi.TextEntityTypePre.CONSTRUCTOR: case TdApi.TextEntityTypePreCode.CONSTRUCTOR: case TdApi.TextEntityTypeCode.CONSTRUCTOR: @@ -5489,14 +4471,41 @@ public static CustomTypefaceSpan toDisplaySpan (TdApi.TextEntityType type, @Null case TdApi.TextEntityTypeMention.CONSTRUCTOR: case TdApi.TextEntityTypePhoneNumber.CONSTRUCTOR: case TdApi.TextEntityTypeUrl.CONSTRUCTOR: - default: return null; + default: + Td.assertTextEntityType_91234a79(); + throw Td.unsupported(type); + } + if (span instanceof TdlibEntitySpan) { + ((TdlibEntitySpan) span).setTextEntityType(type); } - span.setEntityType(type); return span; } - public static CharacterStyle toSpan (TdApi.TextEntityType type) { + public static void handleLegacyClick (TdlibDelegate context, String clickedText, Object span) { + // One day rework to properly designed code instead of this mess collected over time. + TdApi.TextEntityType type = span instanceof TdlibEntitySpan ? ((TdlibEntitySpan) span).getTextEntityType() : null; + if (type != null) { + //noinspection SwitchIntDef + switch (type.getConstructor()) { + case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: { + TdApi.TextEntityTypeMentionName mentionName = (TdApi.TextEntityTypeMentionName) type; + context.tdlib().ui().openPrivateProfile(context, mentionName.userId, null); + break; + } + case TdApi.TextEntityTypeUrl.CONSTRUCTOR: + UI.openUrl(clickedText); + break; + case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: { + TdApi.TextEntityTypeTextUrl textUrl = (TdApi.TextEntityTypeTextUrl) type; + UI.openUrl(textUrl.url); + break; + } + } + } + } + + public static Object toSpan (TdApi.TextEntityType type) { return toSpan(type, true); } @@ -5513,6 +4522,7 @@ public static boolean canConvertToSpan (TdApi.TextEntityType type) { case TdApi.TextEntityTypeCode.CONSTRUCTOR: case TdApi.TextEntityTypePreCode.CONSTRUCTOR: case TdApi.TextEntityTypePre.CONSTRUCTOR: + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: case TdApi.TextEntityTypeStrikethrough.CONSTRUCTOR: case TdApi.TextEntityTypeUnderline.CONSTRUCTOR: @@ -5530,12 +4540,14 @@ public static boolean canConvertToSpan (TdApi.TextEntityType type) { case TdApi.TextEntityTypeMention.CONSTRUCTOR: case TdApi.TextEntityTypePhoneNumber.CONSTRUCTOR: case TdApi.TextEntityTypeUrl.CONSTRUCTOR: - break; + return false; + default: + Td.assertTextEntityType_91234a79(); + throw Td.unsupported(type); } - return false; } - public static CharacterStyle toSpan (TdApi.TextEntityType type, boolean allowInternal) { + public static Object toSpan (TdApi.TextEntityType type, boolean allowInternal) { if (type == null) return null; switch (type.getConstructor()) { @@ -5556,9 +4568,16 @@ public static CharacterStyle toSpan (TdApi.TextEntityType type, boolean allowInt case TdApi.TextEntityTypeSpoiler.CONSTRUCTOR: return new BackgroundColorSpan(SPOILER_BACKGROUND_COLOR); case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: - return allowInternal ? new CustomTypefaceSpan(null, ColorId.textLink).setEntityType(type) : null; + if (allowInternal) { + CustomTypefaceSpan span = new CustomTypefaceSpan(null, ColorId.textLink); + span.setTextEntityType(type); + return span; + } + return null; case TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR: return new CustomEmojiId(((TdApi.TextEntityTypeCustomEmoji) type).customEmojiId); + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: + return tempToBlockQuoteSpan(false, false); // auto-detected entities case TdApi.TextEntityTypeBankCardNumber.CONSTRUCTOR: case TdApi.TextEntityTypeBotCommand.CONSTRUCTOR: @@ -5569,16 +4588,18 @@ public static CharacterStyle toSpan (TdApi.TextEntityType type, boolean allowInt case TdApi.TextEntityTypeMention.CONSTRUCTOR: case TdApi.TextEntityTypePhoneNumber.CONSTRUCTOR: case TdApi.TextEntityTypeUrl.CONSTRUCTOR: - break; + return null; + default: + Td.assertTextEntityType_91234a79(); + throw Td.unsupported(type); } - return null; } - public static TdApi.TextEntityType[] toEntityType (CharacterStyle span) { + public static TdApi.TextEntityType[] toEntityType (Object span) { if (!canConvertToEntityType(span)) return null; if (span instanceof CustomTypefaceSpan) - return new TdApi.TextEntityType[] {((CustomTypefaceSpan) span).getEntityType()}; + return new TdApi.TextEntityType[] {((CustomTypefaceSpan) span).getTextEntityType()}; if (span instanceof URLSpan) { String url = ((URLSpan) span).getURL(); if (!Strings.isValidLink(url)) @@ -5676,7 +4697,7 @@ private static boolean canConvertToEntityType (TypefaceSpan span) { return false; } - public static CharacterStyle cloneSpan (CharacterStyle span) { + public static Object cloneSpan (Object span) { if (span instanceof CustomTypefaceSpan) { CustomTypefaceSpan customTypefaceSpan = (CustomTypefaceSpan) span; return new CustomTypefaceSpan(customTypefaceSpan); @@ -5712,9 +4733,9 @@ public static CharacterStyle cloneSpan (CharacterStyle span) { throw new UnsupportedOperationException(span.toString()); } - public static boolean canConvertToEntityType (CharacterStyle span) { + public static boolean canConvertToEntityType (Object span) { if (span instanceof CustomTypefaceSpan) - return ((CustomTypefaceSpan) span).getEntityType() != null; + return ((CustomTypefaceSpan) span).getTextEntityType() != null; if (span instanceof URLSpan) return Strings.isValidLink(((URLSpan) span).getURL()); if (span instanceof StyleSpan) { @@ -5750,19 +4771,19 @@ public static TdApi.FormattedText toFormattedText (CharSequence cs, boolean only public static TdApi.TextEntity[] toEntities (CharSequence cs, boolean onlyLinks) { if (!(cs instanceof Spanned)) return null; - CharacterStyle[] spans = ((Spanned) cs).getSpans(0, cs.length(), CharacterStyle.class); + Object[] spans = ((Spanned) cs).getSpans(0, cs.length(), Object.class); if (spans == null || spans.length == 0) return null; List entities = null; - for (CharacterStyle span : spans) { + for (Object span : spans) { TdApi.TextEntityType[] types = toEntityType(span); if (types == null || types.length == 0) continue; int start = ((Spanned) cs).getSpanStart(span); int end = ((Spanned) cs).getSpanEnd(span); for (TdApi.TextEntityType type : types) { - if (onlyLinks && type.getConstructor() != TdApi.TextEntityTypeTextUrl.CONSTRUCTOR) + if (onlyLinks && !Td.isTextUrl(type)) continue; if (entities == null) entities = new ArrayList<>(); @@ -5805,11 +4826,11 @@ private static int splitEntities (List entities, int startInde return -1; } - public static List sendMessageText (long chatId, long messageThreadId, long replyToMessageId, TdApi.MessageSendOptions sendOptions, @NonNull TdApi.InputMessageContent content, int maxCodePointCount) { + public static List sendMessageText (long chatId, long messageThreadId, @Nullable TdApi.InputMessageReplyTo replyTo, TdApi.MessageSendOptions sendOptions, @NonNull TdApi.InputMessageContent content, int maxCodePointCount) { List list = explodeText(content, maxCodePointCount); List result = new ArrayList<>(list.size()); for (TdApi.InputMessageContent item : list) { - result.add(new TdApi.SendMessage(chatId, messageThreadId, replyToMessageId, sendOptions, null, item)); + result.add(new TdApi.SendMessage(chatId, messageThreadId, replyTo, sendOptions, null, item)); } return result; } @@ -5873,7 +4894,7 @@ public static List explodeText (@NonNull TdApi.InputM // Send chunk between start ... end substring = Td.substring(text, start, end); boolean first = list.isEmpty(); - list.add(new TdApi.InputMessageText(substring, textContent.disableWebPagePreview, first && textContent.clearDraft)); + list.add(new TdApi.InputMessageText(substring, textContent.linkPreviewOptions, first && textContent.clearDraft)); // Reset loop state start = end; currentCodePointCount = 0; @@ -5882,1096 +4903,484 @@ public static List explodeText (@NonNull TdApi.InputM return list; } - public static class ContentPreview { - public final @Nullable Emoji emoji, parentEmoji; - public final @StringRes int placeholderText; - public final @Nullable TdApi.FormattedText formattedText; - public final boolean isTranslatable; - public final boolean hideAuthor; - - public ContentPreview (@Nullable Emoji emoji, @StringRes int placeholderTextRes) { - this(emoji, placeholderTextRes, (TdApi.FormattedText) null); - } - public ContentPreview (@Nullable Emoji emoji, @StringRes int placeholderTextRes, @Nullable String text) { - this(emoji, placeholderTextRes, StringUtils.isEmpty(text) ? null : new TdApi.FormattedText(text, null), false); - } - public ContentPreview (@Nullable Emoji emoji, @StringRes int placeholderTextRes, @Nullable TdApi.FormattedText text) { - this(emoji, placeholderTextRes, text, false); + public static boolean hasIncompleteLoginAttempts (TdApi.Session[] sessions) { + for (TdApi.Session session : sessions) { + if (session.isPasswordPending) + return true; } + return false; + } - public ContentPreview (@Nullable String text, boolean textTranslatable) { - this(null, 0, text, textTranslatable); - } + public static long[] getUniqueEmojiIdList (@Nullable TdApi.FormattedText text) { + if (text == null || text.text == null || text.entities == null || text.entities.length == 0) return new long[0]; - public ContentPreview (@Nullable TdApi.FormattedText text, boolean textTranslatable) { - this(null, 0, text, textTranslatable); + LongSet emojis = new LongSet(); + for (TdApi.TextEntity entity : text.entities) { + if (Td.isCustomEmoji(entity.type)) { + emojis.add(((TdApi.TextEntityTypeCustomEmoji) entity.type).customEmojiId); + } } - public ContentPreview (@Nullable Emoji emoji, @StringRes int placeholderTextRes, @Nullable TdApi.FormattedText text, boolean textTranslatable) { - this(emoji, placeholderTextRes, text, textTranslatable, false, null); - } + return emojis.toArray(); + } - public ContentPreview (@Nullable Emoji emoji, @StringRes int placeholderTextRes, @Nullable String text, boolean textTranslatable) { - this(emoji, placeholderTextRes, StringUtils.isEmpty(text) ? null : new TdApi.FormattedText(text, null), textTranslatable, false, null); - } + public static String stickerEmoji (TdApi.Sticker sticker) { + return !StringUtils.isEmpty(sticker.emoji) ? sticker.emoji : "\uD83D\uDE00" /*😀*/; + } - public ContentPreview (@Nullable Emoji emoji, ContentPreview copy) { - this(copy.emoji, copy.placeholderText, copy.formattedText, copy.isTranslatable, copy.hideAuthor, emoji); - } + public static TdApi.FormattedText toSingleEmojiText (TdApi.Sticker sticker) { + String emoji = stickerEmoji(sticker); + return new TdApi.FormattedText(emoji, new TdApi.TextEntity[]{ + new TdApi.TextEntity(0, emoji.length(), new TdApi.TextEntityTypeCustomEmoji(Td.customEmojiId(sticker))) + }); + } - public ContentPreview (@Nullable TdApi.FormattedText text, ContentPreview copy) { - this(copy.emoji, copy.placeholderText, text != null ? text: copy.formattedText, copy.isTranslatable, copy.hideAuthor, copy.parentEmoji); + public static int getStickerSetsUnreadCount (TdApi.StickerSetInfo[] stickerSets) { + int unreadCount = 0; + for (TdApi.StickerSetInfo stickerSet : stickerSets) { + if (!stickerSet.isViewed) { + unreadCount++; + } } + return unreadCount; + } - public ContentPreview (@Nullable Emoji emoji, int placeholderText, @Nullable TdApi.FormattedText formattedText, boolean isTranslatable, boolean hideAuthor, @Nullable Emoji parentEmoji) { - this.emoji = emoji; - this.placeholderText = placeholderText; - this.formattedText = formattedText; - this.isTranslatable = isTranslatable; - this.hideAuthor = hideAuthor; - this.parentEmoji = parentEmoji; + public static boolean containsMention (TdApi.FormattedText text, TdApi.User user) { + if (text == null || user == null || text.entities == null || StringUtils.isEmpty(text.text)) { + return false; } - @NonNull - public String buildText (boolean allowIcon) { - if (emoji == null || (allowIcon && emoji.iconRepresentation != 0)) { - return Td.isEmpty(formattedText) ? (placeholderText != 0 ? Lang.getString(placeholderText) : "") : formattedText.text; - } else if (Td.isEmpty(formattedText)) { - return placeholderText != 0 ? emoji.textRepresentation + " " + Lang.getString(placeholderText) : emoji.textRepresentation; - } else if (formattedText.text.startsWith(emoji.textRepresentation)) { - return formattedText.text; - } else { - return emoji.textRepresentation + " " + formattedText.text; + for (TdApi.TextEntity entity: text.entities) { + TdApi.TextEntityType type = entity.type; + if (type.getConstructor() == TdApi.TextEntityTypeMention.CONSTRUCTOR) { + if (entity.length > 1 && Td.findUsername(user.usernames, text.text.substring(entity.offset + 1, entity.offset + entity.length), true)) { + return true; + } + } else if (type.getConstructor() == TdApi.TextEntityTypeMentionName.CONSTRUCTOR) { + if (user.id == ((TdApi.TextEntityTypeMentionName) type).userId) { + return true; + } } } - public TdApi.FormattedText buildFormattedText (boolean allowIcon) { - if (emoji == null || (allowIcon && emoji.iconRepresentation != 0)) { - return Td.isEmpty(formattedText) ? new TdApi.FormattedText(placeholderText != 0 ? Lang.getString(placeholderText) : "", null) : formattedText; - } else if (Td.isEmpty(formattedText)) { - return new TdApi.FormattedText(placeholderText != 0 ? emoji.textRepresentation + " " + Lang.getString(placeholderText) : emoji.textRepresentation, null); - } else if (formattedText.text.startsWith(emoji.textRepresentation)) { - return formattedText; - } else { - return TD.withPrefix(emoji.textRepresentation + " ", formattedText); - } - } + return false; + } - @Override - @NonNull - public String toString () { - return buildText(false); + public static boolean isScreenshotSensitive (TdApi.Message message) { + if (message == null) { + return false; } - - private Refresher refresher; - private boolean isMediaGroup; - - public ContentPreview setRefresher (Refresher refresher, boolean isMediaGroup) { - this.refresher = refresher; - this.isMediaGroup = isMediaGroup; - return this; + if (Td.isSecret(message.content)) { + return true; } - - public boolean hasRefresher () { - return refresher != null; + switch (message.content.getConstructor()) { + case TdApi.MessageExpiredPhoto.CONSTRUCTOR: + case TdApi.MessageExpiredVideo.CONSTRUCTOR: + return true; + default: + Td.assertMessageContent_d40af239(); + break; } + return false; + } - public boolean isMediaGroup () { - return isMediaGroup; + public static boolean hasCustomEmoji (TdApi.FormattedText text) { + if (text == null || text.entities == null) { + return false; } - public void refreshContent (@NonNull RefreshCallback callback) { - if (refresher != null) { - refresher.runRefresher(this, callback); + for (TdApi.TextEntity entity: text.entities) { + if (entity.type.getConstructor() == TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR) { + return true; } } - private Tdlib.Album album; - - @Nullable - public Tdlib.Album getAlbum () { - return album; - } - - public interface Refresher { - void runRefresher (ContentPreview oldPreview, RefreshCallback callback); - } + return false; + } - public interface RefreshCallback { - void onContentPreviewChanged (long chatId, long messageId, ContentPreview newPreview, ContentPreview oldPreview); - default void onContentPreviewNotChanged (long chatId, long messageId, ContentPreview oldContent) { } + public static boolean isStickerFromAnimatedEmojiPack (@Nullable TdApi.MessageContent content) { + if (content == null || content.getConstructor() != TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { + return false; } + return isStickerFromAnimatedEmojiPack(((TdApi.MessageAnimatedEmoji) content).animatedEmoji.sticker); } - @NonNull - public static ContentPreview getChatListPreview (Tdlib tdlib, long chatId, TdApi.Message message) { - return getContentPreview(tdlib, chatId, message, true, true); + public static boolean isStickerFromAnimatedEmojiPack (@Nullable TdApi.Sticker sticker) { + return sticker != null && sticker.setId == TdConstants.TELEGRAM_ANIMATED_EMOJI_STICKER_SET_ID; } - - @NonNull - public static ContentPreview getNotificationPreview (Tdlib tdlib, long chatId, TdApi.Message message, boolean allowContent) { - return getContentPreview(tdlib, chatId, message, allowContent, false); + + public static boolean isChatListMain (@Nullable TdApi.ChatList chatList) { + return chatList != null && chatList.getConstructor() == TdApi.ChatListMain.CONSTRUCTOR; } - private static final int ARG_NONE = 0; - private static final int ARG_TRUE = 1; - private static final int ARG_POLL_QUIZ = 1; - private static final int ARG_CALL_DECLINED = -1; - private static final int ARG_CALL_MISSED = -2; - private static final int ARG_RECURRING_PAYMENT = -3; - - private static final long ADDITIONAL_MESSAGE_UI_LOAD_TIMEOUT_MS = -1; // Always async - private static final long ADDITIONAL_MESSAGE_LOAD_TIMEOUT_MS = 0; + public static boolean isChatListArchive (@Nullable TdApi.ChatList chatList) { + return chatList != null && chatList.getConstructor() == TdApi.ChatListArchive.CONSTRUCTOR; + } - private static long additionalMessageLoadTimeoutMs () { - if (UI.inUiThread()) { - return ADDITIONAL_MESSAGE_UI_LOAD_TIMEOUT_MS; - } else { - return ADDITIONAL_MESSAGE_LOAD_TIMEOUT_MS; - } + public static boolean isChatListFolder (@Nullable TdApi.ChatList chatList) { + return chatList != null && chatList.getConstructor() == TdApi.ChatListFolder.CONSTRUCTOR; } - @NonNull - private static ContentPreview getContentPreview (Tdlib tdlib, long chatId, TdApi.Message message, boolean allowContent, boolean isChatList) { - if (Settings.instance().needRestrictContent()) { - if (!StringUtils.isEmpty(message.restrictionReason)) { - return new ContentPreview(TD.EMOJI_ERROR, 0, message.restrictionReason, false); - } - if (!isChatList) { // Otherwise lookup is done inside TGChat for performance reason - String restrictionReason = tdlib.chatRestrictionReason(chatId); - if (restrictionReason != null) { - return new TD.ContentPreview(TD.EMOJI_ERROR, 0, restrictionReason, false); - } - } - } - int type = message.content.getConstructor(); - TdApi.FormattedText formattedText; - if (allowContent) { - formattedText = Td.textOrCaption(message.content); - if (message.isOutgoing) { - TdApi.FormattedText pendingText = tdlib.getPendingFormattedText(message.chatId, message.id); - if (pendingText != null) { - formattedText = pendingText; - } - } - } else { - formattedText = null; - } - String alternativeText = null; - boolean alternativeTextTranslatable = false; - int arg1 = ARG_NONE; - switch (type) { - case TdApi.MessageText.CONSTRUCTOR: { - TdApi.MessageText messageText = (TdApi.MessageText) message.content; - if (!Td.isEmpty(messageText.text) && messageText.text.entities != null) { - boolean isUrl = false; - for (TdApi.TextEntity entity : messageText.text.entities) { - switch (entity.type.getConstructor()) { - case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: - case TdApi.TextEntityTypeUrl.CONSTRUCTOR: { - if (entity.offset == 0 && ( - entity.length == messageText.text.text.length() || - entity.type.getConstructor() != TdApi.TextEntityTypeTextUrl.CONSTRUCTOR || - !StringUtils.isEmptyOrInvisible(Td.substring(messageText.text.text, entity - ))) - ) { - isUrl = true; - break; - } - break; - } - } - } - if (isUrl) { - arg1 = ARG_TRUE; - } - } - break; - } - case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: { - TdApi.MessageAnimatedEmoji animatedEmoji = (TdApi.MessageAnimatedEmoji) message.content; - alternativeText = animatedEmoji.emoji; - break; - } - case TdApi.MessageDocument.CONSTRUCTOR: - alternativeText = ((TdApi.MessageDocument) message.content).document.fileName; - break; - case TdApi.MessageVoiceNote.CONSTRUCTOR: { - int duration = ((TdApi.MessageVoiceNote) message.content).voiceNote.duration; - if (duration > 0) { - alternativeText = Lang.getString(R.string.ChatContentVoiceDuration, Lang.getString(R.string.ChatContentVoice), Strings.buildDuration(duration)); - alternativeTextTranslatable = true; - } - break; - } - case TdApi.MessageVideoNote.CONSTRUCTOR: { - int duration = ((TdApi.MessageVideoNote) message.content).videoNote.duration; - if (duration > 0) { - alternativeText = Lang.getString(R.string.ChatContentVoiceDuration, Lang.getString(R.string.ChatContentRoundVideo), Strings.buildDuration(duration)); - alternativeTextTranslatable = true; - } - break; - } - case TdApi.MessageAudio.CONSTRUCTOR: - TdApi.Audio audio = ((TdApi.MessageAudio) message.content).audio; - alternativeText = Lang.getString(R.string.ChatContentSong, TD.getTitle(audio), TD.getSubtitle(audio)); - alternativeTextTranslatable = !hasTitle(audio) || !hasSubtitle(audio); - break; - case TdApi.MessageContact.CONSTRUCTOR: { - TdApi.Contact contact = ((TdApi.MessageContact) message.content).contact; - String name = TD.getUserName(contact.firstName, contact.lastName); - if (!StringUtils.isEmpty(name)) - alternativeText = name; - break; - } - case TdApi.MessagePoll.CONSTRUCTOR: - alternativeText = ((TdApi.MessagePoll) message.content).poll.question; - arg1 = ((TdApi.MessagePoll) message.content).poll.type.getConstructor() == TdApi.PollTypeRegular.CONSTRUCTOR ? ARG_NONE : ARG_POLL_QUIZ; - break; - case TdApi.MessageDice.CONSTRUCTOR: - alternativeText = ((TdApi.MessageDice) message.content).emoji; - arg1 = ((TdApi.MessageDice) message.content).value; - break; - case TdApi.MessageCall.CONSTRUCTOR: { - switch (((TdApi.MessageCall) message.content).discardReason.getConstructor()) { - case TdApi.CallDiscardReasonDeclined.CONSTRUCTOR: - arg1 = ARG_CALL_DECLINED; - break; - case TdApi.CallDiscardReasonMissed.CONSTRUCTOR: - arg1 = ARG_CALL_MISSED; - break; - default: - arg1 = ((TdApi.MessageCall) message.content).duration; - break; - } - break; - } - case TdApi.MessageLocation.CONSTRUCTOR: { - TdApi.MessageLocation location = ((TdApi.MessageLocation) message.content); - alternativeText = location.livePeriod == 0 || location.expiresIn == 0 ? null : "live"; - break; - } - case TdApi.MessageGame.CONSTRUCTOR: - alternativeText = ((TdApi.MessageGame) message.content).game.title; - break; - case TdApi.MessageSticker.CONSTRUCTOR: - TdApi.Sticker sticker = ((TdApi.MessageSticker) message.content).sticker; - alternativeText = Td.isAnimated(sticker.format) ? "animated" + sticker.emoji : sticker.emoji; - break; - case TdApi.MessageInvoice.CONSTRUCTOR: { - TdApi.MessageInvoice invoice = (TdApi.MessageInvoice) message.content; - alternativeText = CurrencyUtils.buildAmount(invoice.currency, invoice.totalAmount); - break; - } - case TdApi.MessagePhoto.CONSTRUCTOR: - if (((TdApi.MessagePhoto) message.content).isSecret) - return new ContentPreview(EMOJI_SECRET_PHOTO, R.string.SelfDestructPhoto, formattedText); - break; - case TdApi.MessageVideo.CONSTRUCTOR: - if (((TdApi.MessageVideo) message.content).isSecret) - return new ContentPreview(EMOJI_SECRET_VIDEO, R.string.SelfDestructVideo, formattedText); - break; - case TdApi.MessagePinMessage.CONSTRUCTOR: { - long pinnedMessageId = ((TdApi.MessagePinMessage) message.content).messageId; - TdApi.Message pinnedMessage; - long loadTimeoutMs = additionalMessageLoadTimeoutMs(); - if (pinnedMessageId != 0 && loadTimeoutMs >= 0) { - pinnedMessage = tdlib.getMessageLocally( - message.chatId, pinnedMessageId, loadTimeoutMs - ); - } else { - pinnedMessage = null; - } - if (pinnedMessage != null) { - return new ContentPreview(EMOJI_PIN, getContentPreview(tdlib, chatId, pinnedMessage, allowContent, isChatList)); - } else { - return new ContentPreview(EMOJI_PIN, R.string.ChatContentPinned) - .setRefresher((oldPreview, callback) -> tdlib.getMessage(chatId, pinnedMessageId, remotePinnedMessage -> { - if (remotePinnedMessage != null) { - callback.onContentPreviewChanged(chatId, message.id, new ContentPreview(EMOJI_PIN, getContentPreview(tdlib, chatId, remotePinnedMessage, allowContent, isChatList)), oldPreview); - } else { - callback.onContentPreviewNotChanged(chatId, message.id, oldPreview); - } - }), false); - } - } - case TdApi.MessageGameScore.CONSTRUCTOR: { - TdApi.MessageGameScore score = (TdApi.MessageGameScore) message.content; - long timeoutMs = additionalMessageLoadTimeoutMs(); - TdApi.Message gameMessage = timeoutMs >= 0 ? - tdlib.getMessageLocally( - message.chatId, score.gameMessageId, - timeoutMs - ) : null; - String gameTitle = gameMessage != null && gameMessage.content.getConstructor() == TdApi.MessageGame.CONSTRUCTOR ? TD.getGameName(((TdApi.MessageGame) gameMessage.content).game, false) : null; - if (!StringUtils.isEmpty(gameTitle)) { - return new ContentPreview(EMOJI_GAME, 0, Lang.plural(message.isOutgoing ? R.string.game_ActionYouScoredInGame : R.string.game_ActionScoredInGame, score.score, gameTitle), true); - } else { - return new ContentPreview(EMOJI_GAME, 0, Lang.plural(message.isOutgoing ? R.string.game_ActionYouScored : R.string.game_ActionScored, score.score), true) - .setRefresher(gameMessage != null ? null : - (oldPreview, callback) -> tdlib.getMessage(message.chatId, score.gameMessageId, remoteGameMessage -> { - if (remoteGameMessage != null && remoteGameMessage.content.getConstructor() == TdApi.MessageGame.CONSTRUCTOR) { - String newGameTitle = TD.getGameName(((TdApi.MessageGame) remoteGameMessage.content).game, false); - if (!StringUtils.isEmpty(newGameTitle)) { - callback.onContentPreviewChanged(message.chatId, message.id, new ContentPreview(EMOJI_GAME, 0, Lang.plural(message.isOutgoing ? R.string.game_ActionYouScoredInGame : R.string.game_ActionScoredInGame, score.score, newGameTitle), true), oldPreview); - return; - } - } - callback.onContentPreviewNotChanged(message.chatId, message.id, oldPreview); - }), false); - } - } - case TdApi.MessageProximityAlertTriggered.CONSTRUCTOR: { - TdApi.MessageProximityAlertTriggered alert = (TdApi.MessageProximityAlertTriggered) message.content; - if (tdlib.isSelfSender(alert.travelerId)) { - return new ContentPreview(EMOJI_LOCATION, 0, Lang.plural(alert.distance >= 1000 ? R.string.ChatContentProximityYouKm : R.string.ChatContentProximityYouM, alert.distance >= 1000 ? alert.distance / 1000 : alert.distance, tdlib.senderName(alert.watcherId, true)), true); - } else if (tdlib.isSelfSender(alert.watcherId)) { - return new ContentPreview(EMOJI_LOCATION, 0, Lang.plural(alert.distance >= 1000 ? R.string.ChatContentProximityFromYouKm : R.string.ChatContentProximityFromYouM, alert.distance >= 1000 ? alert.distance / 1000 : alert.distance, tdlib.senderName(alert.travelerId, true)), true); - } else { - return new ContentPreview(EMOJI_LOCATION, 0, Lang.plural(alert.distance >= 1000 ? R.string.ChatContentProximityKm : R.string.ChatContentProximityM, alert.distance >= 1000 ? alert.distance / 1000 : alert.distance, tdlib.senderName(alert.travelerId, true), tdlib.senderName(alert.watcherId, true)), true); - } - } - case TdApi.MessageVideoChatStarted.CONSTRUCTOR: { - if (message.isChannelPost) { - return new ContentPreview(EMOJI_CALL, message.isOutgoing ? R.string.ChatContentLiveStreamStarted_outgoing : R.string.ChatContentLiveStreamStarted); - } else { - return new ContentPreview(EMOJI_CALL, message.isOutgoing ? R.string.ChatContentVoiceChatStarted_outgoing : R.string.ChatContentVoiceChatStarted); - } - } - case TdApi.MessageVideoChatEnded.CONSTRUCTOR: { - TdApi.MessageVideoChatEnded videoChatOrLiveStream = (TdApi.MessageVideoChatEnded) message.content; - if (message.isChannelPost) { - return new ContentPreview(EMOJI_CALL_END, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentLiveStreamFinished_outgoing : R.string.ChatContentLiveStreamFinished, Lang.getCallDuration(videoChatOrLiveStream.duration)), true); - } else { - return new ContentPreview(EMOJI_CALL_END, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentVoiceChatFinished_outgoing : R.string.ChatContentVoiceChatFinished, Lang.getCallDuration(videoChatOrLiveStream.duration)), true); - } - } - case TdApi.MessageVideoChatScheduled.CONSTRUCTOR: { - TdApi.MessageVideoChatScheduled event = (TdApi.MessageVideoChatScheduled) message.content; - return new ContentPreview(EMOJI_CALL, 0, Lang.getString(message.isChannelPost ? R.string.LiveStreamScheduledOn : R.string.VideoChatScheduledFor, Lang.getMessageTimestamp(event.startDate, TimeUnit.SECONDS)), true); - } - case TdApi.MessageInviteVideoChatParticipants.CONSTRUCTOR: { - TdApi.MessageInviteVideoChatParticipants info = (TdApi.MessageInviteVideoChatParticipants) message.content; - if (message.isChannelPost) { - if (info.userIds.length == 1) { - long userId = info.userIds[0]; - if (tdlib.isSelfUserId(userId)) { - return new ContentPreview(EMOJI_GROUP_INVITE, R.string.ChatContentLiveStreamInviteYou); - } else { - return new ContentPreview(EMOJI_GROUP_INVITE, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentLiveStreamInvite_outgoing : R.string.ChatContentLiveStreamInvite, tdlib.cache().userName(userId)), true); - } - } else { - return new ContentPreview(EMOJI_GROUP_INVITE, 0, Lang.plural(message.isOutgoing ? R.string.ChatContentLiveStreamInviteMulti_outgoing : R.string.ChatContentLiveStreamInviteMulti, info.userIds.length), true); - } - } else { - if (info.userIds.length == 1) { - long userId = info.userIds[0]; - if (tdlib.isSelfUserId(userId)) { - return new ContentPreview(EMOJI_GROUP_INVITE, R.string.ChatContentVoiceChatInviteYou); - } else { - return new ContentPreview(EMOJI_GROUP_INVITE, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentVoiceChatInvite_outgoing : R.string.ChatContentVoiceChatInvite, tdlib.cache().userName(userId)), true); - } - } else { - return new ContentPreview(EMOJI_GROUP_INVITE, 0, Lang.plural(message.isOutgoing ? R.string.ChatContentVoiceChatInviteMulti_outgoing : R.string.ChatContentVoiceChatInviteMulti, info.userIds.length), true); - } - } - } - case TdApi.MessageChatAddMembers.CONSTRUCTOR: { - TdApi.MessageChatAddMembers info = (TdApi.MessageChatAddMembers) message.content; - if (info.memberUserIds.length == 1) { - long userId = info.memberUserIds[0]; - if (userId == Td.getSenderUserId(message)) { - if (ChatId.isSupergroup(message.chatId)) { - return new ContentPreview(EMOJI_GROUP, message.isOutgoing ? R.string.ChatContentGroupJoinPublic_outgoing : R.string.ChatContentGroupJoinPublic); - } else { // isReturned - return new ContentPreview(EMOJI_GROUP, message.isOutgoing ? R.string.ChatContentGroupReturn_outgoing : R.string.ChatContentGroupReturn); - } - } else if (tdlib.isSelfUserId(userId)) { - return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupAddYou); - } else { - return new ContentPreview(EMOJI_GROUP, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentGroupAdd_outgoing : R.string.ChatContentGroupAdd, tdlib.cache().userName(userId)), true); - } - } else { - return new ContentPreview(EMOJI_GROUP, 0, Lang.plural(message.isOutgoing ? R.string.ChatContentGroupAddMembers_outgoing : R.string.ChatContentGroupAddMembers, info.memberUserIds.length), true); - } - } - case TdApi.MessageChatDeleteMember.CONSTRUCTOR: { - long userId = ((TdApi.MessageChatDeleteMember) message.content).userId; - if (userId == Td.getSenderUserId(message)) { - return new ContentPreview(EMOJI_GROUP, message.isOutgoing ? R.string.ChatContentGroupLeft_outgoing : R.string.ChatContentGroupLeft); - } else if (tdlib.isSelfUserId(userId)) { - return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupKickYou); - } else { - return new ContentPreview(EMOJI_GROUP, 0, Lang.getString(message.isOutgoing ? R.string.ChatContentGroupKick_outgoing : R.string.ChatContentGroupKick, tdlib.cache().userFirstName(userId)), true); - } - } - case TdApi.MessageChatChangeTitle.CONSTRUCTOR: - alternativeText = ((TdApi.MessageChatChangeTitle) message.content).title; - break; - case TdApi.MessageChatSetMessageAutoDeleteTime.CONSTRUCTOR: - arg1 = ((TdApi.MessageChatSetMessageAutoDeleteTime) message.content).messageAutoDeleteTime; - break; - case TdApi.MessageChatSetTheme.CONSTRUCTOR: - alternativeText = ((TdApi.MessageChatSetTheme) message.content).themeName; - break; - } - ContentPreview.Refresher refresher = null; - if (message.mediaAlbumId != 0 && getCombineMode(message) != COMBINE_MODE_NONE) { - refresher = (oldContent, callback) -> tdlib.getAlbum(message, true, null, localAlbum -> { - if (localAlbum.messages.size() == 1 && !localAlbum.mayHaveMoreItems()) { - callback.onContentPreviewNotChanged(message.chatId, message.id, oldContent); - } else { - ContentPreview newPreview = getAlbumPreview(tdlib, message, localAlbum, allowContent); - if (localAlbum.messages.size() == 1) { - if (newPreview.hasRefresher()) { - newPreview.refreshContent(callback); - } else { - callback.onContentPreviewNotChanged(message.chatId, message.id, oldContent); - } - } else { - callback.onContentPreviewChanged(message.chatId, message.id, newPreview, oldContent); - } - } - }); - } - TdApi.FormattedText argument; - boolean argumentTranslatable; - if (Td.isEmpty(formattedText)) { - argument = new TdApi.FormattedText(alternativeText, null); - argumentTranslatable = alternativeTextTranslatable; - } else { - argument = formattedText; - argumentTranslatable = false; + public static void saveChatFolder (Bundle bundle, String prefix, @Nullable TdApi.ChatFolder chatFolder) { + if (chatFolder == null) { + return; } - ContentPreview preview = getContentPreview(message.content.getConstructor(), tdlib, chatId, message.senderId, null, !message.isChannelPost && message.isOutgoing, isChatList, argument, argumentTranslatable, arg1); - if (preview != null) { - return preview.setRefresher(refresher, true); - } - if (allowContent) { - AtomicBoolean translatable = new AtomicBoolean(false); - String text = buildShortPreview(tdlib, message, true, true, translatable::set); - return new ContentPreview(new TdApi.FormattedText(text, null), translatable.get()); - } else { - return new ContentPreview(null, R.string.YouHaveNewMessage); + bundle.putString(prefix + "_title", chatFolder.title); + if (chatFolder.icon != null) { + bundle.putString(prefix + "_iconName", chatFolder.icon.name); } + bundle.putLongArray(prefix + "_pinnedChatIds", chatFolder.pinnedChatIds); + bundle.putLongArray(prefix + "_includedChatIds", chatFolder.includedChatIds); + bundle.putLongArray(prefix + "_excludedChatIds", chatFolder.excludedChatIds); + bundle.putIntArray(prefix + "_includedChatTypes", includedChatTypes(chatFolder)); + bundle.putIntArray(prefix + "_excludedChatTypes", excludedChatTypes(chatFolder)); } - public static ContentPreview getAlbumPreview (Tdlib tdlib, TdApi.Message message, Tdlib.Album album, boolean allowContent) { - SparseIntArray counters = new SparseIntArray(); - for (TdApi.Message m : album.messages) { - ArrayUtils.increment(counters, m.content.getConstructor()); - } - int textRes; - Emoji emoji; - switch (counters.size() == 1 ? counters.keyAt(0) : 0) { - case TdApi.MessagePhoto.CONSTRUCTOR: - textRes = R.string.xPhotos; - emoji = EMOJI_ALBUM_PHOTOS; - break; - case TdApi.MessageVideo.CONSTRUCTOR: - textRes = R.string.xVideos; - emoji = EMOJI_ALBUM_VIDEOS; - break; - case TdApi.MessageDocument.CONSTRUCTOR: - textRes = R.string.xFiles; - emoji = EMOJI_ALBUM_FILES; - break; - case TdApi.MessageAudio.CONSTRUCTOR: - textRes = R.string.xAudios; - emoji = EMOJI_ALBUM_AUDIO; - break; - default: - textRes = R.string.xMedia; - emoji = EMOJI_ALBUM_MEDIA; - break; + public static @Nullable TdApi.ChatFolder restoreChatFolder (Bundle bundle, String prefix) { + String title = bundle.getString(prefix + "_title"); + if (title == null) { + return null; } - TdApi.Message captionMessage = allowContent ? getAlbumCaptionMessage(tdlib, album.messages) : null; - TdApi.FormattedText formattedCaption = captionMessage != null ? Td.textOrCaption(captionMessage.content) : null; - ContentPreview preview = new ContentPreview(emoji, 0, Td.isEmpty(formattedCaption) ? new TdApi.FormattedText(Lang.plural(textRes, album.messages.size()), null) : formattedCaption, Td.isEmpty(formattedCaption)); - preview.album = album; - if (album.mayHaveMoreItems()) { - preview.setRefresher((oldPreview, callback) -> - tdlib.getAlbum(message, false, album, remoteAlbum -> { - if (remoteAlbum.messages.size() > album.messages.size()) { - callback.onContentPreviewChanged(message.chatId, message.id, getAlbumPreview(tdlib, message, remoteAlbum, allowContent), oldPreview); - } else { - callback.onContentPreviewNotChanged(message.chatId, message.id, oldPreview); - } - }), true - ); + TdApi.ChatFolder chatFolder = newChatFolder(title); + String iconName = bundle.getString(prefix + "_iconName", null); + if (iconName != null) { + chatFolder.icon = new TdApi.ChatFolderIcon(iconName); } - return preview; + chatFolder.pinnedChatIds = bundle.getLongArray(prefix + "_pinnedChatIds"); + chatFolder.includedChatIds = bundle.getLongArray(prefix + "_includedChatIds"); + chatFolder.excludedChatIds = bundle.getLongArray(prefix + "_excludedChatIds"); + int[] includedChatTypes = bundle.getIntArray(prefix + "_includedChatTypes"); + int[] excludedChatTypes = bundle.getIntArray(prefix + "_excludedChatTypes"); + updateIncludedChatTypes(chatFolder, (chatType) -> ArrayUtils.contains(includedChatTypes, chatType)); + updateExcludedChatTypes(chatFolder, (chatType) -> ArrayUtils.contains(excludedChatTypes, chatType)); + return chatFolder; } - public static TdApi.Message getAlbumCaptionMessage (Tdlib tdlib, List messages) { - TdApi.Message captionMessage = null; - for (TdApi.Message message : messages) { - TdApi.FormattedText currentCaption = tdlib.getFormattedText(message); - if (!Td.isEmpty(currentCaption)) { - if (captionMessage != null) { - captionMessage = null; - break; - } else { - captionMessage = message; - } - } - } - return captionMessage; + public static TdApi.ChatFolder newChatFolder () { + return new TdApi.ChatFolder("", null, false, ArrayUtils.EMPTY_LONGS, ArrayUtils.EMPTY_LONGS, ArrayUtils.EMPTY_LONGS, false, false, false, false, false, false, false, false); } - private static ContentPreview getNotificationPinned(int res, int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument, int arg1) { - return getNotificationPinned(res, type, tdlib, chatId, sender, argument, senderName, false, arg1); + public static TdApi.ChatFolder newChatFolder (String title) { + TdApi.ChatFolder chatFolder = newChatFolder(); + chatFolder.title = title; + return chatFolder; } - private static ContentPreview getNotificationPinned(int res, int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument, boolean argumentTranslatable, int arg1) { - String text; - if (StringUtils.isEmpty(argument)) { - try { - text = Lang.formatString(Strings.replaceBoldTokens(Lang.getString(res)).toString(), null, getSenderName(tdlib, sender, senderName)).toString(); - } catch (Throwable t) { - text = Lang.getString(res); - } - } else { - ContentPreview contentPreview = getNotificationPreview(type, tdlib, chatId, sender, senderName, argument, argumentTranslatable, arg1); - String preview = contentPreview != null ? contentPreview.toString() : null; - if (StringUtils.isEmpty(preview)) { - preview = argument; - } - try { - text = Lang.formatString(Strings.replaceBoldTokens(Lang.getString(R.string.ActionPinnedText)).toString(), null, getSenderName(tdlib, sender, senderName), preview).toString(); - } catch (Throwable t) { - text = Lang.getString(R.string.ActionPinnedText); - } - } - // TODO icon? - return new ContentPreview(null, 0, new TdApi.FormattedText(text, null), true, true, EMOJI_PIN); + public static TdApi.ChatFolder newChatFolder (long[] includedChatIds) { + TdApi.ChatFolder chatFolder = newChatFolder(); + chatFolder.includedChatIds = includedChatIds; + return chatFolder; } - public static ContentPreview getNotificationPreview (Tdlib tdlib, long chatId, TdApi.NotificationTypeNewPushMessage push, boolean allowContent) { - switch (push.content.getConstructor()) { - case TdApi.PushMessageContentHidden.CONSTRUCTOR: - if (((TdApi.PushMessageContentHidden) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedNoText, TdApi.MessageText.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); - else - return new ContentPreview(Lang.plural(R.string.xNewMessages, 1), true); - - case TdApi.PushMessageContentText.CONSTRUCTOR: - if (((TdApi.PushMessageContentText) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedNoText, TdApi.MessageText.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentText) push.content).text, 0); - else - return getNotificationPreview(TdApi.MessageText.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentText) push.content).text, 0); - - case TdApi.PushMessageContentMessageForwards.CONSTRUCTOR: - return new ContentPreview(Lang.plural(R.string.xForwards, ((TdApi.PushMessageContentMessageForwards) push.content).totalCount), true); - - case TdApi.PushMessageContentPhoto.CONSTRUCTOR: { - String caption = ((TdApi.PushMessageContentPhoto) push.content).caption; - if (((TdApi.PushMessageContentPhoto) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedPhoto, TdApi.MessagePhoto.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, 0); - else if (((TdApi.PushMessageContentPhoto) push.content).isSecret) - return new ContentPreview(EMOJI_SECRET_PHOTO, R.string.SelfDestructPhoto, caption, false); - else - return getNotificationPreview(TdApi.MessagePhoto.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, 0); - } - - case TdApi.PushMessageContentVideo.CONSTRUCTOR: { - String caption = ((TdApi.PushMessageContentVideo) push.content).caption; - if (((TdApi.PushMessageContentVideo) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedVideo, TdApi.MessageVideo.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, 0); - else if (((TdApi.PushMessageContentVideo) push.content).isSecret) - return new ContentPreview(EMOJI_SECRET_VIDEO, R.string.SelfDestructVideo, caption); - else - return getNotificationPreview(TdApi.MessageVideo.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, 0); - } - - case TdApi.PushMessageContentAnimation.CONSTRUCTOR: { - String caption = ((TdApi.PushMessageContentAnimation) push.content).caption; - if (((TdApi.PushMessageContentAnimation) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedGif, TdApi.MessageAnimation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, 0); - else - return getNotificationPreview(TdApi.MessageAnimation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, 0); - } - - case TdApi.PushMessageContentDocument.CONSTRUCTOR: { - TdApi.Document media = ((TdApi.PushMessageContentDocument) push.content).document; - String caption = null; // FIXME server ((TdApi.PushMessageContentDocument) push.content).caption; - if (StringUtils.isEmpty(caption) && media != null) { - caption = media.fileName; - } - if (((TdApi.PushMessageContentDocument) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedFile, TdApi.MessageDocument.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, 0); - else - return getNotificationPreview(TdApi.MessageDocument.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, 0); - } - - case TdApi.PushMessageContentSticker.CONSTRUCTOR: - if (((TdApi.PushMessageContentSticker) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedSticker, TdApi.MessageSticker.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentSticker) push.content).emoji, 0); - else if (((TdApi.PushMessageContentSticker) push.content).sticker != null && Td.isAnimated(((TdApi.PushMessageContentSticker) push.content).sticker.format)) - return getNotificationPreview(TdApi.MessageSticker.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, "animated" + ((TdApi.PushMessageContentSticker) push.content).emoji, 0); - else - return getNotificationPreview(TdApi.MessageSticker.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentSticker) push.content).emoji, 0); + public static void updateIncludedChats (TdApi.ChatFolder chatFolder, Set chatIds) { + updateIncludedChats(chatFolder, null, chatIds); + } - case TdApi.PushMessageContentLocation.CONSTRUCTOR: - if (((TdApi.PushMessageContentLocation) push.content).isLive) { - if (((TdApi.PushMessageContentLocation) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedGeoLive, TdApi.MessageLocation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); - else - return getNotificationPreview(TdApi.MessageLocation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, "live", 0); + public static void updateIncludedChats (TdApi.ChatFolder chatFolder, @Nullable TdApi.ChatFolder originChatFolder, Set chatIds) { + if (chatIds.isEmpty()) { + chatFolder.pinnedChatIds = ArrayUtils.EMPTY_LONGS; + chatFolder.includedChatIds = ArrayUtils.EMPTY_LONGS; + } else { + LongList pinnedChatIds = new LongList(chatIds.size()); + LongList includedChatIds = new LongList(chatIds.size()); + for (long chatId : chatIds) { + if (ArrayUtils.contains(chatFolder.pinnedChatIds, chatId) || (originChatFolder != null && ArrayUtils.contains(originChatFolder.pinnedChatIds, chatId))) { + pinnedChatIds.append(chatId); } else { - if (((TdApi.PushMessageContentLocation) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedGeo, TdApi.MessageLocation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); - else - return getNotificationPreview(TdApi.MessageLocation.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); + includedChatIds.append(chatId); } - - case TdApi.PushMessageContentPoll.CONSTRUCTOR: - if (((TdApi.PushMessageContentPoll) push.content).isPinned) - return getNotificationPinned(((TdApi.PushMessageContentPoll) push.content).isRegular ? R.string.ActionPinnedPoll : R.string.ActionPinnedQuiz, TdApi.MessagePoll.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentPoll) push.content).question, ((TdApi.PushMessageContentPoll) push.content).isRegular ? ARG_NONE : ARG_POLL_QUIZ); - else - return getNotificationPreview(TdApi.MessagePoll.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentPoll) push.content).question, ((TdApi.PushMessageContentPoll) push.content).isRegular ? ARG_NONE : ARG_POLL_QUIZ); - - case TdApi.PushMessageContentAudio.CONSTRUCTOR: { - TdApi.Audio audio = ((TdApi.PushMessageContentAudio) push.content).audio; - String caption = null; // FIXME server ((TdApi.PushMessageContentAudio) push.content).caption; - boolean translatable = false; - if (StringUtils.isEmpty(caption) && audio != null) { - caption = Lang.getString(R.string.ChatContentSong, TD.getTitle(audio), TD.getSubtitle(audio)); - translatable = !hasTitle(audio) || !hasSubtitle(audio); - } - if (((TdApi.PushMessageContentAudio) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedMusic, TdApi.MessageAudio.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, translatable, 0); - else - return getNotificationPreview(TdApi.MessageAudio.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, caption, translatable, 0); - } - - case TdApi.PushMessageContentVideoNote.CONSTRUCTOR: { - String argument = null; - boolean argumentTranslatable = false; - TdApi.VideoNote videoNote = ((TdApi.PushMessageContentVideoNote) push.content).videoNote; - if (videoNote != null && videoNote.duration > 0) { - argument = Lang.getString(R.string.ChatContentVoiceDuration, Lang.getString(R.string.ChatContentRoundVideo), Strings.buildDuration(videoNote.duration)); - argumentTranslatable = true; - } - if (((TdApi.PushMessageContentVideoNote) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedRound, TdApi.MessageVideoNote.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, argument, argumentTranslatable, 0); - else - return getNotificationPreview(TdApi.MessageVideoNote.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, argument, argumentTranslatable, 0); } + chatFolder.pinnedChatIds = pinnedChatIds.get(); + chatFolder.includedChatIds = includedChatIds.get(); + } + chatFolder.excludedChatIds = U.removeAll(chatFolder.excludedChatIds, chatIds); + } - case TdApi.PushMessageContentVoiceNote.CONSTRUCTOR: { - String argument = null; // FIXME server ((TdApi.PushMessageContentVoiceNote) push.content).caption; - boolean argumentTranslatable = false; - if (StringUtils.isEmpty(argument)) { - TdApi.VoiceNote voiceNote = ((TdApi.PushMessageContentVoiceNote) push.content).voiceNote; - if (voiceNote != null && voiceNote.duration > 0) { - argument = Lang.getString(R.string.ChatContentVoiceDuration, Lang.getString(R.string.ChatContentVoice), Strings.buildDuration(voiceNote.duration)); - argumentTranslatable = true; - } - } - if (((TdApi.PushMessageContentVoiceNote) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedVoice, TdApi.MessageVoiceNote.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, argument, argumentTranslatable, 0); - else - return getNotificationPreview(TdApi.MessageVoiceNote.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, argument, argumentTranslatable, 0); - } - - case TdApi.PushMessageContentGame.CONSTRUCTOR: - if (((TdApi.PushMessageContentGame) push.content).isPinned) { - String gameTitle = ((TdApi.PushMessageContentGame) push.content).title; - return getNotificationPinned(StringUtils.isEmpty(gameTitle) ? R.string.ActionPinnedGameNoName : R.string.ActionPinnedGame, TdApi.MessageGame.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, StringUtils.isEmpty(gameTitle) ? null : gameTitle, 0); - } else - return getNotificationPreview(TdApi.MessageGame.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentGame) push.content).title, 0); - - case TdApi.PushMessageContentContact.CONSTRUCTOR: - if (((TdApi.PushMessageContentContact) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedContact, TdApi.MessageContact.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentContact) push.content).name, 0); - else - return getNotificationPreview(TdApi.MessageContact.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentContact) push.content).name, 0); + public static void updateExcludedChats (TdApi.ChatFolder chatFolder, Set chatIds) { + chatFolder.pinnedChatIds = U.removeAll(chatFolder.pinnedChatIds, chatIds); + chatFolder.includedChatIds = U.removeAll(chatFolder.includedChatIds, chatIds); + chatFolder.excludedChatIds = U.toArray(chatIds); + } - case TdApi.PushMessageContentInvoice.CONSTRUCTOR: - if (((TdApi.PushMessageContentInvoice) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedNoText, TdApi.MessageInvoice.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); // TODO - else - return getNotificationPreview(TdApi.MessageInvoice.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentInvoice) push.content).price, 0); - - case TdApi.PushMessageContentScreenshotTaken.CONSTRUCTOR: - return getNotificationPreview(TdApi.MessageScreenshotTaken.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); - - case TdApi.PushMessageContentGameScore.CONSTRUCTOR: - if (((TdApi.PushMessageContentGameScore) push.content).isPinned) - return getNotificationPinned(R.string.ActionPinnedNoText, TdApi.MessageGameScore.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); // TODO - else { - TdApi.PushMessageContentGameScore score = (TdApi.PushMessageContentGameScore) push.content; - String gameTitle = score.title; - if (!StringUtils.isEmpty(gameTitle)) - return new ContentPreview(EMOJI_GAME, 0, Lang.plural(R.string.game_ActionScoredInGame, score.score, gameTitle), true); - else - return new ContentPreview(EMOJI_GAME, 0, Lang.plural(R.string.game_ActionScored, score.score), true); - } + public static TdApi.ChatFolder copyOf (TdApi.ChatFolder folder) { + return new TdApi.ChatFolder( + folder.title, + folder.icon, + folder.isShareable, + folder.pinnedChatIds, + folder.includedChatIds, + folder.excludedChatIds, + folder.excludeMuted, + folder.excludeRead, + folder.excludeArchived, + folder.includeContacts, + folder.includeNonContacts, + folder.includeBots, + folder.includeGroups, + folder.includeChannels + ); + } - case TdApi.PushMessageContentContactRegistered.CONSTRUCTOR: - return getNotificationPreview(TdApi.MessageContactRegistered.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); - - case TdApi.PushMessageContentMediaAlbum.CONSTRUCTOR: { - TdApi.PushMessageContentMediaAlbum album = ((TdApi.PushMessageContentMediaAlbum) push.content); - int mediaTypeCount = 0; - if (album.hasPhotos) - mediaTypeCount++; - if (album.hasVideos) - mediaTypeCount++; - if (album.hasAudios) - mediaTypeCount++; - if (album.hasDocuments) - mediaTypeCount++; - if (mediaTypeCount > 1 || mediaTypeCount == 0) { - return new ContentPreview(EMOJI_ALBUM_MEDIA, 0, Lang.plural(R.string.xMedia, album.totalCount), true); - } else if (album.hasDocuments) { - return new ContentPreview(EMOJI_ALBUM_FILES, 0, Lang.plural(R.string.xFiles, album.totalCount), true); - } else if (album.hasAudios) { - return new ContentPreview(EMOJI_ALBUM_AUDIO, 0, Lang.plural(R.string.xAudios, album.totalCount), true); - } else if (album.hasVideos) { - return new ContentPreview(EMOJI_ALBUM_VIDEOS, 0, Lang.plural(R.string.xVideos, album.totalCount), true); - } else { - return new ContentPreview(EMOJI_ALBUM_PHOTOS, 0, Lang.plural(R.string.xPhotos, album.totalCount), true); - } - } + public static boolean contentEquals (TdApi.ChatFolder lhs, TdApi.ChatFolder rhs) { + if (lhs == rhs) { + return true; + } + if (!ObjectUtils.equals(lhs.title, rhs.title)) return false; + String a = lhs.icon != null ? lhs.icon.name : null; + String b = rhs.icon != null ? rhs.icon.name : null; + return ObjectUtils.equals(a, b) && + lhs.includeContacts == rhs.includeContacts && + lhs.includeNonContacts == rhs.includeNonContacts && + lhs.includeGroups == rhs.includeGroups && + lhs.includeChannels == rhs.includeChannels && + lhs.includeBots == rhs.includeBots && + lhs.excludeMuted == rhs.excludeMuted && + lhs.excludeRead == rhs.excludeRead && + lhs.excludeArchived == rhs.excludeArchived && + lhs.pinnedChatIds.length == rhs.pinnedChatIds.length && + lhs.includedChatIds.length == rhs.includedChatIds.length && + lhs.excludedChatIds.length == rhs.excludedChatIds.length && + Arrays.equals(lhs.pinnedChatIds, rhs.pinnedChatIds) && + U.unmodifiableTreeSetOf(lhs.includedChatIds).equals(U.unmodifiableTreeSetOf(rhs.includedChatIds)) && + U.unmodifiableTreeSetOf(lhs.excludedChatIds).equals(U.unmodifiableTreeSetOf(rhs.excludedChatIds)); + } + + public static int countIncludedChatTypes (@Nullable TdApi.ChatFolder chatFolder) { + if (chatFolder == null) + return 0; + int count = 0; + if (chatFolder.includeContacts) count++; + if (chatFolder.includeNonContacts) count++; + if (chatFolder.includeGroups) count++; + if (chatFolder.includeChannels) count++; + if (chatFolder.includeBots) count++; + return count; + } - case TdApi.PushMessageContentBasicGroupChatCreate.CONSTRUCTOR: - return getNotificationPreview(TdApi.MessageBasicGroupChatCreate.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); + public static int countExcludedChatTypes (@Nullable TdApi.ChatFolder chatFolder) { + if (chatFolder == null) + return 0; + int count = 0; + if (chatFolder.excludeMuted) count++; + if (chatFolder.excludeRead) count++; + if (chatFolder.excludeArchived) count++; + return count; + } - case TdApi.PushMessageContentChatAddMembers.CONSTRUCTOR: { - TdApi.PushMessageContentChatAddMembers info = (TdApi.PushMessageContentChatAddMembers) push.content; - if (info.isReturned) { - return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupReturn); - } else if (info.isCurrentUser) { - return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupAddYou); - } else { - return new ContentPreview(EMOJI_GROUP, 0, Lang.getString(R.string.ChatContentGroupAdd, info.memberName), true); - } - } + public static int[] includedChatTypes (@Nullable TdApi.ChatFolder chatFolder) { + if (chatFolder == null) + return ArrayUtils.EMPTY_INTS; + IntList chatTypes = new IntList(countIncludedChatTypes(chatFolder)); + if (chatFolder.includeContacts) chatTypes.append(R.id.chatType_contact); + if (chatFolder.includeNonContacts) chatTypes.append(R.id.chatType_nonContact); + if (chatFolder.includeGroups) chatTypes.append(R.id.chatType_group); + if (chatFolder.includeChannels) chatTypes.append(R.id.chatType_channel); + if (chatFolder.includeBots) chatTypes.append(R.id.chatType_bot); + return chatTypes.get(); + } - case TdApi.PushMessageContentChatDeleteMember.CONSTRUCTOR: { - TdApi.PushMessageContentChatDeleteMember info = (TdApi.PushMessageContentChatDeleteMember) push.content; - if (info.isLeft) { - return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupLeft); - } else if (info.isCurrentUser) { - return new ContentPreview(EMOJI_GROUP, R.string.ChatContentGroupKickYou); - } else { - return new ContentPreview(EMOJI_GROUP, 0, Lang.getString(R.string.ChatContentGroupKick, info.memberName), true); - } - } + public static int[] excludedChatTypes (@Nullable TdApi.ChatFolder chatFolder) { + if (chatFolder == null) + return ArrayUtils.EMPTY_INTS; + IntList chatTypes = new IntList(countExcludedChatTypes(chatFolder)); + if (chatFolder.excludeMuted) chatTypes.append(R.id.chatType_muted); + if (chatFolder.excludeRead) chatTypes.append(R.id.chatType_read); + if (chatFolder.excludeArchived) chatTypes.append(R.id.chatType_archived); + return chatTypes.get(); + } - case TdApi.PushMessageContentChatJoinByLink.CONSTRUCTOR: - return getNotificationPreview(TdApi.MessageChatJoinByLink.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); - case TdApi.PushMessageContentChatJoinByRequest.CONSTRUCTOR: - return getNotificationPreview(TdApi.MessageChatJoinByRequest.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); - case TdApi.PushMessageContentRecurringPayment.CONSTRUCTOR: - return getNotificationPreview(TdApi.MessageInvoice.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentRecurringPayment) push.content).amount, ARG_RECURRING_PAYMENT); + public static void updateIncludedChatTypes (TdApi.ChatFolder chatFolder, Set chatTypes) { + updateIncludedChatTypes(chatFolder, chatTypes::contains); + } - case TdApi.PushMessageContentChatChangePhoto.CONSTRUCTOR: - return getNotificationPreview(TdApi.MessageChatChangePhoto.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, null, 0); // FIXME Server: Missing isRemoved + public static void updateIncludedChatTypes (TdApi.ChatFolder chatFolder, Filter filter) { + chatFolder.includeContacts = filter.accept(R.id.chatType_contact); + chatFolder.includeNonContacts = filter.accept(R.id.chatType_nonContact); + chatFolder.includeGroups = filter.accept(R.id.chatType_group); + chatFolder.includeChannels = filter.accept(R.id.chatType_channel); + chatFolder.includeBots = filter.accept(R.id.chatType_bot); + } - case TdApi.PushMessageContentChatChangeTitle.CONSTRUCTOR: - return getNotificationPreview(TdApi.MessageChatChangeTitle.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentChatChangeTitle) push.content).title, 0); + public static void updateExcludedChatTypes (TdApi.ChatFolder chatFolder, Set chatTypes) { + updateExcludedChatTypes(chatFolder, chatTypes::contains); + } - case TdApi.PushMessageContentChatSetTheme.CONSTRUCTOR: - return getNotificationPreview(TdApi.MessageChatSetTheme.CONSTRUCTOR, tdlib, chatId, push.senderId, push.senderName, ((TdApi.PushMessageContentChatSetTheme) push.content).themeName, 0); - } - throw new AssertionError(push.content); + public static void updateExcludedChatTypes (TdApi.ChatFolder chatFolder, Filter filter) { + chatFolder.excludeMuted = filter.accept(R.id.chatType_muted); + chatFolder.excludeRead = filter.accept(R.id.chatType_read); + chatFolder.excludeArchived = filter.accept(R.id.chatType_archived); } - public static final class Emoji { - public final @NonNull String textRepresentation; - public final @DrawableRes int iconRepresentation; + public static final int[] CHAT_TYPES = { + R.id.chatType_contact, + R.id.chatType_nonContact, + R.id.chatType_group, + R.id.chatType_channel, + R.id.chatType_bot, + R.id.chatType_muted, + R.id.chatType_read, + R.id.chatType_archived + }; - public Emoji (@NonNull String textRepresentation, @DrawableRes int iconRepresentation) { - this.textRepresentation = textRepresentation; - this.iconRepresentation = iconRepresentation; - } + public static final int[] CHAT_TYPES_TO_INCLUDE = { + R.id.chatType_contact, + R.id.chatType_nonContact, + R.id.chatType_group, + R.id.chatType_channel, + R.id.chatType_bot + }; - @NonNull - @Override - public String toString () { - return textRepresentation; - } - - public Emoji toNewEmoji (String newEmoji) { - return StringUtils.equalsOrBothEmpty(this.textRepresentation, newEmoji) ? this : new Emoji(newEmoji, iconRepresentation); - } - } - - public static final Emoji - EMOJI_PHOTO = new Emoji("\uD83D\uDDBC", R.drawable.baseline_camera_alt_16), // "\uD83D\uDCF7" - EMOJI_VIDEO = new Emoji("\uD83C\uDFA5", R.drawable.baseline_videocam_16), // "\uD83D\uDCF9" - EMOJI_ROUND_VIDEO = new Emoji("\uD83D\uDCF9", R.drawable.deproko_baseline_msg_video_16), - EMOJI_SECRET_PHOTO = new Emoji("\uD83D\uDD25", R.drawable.deproko_baseline_whatshot_16), - EMOJI_SECRET_VIDEO = new Emoji("\uD83D\uDD25", R.drawable.deproko_baseline_whatshot_16), - EMOJI_LINK = new Emoji("\uD83D\uDD17", R.drawable.baseline_link_16), - EMOJI_GAME = new Emoji("\uD83C\uDFAE", R.drawable.baseline_videogame_asset_16), - EMOJI_GROUP = new Emoji("\uD83D\uDC65", R.drawable.baseline_group_16), - EMOJI_THEME = new Emoji("\uD83C\uDFA8", R.drawable.baseline_palette_16), - EMOJI_GROUP_INVITE = new Emoji("\uD83D\uDC65", R.drawable.baseline_group_add_16), - EMOJI_CHANNEL = new Emoji("\uD83D\uDCE2", R.drawable.baseline_bullhorn_16), // "\uD83D\uDCE3" - EMOJI_FILE = new Emoji("\uD83D\uDCCE", R.drawable.baseline_insert_drive_file_16), - EMOJI_AUDIO = new Emoji("\uD83C\uDFB5", R.drawable.baseline_music_note_16), - EMOJI_CONTACT = new Emoji("\uD83D\uDC64", R.drawable.baseline_person_16), - EMOJI_POLL = new Emoji("\uD83D\uDCCA", R.drawable.baseline_poll_16), - EMOJI_QUIZ = new Emoji("\u2753", R.drawable.baseline_help_16), - EMOJI_VOICE = new Emoji("\uD83C\uDFA4", R.drawable.baseline_mic_16), - EMOJI_GIF = new Emoji("\uD83D\uDC7E", R.drawable.deproko_baseline_gif_filled_16), - EMOJI_LOCATION = new Emoji("\uD83D\uDCCC", R.drawable.baseline_gps_fixed_16), - EMOJI_INVOICE = new Emoji("\uD83D\uDCB8", R.drawable.baseline_receipt_16), - EMOJI_USER_JOINED = new Emoji("\uD83C\uDF89", R.drawable.baseline_party_popper_16), - EMOJI_SCREENSHOT = new Emoji("\uD83D\uDCF8", R.drawable.round_warning_16), - EMOJI_PIN = new Emoji("\uD83D\uDCCC", R.drawable.deproko_baseline_pin_16), - EMOJI_ALBUM_MEDIA = new Emoji("\uD83D\uDDBC", R.drawable.baseline_collections_16), - EMOJI_ALBUM_PHOTOS = new Emoji("\uD83D\uDDBC", R.drawable.baseline_collections_16), - EMOJI_ALBUM_AUDIO = new Emoji("\uD83C\uDFB5", R.drawable.ivanliana_baseline_audio_collections_16), - EMOJI_ALBUM_FILES = new Emoji("\uD83D\uDCCE", R.drawable.ivanliana_baseline_file_collections_16), - EMOJI_ALBUM_VIDEOS = new Emoji("\uD83C\uDFA5", R.drawable.ivanliana_baseline_video_collections_16), - EMOJI_FORWARD = new Emoji("\u21A9", R.drawable.baseline_share_arrow_16), - EMOJI_ABACUS = new Emoji("\uD83E\uDDEE", R.drawable.baseline_bar_chart_24), - EMOJI_DART = new Emoji("\uD83C\uDFAF", R.drawable.baseline_gps_fixed_16), - EMOJI_DICE = new Emoji("\uD83C\uDFB2", R.drawable.baseline_casino_16), - EMOJI_DICE_1 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_1_16), - EMOJI_DICE_2 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_2_16), - EMOJI_DICE_3 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_3_16), - EMOJI_DICE_4 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_4_16), - EMOJI_DICE_5 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_5_16), - EMOJI_DICE_6 = new Emoji("\uD83C\uDFB2", R.drawable.belledeboheme_baseline_dice_6_16), - EMOJI_CALL = new Emoji("\uD83D\uDCDE", R.drawable.baseline_call_16), - EMOJI_TIMER = new Emoji("\u23F2", R.drawable.baseline_timer_16), - EMOJI_TIMER_OFF = new Emoji("\u23F2", R.drawable.baseline_timer_off_16), - EMOJI_CALL_END = new Emoji("\uD83D\uDCDE", R.drawable.baseline_call_end_16), - EMOJI_CALL_MISSED = new Emoji("\u260E", R.drawable.baseline_call_missed_18), - EMOJI_CALL_DECLINED = new Emoji("\u260E", R.drawable.baseline_call_received_18), - EMOJI_WARN = new Emoji("\u26A0", R.drawable.baseline_warning_18), - EMOJI_INFO = new Emoji("\u2139", R.drawable.baseline_info_18), - EMOJI_ERROR = new Emoji("\u2139", R.drawable.baseline_error_18), - EMOJI_LOCK = new Emoji("\uD83D\uDD12", R.drawable.baseline_lock_16) - ; - - private static ContentPreview getNotificationPreview (@TdApi.MessageContent.Constructors int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument, boolean argumentTranslatable, int arg1) { - return getContentPreview(type, tdlib, chatId, sender, senderName, tdlib.isSelfSender(sender), false, new TdApi.FormattedText(argument, null), argumentTranslatable, arg1); - } - - private static ContentPreview getNotificationPreview (@TdApi.MessageContent.Constructors int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, String argument, int arg1) { - return getContentPreview(type, tdlib, chatId, sender, senderName, tdlib.isSelfSender(sender), false, new TdApi.FormattedText(argument, null), false, arg1); - } - - private static String getSenderName (Tdlib tdlib, TdApi.MessageSender sender, String senderName) { - return StringUtils.isEmpty(senderName) ? tdlib.senderName(sender, true) : senderName; - } - - private static ContentPreview getContentPreview (@TdApi.MessageContent.Constructors int type, Tdlib tdlib, long chatId, TdApi.MessageSender sender, String senderName, boolean isOutgoing, boolean isChatsList, TdApi.FormattedText formattedArgument, boolean argumentTranslatable, int arg1) { - switch (type) { - case TdApi.MessageText.CONSTRUCTOR: - return new ContentPreview(arg1 == ARG_TRUE ? EMOJI_LINK : null, R.string.YouHaveNewMessage, formattedArgument, argumentTranslatable); - case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: - return new ContentPreview(null, R.string.YouHaveNewMessage, formattedArgument, argumentTranslatable); - case TdApi.MessagePhoto.CONSTRUCTOR: - return new ContentPreview(EMOJI_PHOTO, R.string.ChatContentPhoto, formattedArgument, argumentTranslatable); - case TdApi.MessageVideo.CONSTRUCTOR: - return new ContentPreview(EMOJI_VIDEO, R.string.ChatContentVideo, formattedArgument, argumentTranslatable); - case TdApi.MessageDocument.CONSTRUCTOR: - return new ContentPreview(EMOJI_FILE, R.string.ChatContentFile, formattedArgument, argumentTranslatable); - case TdApi.MessageAudio.CONSTRUCTOR: - return new ContentPreview(EMOJI_AUDIO, 0, formattedArgument, argumentTranslatable); // FIXME: does it need a placeholder or argument is always non-null? - case TdApi.MessageContact.CONSTRUCTOR: - return new ContentPreview(EMOJI_CONTACT, R.string.AttachContact, formattedArgument, argumentTranslatable); - case TdApi.MessagePoll.CONSTRUCTOR: - if (arg1 == ARG_POLL_QUIZ) - return new ContentPreview(EMOJI_QUIZ, R.string.Quiz, formattedArgument, argumentTranslatable); - else - return new ContentPreview(EMOJI_POLL, R.string.Poll, formattedArgument, argumentTranslatable); - case TdApi.MessageVoiceNote.CONSTRUCTOR: - return new ContentPreview(EMOJI_VOICE, R.string.ChatContentVoice, formattedArgument, argumentTranslatable); - case TdApi.MessageVideoNote.CONSTRUCTOR: - return new ContentPreview(EMOJI_ROUND_VIDEO, R.string.ChatContentRoundVideo, formattedArgument, argumentTranslatable); - case TdApi.MessageAnimation.CONSTRUCTOR: - return new ContentPreview(EMOJI_GIF, R.string.ChatContentAnimation, formattedArgument, argumentTranslatable); - case TdApi.MessageLocation.CONSTRUCTOR: - return new ContentPreview(EMOJI_LOCATION, "live".equals(Td.getText(formattedArgument)) ? R.string.AttachLiveLocation : R.string.Location); - case TdApi.MessageSticker.CONSTRUCTOR: { - String emoji = Td.getText(formattedArgument); - boolean isAnimated = false; - if (emoji != null && emoji.startsWith("animated")) { - emoji = emoji.substring("animated".length()); - isAnimated = true; - } - return new ContentPreview(StringUtils.isEmpty(emoji) ? null : new Emoji(emoji, 0), isAnimated && !isChatsList ? R.string.AnimatedSticker : R.string.Sticker); - } - case TdApi.MessageScreenshotTaken.CONSTRUCTOR: - if (isOutgoing) - return new ContentPreview(EMOJI_SCREENSHOT, R.string.YouTookAScreenshot); - else if (isChatsList) - return new ContentPreview(EMOJI_SCREENSHOT, R.string.ChatContentScreenshot); - else - return new ContentPreview(EMOJI_SCREENSHOT, 0, Lang.getString(R.string.XTookAScreenshot, getSenderName(tdlib, sender, senderName)), true); - case TdApi.MessageGame.CONSTRUCTOR: - return new ContentPreview(EMOJI_GAME, 0, Lang.getString(ChatId.isMultiChat(chatId) ? (isOutgoing ? R.string.NotificationGame_group_outgoing : R.string.NotificationGame_group) : (isOutgoing ? R.string.NotificationGame_outgoing : R.string.NotificationGame), Td.getText(formattedArgument)), true); - case TdApi.MessageInvoice.CONSTRUCTOR: - if (arg1 == ARG_RECURRING_PAYMENT) { - return new ContentPreview(EMOJI_INVOICE, R.string.RecurringPayment, Td.isEmpty(formattedArgument) ? null : Lang.getString(R.string.PaidX, Td.getText(formattedArgument)), true); - } else { - return new ContentPreview(EMOJI_INVOICE, R.string.Invoice, Td.isEmpty(formattedArgument) ? null : Lang.getString(R.string.InvoiceFor, Td.getText(formattedArgument)), true); - } - case TdApi.MessageContactRegistered.CONSTRUCTOR: - return new ContentPreview(EMOJI_USER_JOINED, 0, Lang.getString(R.string.NotificationContactJoined, getSenderName(tdlib, sender, senderName)), true); - case TdApi.MessageSupergroupChatCreate.CONSTRUCTOR: - if (tdlib.isChannel(chatId)) - return new ContentPreview(EMOJI_CHANNEL, R.string.ActionCreateChannel); - else - return new ContentPreview(EMOJI_GROUP, isOutgoing ? R.string.ChatContentGroupCreate_outgoing : R.string.ChatContentGroupCreate); - case TdApi.MessageBasicGroupChatCreate.CONSTRUCTOR: - return new ContentPreview(EMOJI_GROUP, isOutgoing ? R.string.ChatContentGroupCreate_outgoing : R.string.ChatContentGroupCreate); - case TdApi.MessageChatJoinByLink.CONSTRUCTOR: - return new ContentPreview(EMOJI_GROUP, isOutgoing ? R.string.ChatContentGroupJoin_outgoing : R.string.ChatContentGroupJoin); - case TdApi.MessageChatJoinByRequest.CONSTRUCTOR: - return new ContentPreview(EMOJI_GROUP, isOutgoing ? R.string.ChatContentGroupAccept_outgoing : R.string.ChatContentGroupAccept); - case TdApi.MessageChatChangePhoto.CONSTRUCTOR: - if (tdlib.isChannel(chatId)) - return new ContentPreview(EMOJI_PHOTO, R.string.ActionChannelChangedPhoto); - else - return new ContentPreview(EMOJI_PHOTO, isOutgoing ? R.string.ChatContentGroupPhoto_outgoing : R.string.ChatContentGroupPhoto); - case TdApi.MessageChatDeletePhoto.CONSTRUCTOR: - if (tdlib.isChannel(chatId)) - return new ContentPreview(EMOJI_CHANNEL, R.string.ActionChannelRemovedPhoto); - else - return new ContentPreview(EMOJI_GROUP, isOutgoing ? R.string.ChatContentGroupPhotoRemove_outgoing : R.string.ChatContentGroupPhotoRemove); - case TdApi.MessageChatChangeTitle.CONSTRUCTOR: - if (tdlib.isChannel(chatId)) - return new ContentPreview(EMOJI_CHANNEL, 0, Lang.getString(R.string.ActionChannelChangedTitleTo, Td.getText(formattedArgument)), true); - else - return new ContentPreview(EMOJI_GROUP, 0, Lang.getString(isOutgoing ? R.string.ChatContentGroupName_outgoing : R.string.ChatContentGroupName, Td.getText(formattedArgument)), true); - case TdApi.MessageChatSetTheme.CONSTRUCTOR: - if (StringUtils.isEmpty(formattedArgument.text)) { - if (isOutgoing) - return new ContentPreview(EMOJI_THEME, R.string.ChatContentThemeDisabled_outgoing); - else - return new ContentPreview(EMOJI_THEME, R.string.ChatContentThemeDisabled); - } else { - if (isOutgoing) - return new ContentPreview(EMOJI_THEME, 0, toFormattedText(Lang.getStringBold(R.string.ChatContentThemeSet_outgoing, formattedArgument.text), true)); - else - return new ContentPreview(EMOJI_THEME, 0, toFormattedText(Lang.getStringBold(R.string.ChatContentThemeSet, formattedArgument.text), true)); - } - case TdApi.MessageChatSetMessageAutoDeleteTime.CONSTRUCTOR: { - if (arg1 > 0) { - final int secondsRes, minutesRes, hoursRes, daysRes, weeksRes, monthsRes; - if (ChatId.isUserChat(chatId)) { - secondsRes = R.string.ChatContentTtlSeconds; - minutesRes = R.string.ChatContentTtlMinutes; - hoursRes = R.string.ChatContentTtlHours; - daysRes = R.string.ChatContentTtlDays; - weeksRes = R.string.ChatContentTtlWeeks; - monthsRes = R.string.ChatContentTtlMonths; - } else if (tdlib.isChannel(chatId)) { - secondsRes = R.string.ChatContentChannelTtlSeconds; - minutesRes = R.string.ChatContentChannelTtlMinutes; - hoursRes = R.string.ChatContentChannelTtlHours; - daysRes = R.string.ChatContentChannelTtlDays; - weeksRes = R.string.ChatContentChannelTtlWeeks; - monthsRes = R.string.ChatContentChannelTtlMonths; - } else { - secondsRes = R.string.ChatContentGroupTtlSeconds; - minutesRes = R.string.ChatContentGroupTtlMinutes; - hoursRes = R.string.ChatContentGroupTtlHours; - daysRes = R.string.ChatContentGroupTtlDays; - weeksRes = R.string.ChatContentGroupTtlWeeks; - monthsRes = R.string.ChatContentGroupTtlMonths; - } - final CharSequence text = Lang.pluralDuration(arg1, TimeUnit.SECONDS, secondsRes, minutesRes, hoursRes, daysRes, weeksRes, monthsRes); - return new ContentPreview(EMOJI_TIMER, 0, toFormattedText(text, false), true); - } else { - final int stringRes; - if (ChatId.isUserChat(chatId)) { - stringRes = R.string.ChatContentTtlOff; - } else if (tdlib.isChannel(chatId)) { - stringRes = R.string.ChatContentChannelTtlOff; - } else { - stringRes = R.string.ChatContentGroupTtlOff; - } - return new ContentPreview(EMOJI_TIMER_OFF, stringRes); - } - } - case TdApi.MessageDice.CONSTRUCTOR: { - String diceEmoji = !Td.isEmpty(formattedArgument) && tdlib.isDiceEmoji(formattedArgument.text) ? formattedArgument.text : TD.EMOJI_DICE.textRepresentation; - if (TD.EMOJI_DART.textRepresentation.equals(diceEmoji)) { - return new ContentPreview(EMOJI_DART, getDartRes(arg1)); - } - if (TD.EMOJI_DICE.textRepresentation.equals(diceEmoji)) { - if (arg1 >= 1 && arg1 <= 6) { - switch (arg1) { - case 1: - return new ContentPreview(EMOJI_DICE_1, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); - case 2: - return new ContentPreview(EMOJI_DICE_2, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); - case 3: - return new ContentPreview(EMOJI_DICE_3, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); - case 4: - return new ContentPreview(EMOJI_DICE_4, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); - case 5: - return new ContentPreview(EMOJI_DICE_5, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); - case 6: - return new ContentPreview(EMOJI_DICE_6, 0, Lang.plural(R.string.ChatContentDiceRolled, arg1), true); - } - } - return new ContentPreview(EMOJI_DICE, R.string.ChatContentDice); - } - return new ContentPreview(new Emoji(diceEmoji, 0), 0); - } - case TdApi.MessageExpiredPhoto.CONSTRUCTOR: - return new ContentPreview(EMOJI_SECRET_PHOTO, R.string.AttachPhotoExpired); - case TdApi.MessageExpiredVideo.CONSTRUCTOR: - return new ContentPreview(EMOJI_SECRET_VIDEO, R.string.AttachVideoExpired); - case TdApi.MessageCall.CONSTRUCTOR: - switch (arg1) { - case ARG_CALL_DECLINED: - return new ContentPreview(EMOJI_CALL_DECLINED, isOutgoing ? R.string.OutgoingCall : R.string.CallMessageIncomingDeclined); - case ARG_CALL_MISSED: - return new ContentPreview(EMOJI_CALL_MISSED, isOutgoing ? R.string.CallMessageOutgoingMissed : R.string.MissedCall); - default: - if (arg1 > 0) { - return new ContentPreview(EMOJI_CALL, 0, Lang.getString(R.string.ChatContentCallWithDuration, Lang.getString(isOutgoing ? R.string.OutgoingCall : R.string.IncomingCall), Lang.getDurationFull(arg1)), true); - } else { - return new ContentPreview(EMOJI_CALL, isOutgoing ? R.string.OutgoingCall : R.string.IncomingCall); - } - } - case TdApi.MessageGiftedPremium.CONSTRUCTOR: // TODO - case TdApi.MessageChatAddMembers.CONSTRUCTOR: - case TdApi.MessageChatDeleteMember.CONSTRUCTOR: - case TdApi.MessageChatUpgradeFrom.CONSTRUCTOR: - case TdApi.MessageChatUpgradeTo.CONSTRUCTOR: - case TdApi.MessageCustomServiceAction.CONSTRUCTOR: - case TdApi.MessageGameScore.CONSTRUCTOR: - case TdApi.MessagePassportDataReceived.CONSTRUCTOR: - case TdApi.MessagePassportDataSent.CONSTRUCTOR: - case TdApi.MessagePaymentSuccessful.CONSTRUCTOR: - case TdApi.MessagePaymentSuccessfulBot.CONSTRUCTOR: - case TdApi.MessagePinMessage.CONSTRUCTOR: - case TdApi.MessageVenue.CONSTRUCTOR: - case TdApi.MessageWebsiteConnected.CONSTRUCTOR: - case TdApi.MessageUnsupported.CONSTRUCTOR: - case TdApi.MessageProximityAlertTriggered.CONSTRUCTOR: - case TdApi.MessageInviteVideoChatParticipants.CONSTRUCTOR: - case TdApi.MessageVideoChatStarted.CONSTRUCTOR: - case TdApi.MessageVideoChatEnded.CONSTRUCTOR: - case TdApi.MessageVideoChatScheduled.CONSTRUCTOR: - case TdApi.MessageWebAppDataReceived.CONSTRUCTOR: - case TdApi.MessageWebAppDataSent.CONSTRUCTOR: - break; + public static final int[] CHAT_TYPES_TO_EXCLUDE = { + R.id.chatType_muted, + R.id.chatType_read, + R.id.chatType_archived + }; + + public static @StringRes int chatTypeName (@IdRes int chatType) { + if (chatType == R.id.chatType_contact) return R.string.CategoryContacts; + if (chatType == R.id.chatType_nonContact) return R.string.CategoryNonContacts; + if (chatType == R.id.chatType_group) return R.string.CategoryGroups; + if (chatType == R.id.chatType_channel) return R.string.CategoryChannels; + if (chatType == R.id.chatType_bot) return R.string.CategoryBots; + if (chatType == R.id.chatType_muted) return R.string.CategoryMuted; + if (chatType == R.id.chatType_read) return R.string.CategoryRead; + if (chatType == R.id.chatType_archived) return R.string.CategoryArchived; + throw new IllegalArgumentException(); + } + + public static @DrawableRes int chatTypeIcon16 (@IdRes int chatType) { + if (chatType == R.id.chatType_contact) return R.drawable.baseline_account_circle_16; + if (chatType == R.id.chatType_nonContact) return R.drawable.baseline_help_16; + if (chatType == R.id.chatType_group) return R.drawable.baseline_group_16; + if (chatType == R.id.chatType_channel) return R.drawable.baseline_bullhorn_16; + if (chatType == R.id.chatType_bot) return R.drawable.deproko_baseline_bots_16; + if (chatType == R.id.chatType_muted) return R.drawable.baseline_notifications_off_16; + if (chatType == R.id.chatType_read) return R.drawable.andrejsharapov_baseline_message_check_16; + if (chatType == R.id.chatType_archived) return R.drawable.baseline_archive_16; + throw new IllegalArgumentException(); + } + + public static @DrawableRes int chatTypeIcon24 (@IdRes int chatType) { + if (chatType == R.id.chatType_contact) return R.drawable.baseline_account_circle_24; + if (chatType == R.id.chatType_nonContact) return R.drawable.baseline_help_24; + if (chatType == R.id.chatType_group) return R.drawable.baseline_group_24; + if (chatType == R.id.chatType_channel) return R.drawable.baseline_bullhorn_24; + if (chatType == R.id.chatType_bot) return R.drawable.deproko_baseline_bots_24; + if (chatType == R.id.chatType_muted) return R.drawable.baseline_notifications_off_24; + if (chatType == R.id.chatType_read) return R.drawable.andrejsharapov_baseline_message_check_24; + if (chatType == R.id.chatType_archived) return R.drawable.baseline_archive_24; + throw new IllegalArgumentException(); + } + + public static int chatTypeAccentColorId (@IdRes int chatType) { + if (chatType == R.id.chatType_contact) return TdlibAccentColor.BuiltInId.BLUE; + if (chatType == R.id.chatType_nonContact) return TdlibAccentColor.BuiltInId.CYAN; + if (chatType == R.id.chatType_group) return TdlibAccentColor.BuiltInId.GREEN; + if (chatType == R.id.chatType_channel) return TdlibAccentColor.BuiltInId.ORANGE; + if (chatType == R.id.chatType_bot) return TdlibAccentColor.BuiltInId.RED; + if (chatType == R.id.chatType_muted) return TdlibAccentColor.BuiltInId.PINK; + if (chatType == R.id.chatType_read) return TdlibAccentColor.BuiltInId.BLUE; + if (chatType == R.id.chatType_archived) return TdlibAccentColor.InternalId.ARCHIVE; + throw new IllegalArgumentException(); + } + + public static @Nullable TdApi.ChatFolderIcon chatTypeIcon (@IdRes int chatType) { + if (chatType == R.id.chatType_contact || chatType == R.id.chatType_nonContact) { + return new TdApi.ChatFolderIcon("Private"); + } + if (chatType == R.id.chatType_group) { + return new TdApi.ChatFolderIcon( "Groups"); + } + if (chatType == R.id.chatType_channel) { + return new TdApi.ChatFolderIcon("Channels"); + } + if (chatType == R.id.chatType_bot) { + return new TdApi.ChatFolderIcon("Bots"); } return null; } - public static boolean hasIncompleteLoginAttempts (TdApi.Session[] sessions) { - for (TdApi.Session session : sessions) { - if (session.isPasswordPending) - return true; + public static @DrawableRes int findFolderIcon (TdApi.ChatFolderIcon icon, @DrawableRes int defaultIcon) { + if (icon != null && !StringUtils.isEmpty(icon.name)) { + return iconByName(icon.name, defaultIcon); + } else { + return defaultIcon; + } + } + + public static @DrawableRes int iconByName (String iconName, @DrawableRes int defaultIcon) { + if (StringUtils.isEmpty(iconName)) + return defaultIcon; + switch (iconName) { + case "All": + return R.drawable.baseline_forum_24; + case "Unmuted": + return R.drawable.baseline_notifications_24; + case "Bots": + return R.drawable.deproko_baseline_bots_24; + case "Channels": + return R.drawable.baseline_bullhorn_24; + case "Groups": + return R.drawable.baseline_group_24; + case "Private": + return R.drawable.baseline_person_24; + case "Setup": + return R.drawable.baseline_assignment_24; + case "Cat": + return R.drawable.templarian_baseline_cat_24; + case "Crown": + return R.drawable.baseline_crown_circle_24; + case "Favorite": + return R.drawable.baseline_star_24; + case "Flower": + return R.drawable.baseline_local_florist_24; + case "Game": + return R.drawable.baseline_sports_esports_24; + case "Home": + return R.drawable.baseline_home_24; + case "Love": + return R.drawable.baseline_favorite_24; + case "Mask": + return R.drawable.deproko_baseline_masks_24; + case "Party": + return R.drawable.baseline_party_popper_24; + case "Sport": + return R.drawable.baseline_sports_soccer_24; + case "Study": + return R.drawable.baseline_school_24; + case "Work": + return R.drawable.baseline_work_24; + case "Airplane": + // return R.drawable.baseline_flight_24; + return R.drawable.baseline_logo_telegram_24; + case "Book": + return R.drawable.baseline_book_24; + case "Light": + return R.drawable.deproko_baseline_lamp_filled_24; + case "Like": + return R.drawable.baseline_thumb_up_24; + case "Money": + return R.drawable.baseline_currency_bitcoin_24; + case "Note": + return R.drawable.baseline_music_note_24; + case "Palette": + // return R.drawable.baseline_palette_24; + return R.drawable.baseline_brush_24; + case "Unread": + return R.drawable.baseline_mark_chat_unread_24; + case "Travel": + // return R.drawable.baseline_explore_24; + return R.drawable.baseline_flight_24; + case "Custom": + return R.drawable.baseline_folder_24; + case "Trade": + return R.drawable.baseline_finance_24; + default: + return defaultIcon; } - return false; } + + public static final String[] ICON_NAMES = {"All", "Unread", "Unmuted", "Bots", "Channels", "Groups", "Private", "Custom", "Setup", "Cat", "Crown", "Favorite", "Flower", "Game", "Home", "Love", "Mask", "Party", "Sport", "Study", "Trade", "Travel", "Work", "Airplane", "Book", "Light", "Like", "Money", "Note", "Palette"}; } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGAvatars.java b/app/src/main/java/org/thunderdog/challegram/data/TGAvatars.java index 2cf9fb475c..28c7322dc2 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGAvatars.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGAvatars.java @@ -26,6 +26,7 @@ import me.vkryl.android.animator.FactorAnimator; import me.vkryl.android.animator.ListAnimator; import me.vkryl.android.util.ViewProvider; +import me.vkryl.core.MathUtils; import me.vkryl.td.Td; public final class TGAvatars implements FactorAnimator.Target { @@ -134,17 +135,17 @@ private void setEntries (@Nullable List entries, boolean animated) } } - public void requestFiles (ComplexReceiver complexReceiver, boolean isUpdate) { + public void requestFiles (ComplexReceiver complexReceiver, boolean isUpdate, boolean neverClear) { if (complexReceiver != null) { if (entries != null && !entries.isEmpty()) { for (AvatarEntry entry : entries) { AvatarReceiver receiver = complexReceiver.getAvatarReceiver(entry.id()); receiver.requestMessageSender(tdlib, entry.senderId, AvatarReceiver.Options.NONE); } - if (!isUpdate) { - complexReceiver.clearReceivers((receiverType, receiver, key) -> receiverType == ComplexReceiver.RECEIVER_TYPE_AVATAR && entriesIds != null && entriesIds.contains(key)); + if (!isUpdate && !neverClear) { + complexReceiver.clearReceivers((receiverType, receiver, key) -> receiverType == ComplexReceiver.ReceiverType.AVATAR && entriesIds != null && entriesIds.contains(key)); } - } else { + } else if (!neverClear) { complexReceiver.clear(); } } @@ -162,13 +163,27 @@ public float getAnimatedWidth () { return avatarSize + (avatarSize + Screen.dp(avatarSpacing)) * (factor - 1f); } - public void draw (@NonNull MessageView view, @NonNull Canvas c, int x, int cy, int gravity, float alpha) { - if (animator == null || animator.size() == 0 || alpha == 0f) { - return; + public float getTargetWidth (float offset) { + if (countAnimator == null) { + return 0f; + } + float factor = countAnimator.getToFactor(); + int avatarSize = Screen.dp(avatarRadius) * 2; + if (factor < 1f) { + return avatarSize * factor; } + return avatarSize + (avatarSize + Screen.dp(avatarSpacing)) * (factor - 1f) + offset * MathUtils.clamp(countAnimator.getToFactor()); + } + + public float getAvatarsVisibility () { + if (countAnimator == null) { + return 0f; + } + return MathUtils.clamp(countAnimator.getFactor()); + } - ComplexReceiver avatarsReceiver = view.getAvatarsReceiver(); - if (avatarsReceiver == null) { + public void draw (@NonNull MessageView view, @NonNull Canvas c, @Nullable ComplexReceiver avatarsReceiver, int x, int cy, int gravity, float alpha) { + if (animator == null || animator.size() == 0 || alpha == 0f || avatarsReceiver == null) { return; } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGChat.java b/app/src/main/java/org/thunderdog/challegram/data/TGChat.java index ebffa8d4d1..0ab6507420 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGChat.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGChat.java @@ -14,7 +14,6 @@ */ package org.thunderdog.challegram.data; -import android.os.SystemClock; import android.view.Gravity; import android.view.View; @@ -28,18 +27,24 @@ import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.loader.ComplexReceiver; import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.ReactionLoadListener; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibChatList; import org.thunderdog.challegram.telegram.TdlibCounter; import org.thunderdog.challegram.telegram.TdlibStatusManager; +import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Icons; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.util.EmojiStatusHelper; +import org.thunderdog.challegram.util.ReactionsCounterDrawable; +import org.thunderdog.challegram.util.ReactionsListAnimator; import org.thunderdog.challegram.util.text.Counter; import org.thunderdog.challegram.util.text.Text; import org.thunderdog.challegram.util.text.TextColorSetOverride; @@ -48,16 +53,21 @@ import org.thunderdog.challegram.util.text.TextEntityCustom; import org.thunderdog.challegram.util.text.TextMedia; import org.thunderdog.challegram.util.text.TextStyleProvider; +import org.thunderdog.challegram.v.MessagesRecyclerView; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; +import me.vkryl.android.AnimatorUtils; import me.vkryl.android.animator.BounceAnimator; import me.vkryl.android.util.MultipleViewProvider; import me.vkryl.core.ArrayUtils; import me.vkryl.core.BitwiseUtils; -import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.collection.IntList; import me.vkryl.core.lambda.Destroyable; @@ -66,7 +76,7 @@ import me.vkryl.td.ChatPosition; import me.vkryl.td.Td; -public class TGChat implements TdlibStatusManager.HelperTarget, TD.ContentPreview.RefreshCallback, Counter.Callback, Destroyable { +public class TGChat implements TdlibStatusManager.HelperTarget, ContentPreview.RefreshCallback, Counter.Callback, ReactionLoadListener, Destroyable, TdlibUi.MessageProvider { private static final int FLAG_HAS_PREFIX = 1; private static final int FLAG_TEXT_DRAFT = 1 << 4; private static final int FLAG_SHOW_VERIFY = 1 << 5; @@ -78,6 +88,7 @@ public class TGChat implements TdlibStatusManager.HelperTarget, TD.ContentPrevie private static final int FLAG_ARCHIVE = 1 << 14; private static final int FLAG_SHOW_SCAM = 1 << 15; private static final int FLAG_SHOW_FAKE = 1 << 16; + private static final int FLAG_MESSAGE = 1 << 17; private int flags, listMode; @@ -93,8 +104,6 @@ public class TGChat implements TdlibStatusManager.HelperTarget, TD.ContentPrevie private int currentWidth; - private long viewedMessageId; - private AvatarPlaceholder.Metadata avatarPlaceholder; private String title; @@ -111,7 +120,7 @@ public class TGChat implements TdlibStatusManager.HelperTarget, TD.ContentPrevie private TextEntity[] entities; private Text trimmedText; - private TD.ContentPreview currentPreview; + private ContentPreview currentPreview; private IntList textIconIds; private @PorterDuffColorId int textIconColorId; @@ -120,6 +129,7 @@ public class TGChat implements TdlibStatusManager.HelperTarget, TD.ContentPrevie private int muteLeft, verifyLeft; private int emojiStatusLeft; private int checkRight; + private int reactionsRight; private Text chatMark; @@ -128,6 +138,12 @@ public class TGChat implements TdlibStatusManager.HelperTarget, TD.ContentPrevie private final BounceAnimator scheduleAnimator; private final Counter counter, mentionCounter, reactionsCounter, viewCounter; + private final ReactionsCounterDrawable reactionsCounterDrawable; + private final HashMap reactionsMapEntry = new HashMap<>(); + private final ReactionsListAnimator reactionsAnimator; + private final ArrayList reactionsListEntry = new ArrayList<>(); + private Set awaitingReactions; + public TGChat (ViewController context, TdApi.ChatList chatList, TdApi.Chat chat, boolean makeMeasures) { this.context = context; this.statusHelper = new TdlibStatusManager.Helper(context.context(), context.tdlib(), this, context); @@ -159,6 +175,11 @@ public TGChat (ViewController context, TdApi.ChatList chatList, TdApi.Chat ch } } this.scheduleAnimator = new BounceAnimator(currentViews); + this.reactionsAnimator = new ReactionsListAnimator( + (a) -> currentViews.invalidate(), + AnimatorUtils.DECELERATE_INTERPOLATOR, + MessagesRecyclerView.ITEM_ANIMATOR_DURATION + 50L); + this.reactionsCounterDrawable = new ReactionsCounterDrawable(reactionsAnimator); this.counter = new Counter.Builder().callback(this).build(); this.mentionCounter = new Counter.Builder() .drawable(R.drawable.baseline_at_16, 16f, 0f, Gravity.CENTER) @@ -177,7 +198,9 @@ public TGChat (ViewController context, TdApi.ChatList chatList, TdApi.Chat ch .drawable(R.drawable.baseline_visibility_14, 14f, 3f, Gravity.RIGHT) .build(); setCounter(false); + setReactions(false); setViews(); + this.tdlib.singleUnreadReactionsManager().checkChat(chat); this.scheduleAnimator.setValue(hasScheduledMessages(), false); checkOnline(); if (makeMeasures) { @@ -194,6 +217,11 @@ public TGChat (ViewController context, TdlibChatList list, boolean makeMeasur this.archive = list; this.listMode = Settings.instance().getChatListMode(); this.scheduleAnimator = new BounceAnimator(currentViews); + this.reactionsAnimator = new ReactionsListAnimator( + (a) -> currentViews.invalidate(), + AnimatorUtils.DECELERATE_INTERPOLATOR, + MessagesRecyclerView.ITEM_ANIMATOR_DURATION + 50L); + this.reactionsCounterDrawable = new ReactionsCounterDrawable(reactionsAnimator); this.counter = new Counter.Builder().callback(this).build(); this.mentionCounter = new Counter.Builder() .drawable(R.drawable.baseline_at_16, 16f, 0f, Gravity.CENTER) @@ -280,17 +308,8 @@ public void checkChatListMode () { } } - private long lastSyncTime; - - public void syncCounter () { - if (chat != null && chat.lastMessage != null && isChannel() && showViews()) { - long time = SystemClock.uptimeMillis(); - if (viewedMessageId != chat.lastMessage.id || time - lastSyncTime > 60000 * 5 + (1f - MathUtils.clamp((float) TD.getViewCount(chat.lastMessage.interactionInfo) / 1000.0f)) * 1800000 ) { - lastSyncTime = time; - viewedMessageId = chat.lastMessage.id; - tdlib.client().send(new TdApi.ViewMessages(chat.id, new long[] {viewedMessageId}, new TdApi.MessageSourceChatList(), false), tdlib.okHandler()); - } - } + public boolean needRefreshInteractionInfo () { + return chat != null && chat.lastMessage != null && isChannel() && showViews(); } public boolean checkOnline () { @@ -502,7 +521,8 @@ public boolean updateMessageInteractionInfo (long chatId, long messageId, @Nulla if (getChatId() == chatId && checkLastMessageId(messageId)) { chat.lastMessage.interactionInfo = interactionInfo; setViews(); - return showViews(); + boolean r = setReactions(true); + return showViews() || r; } return false; } @@ -521,10 +541,13 @@ public boolean updateMessageContent (long chatId, long messageId, TdApi.MessageC } } if (updatedAlbum) { - setContentPreview(TD.getAlbumPreview(tdlib, chat.lastMessage, album, true)); + setContentPreview(ContentPreview.getAlbumPreview(tdlib, chat.lastMessage, album, true)); return true; } } + if (currentPreview.updateRelatedMessage(chatId, messageId, newContent, this)) { + return true; + } } if (chat.lastMessage.id == messageId) { chat.lastMessage.content = newContent; @@ -537,7 +560,7 @@ public boolean updateMessageContent (long chatId, long messageId, TdApi.MessageC public boolean updateMessagesDeleted (long chatId, long[] messageIds) { if (chat.id == chatId && chat.lastMessage != null) { - if (ArrayUtils.indexOf(messageIds, chat.lastMessage.id) >= 0) { + if (ArrayUtils.contains(messageIds, chat.lastMessage.id)) { chat.lastMessage = null; setText(); return true; @@ -548,7 +571,7 @@ public boolean updateMessagesDeleted (long chatId, long[] messageIds) { if (album != null) { boolean albumChanged = false; for (int index = album.messages.size() - 1; index >= 0; index--) { - if (ArrayUtils.indexOf(messageIds, album.messages.get(index).id) >= 0) { + if (ArrayUtils.contains(messageIds, album.messages.get(index).id)) { album.messages.remove(index); albumChanged = true; } @@ -558,9 +581,13 @@ public boolean updateMessagesDeleted (long chatId, long[] messageIds) { return true; } if (albumChanged) { - setContentPreview(TD.getAlbumPreview(tdlib, chat.lastMessage, album, true)); + setContentPreview(ContentPreview.getAlbumPreview(tdlib, chat.lastMessage, album, true)); } } + if (currentPreview.belongsToRelatedMessage(chatId, messageIds)) { + setText(); + return true; + } } return true; } @@ -698,7 +725,7 @@ public boolean updateChatSettings (long chatId, final TdApi.ChatNotificationSett private void setAvatar () { if (isArchive()) { - avatarPlaceholder = new AvatarPlaceholder.Metadata(ColorId.avatarArchive, R.drawable.baseline_archive_24); + avatarPlaceholder = new AvatarPlaceholder.Metadata(tdlib.accentColor(TdlibAccentColor.InternalId.ARCHIVE), R.drawable.baseline_archive_24); } else { avatarPlaceholder = null; } @@ -894,13 +921,17 @@ public void layoutTitle (boolean changed) { if (!tdlib.isSelfChat(chat)) { emojiStatusDrawable = EmojiStatusHelper.makeDrawable(null, tdlib, chat != null ? tdlib.chatUser(chat) : null, new TextColorSetOverride(TextColorSets.Regular.NORMAL) { @Override - public int emojiStatusColor () { - return Theme.getColor(ColorId.iconActive); + public long mediaTextComplexColor () { + return Theme.newComplexColor(true, ColorId.iconActive); } }, this::invalidateEmojiStatusReceiver); emojiStatusDrawable.invalidateTextMedia(); avail -= emojiStatusDrawable.getWidth(Screen.dp(6)); } + if (needDrawReactionsPreview()) { + int reactionsWidth = reactionsCounterDrawable.getTargetWidth(); + avail -= reactionsWidth + (reactionsWidth > 0 ? Screen.dp(3) : 0); + } if (changed || lastAvailWidth != avail) { lastAvailWidth = avail; @@ -917,7 +948,7 @@ public int emojiStatusColor () { int titleWidth = getTitleWidth(); verifyLeft = ChatView.getLeftPadding(listMode) + titleWidth; muteLeft = ChatView.getLeftPadding(listMode) + titleWidth + ChatView.getMutePadding(); - emojiStatusLeft = ChatView.getLeftPadding(listMode) + titleWidth + Screen.dp(6); + emojiStatusLeft = ChatView.getLeftPadding(listMode) + titleWidth + Screen.dp(3); if (emojiStatusDrawable != null) { muteLeft += emojiStatusDrawable.getWidth(Screen.dp(6)); verifyLeft += emojiStatusDrawable.getWidth(Screen.dp(6)); @@ -937,6 +968,10 @@ public int emojiStatusColor () { } } + public boolean needDrawReactionsPreview () { + return isPrivate() && !isSelfChat() && !showDraft(); + } + public @Nullable EmojiStatusHelper.EmojiStatusDrawable getEmojiStatus () { return emojiStatusDrawable; } @@ -949,6 +984,22 @@ public void invalidateEmojiStatusReceiver (Text text, @Nullable TextMedia specif }); } + public void invalidateReactionsReceiver () { + currentViews.performWithViews(view -> { + if (view instanceof ChatView) { + requestReactionFiles(((ChatView) view).getReactionsReceiver()); + } + }); + currentViews.invalidate(); + } + + public void requestReactionFiles (ComplexReceiver complexReceiver) { + for (Map.Entry pair : reactionsMapEntry.entrySet()) { + TGReactions.MessageReactionEntry entry = pair.getValue(); + entry.requestReactionFiles(complexReceiver); + } + } + private void setTitleImpl (String title) { this.title = title; } @@ -962,7 +1013,8 @@ public Text getTitle () { return trimmedTitle; } - public void setTime () { + public boolean setTime () { + String time; if (isArchive()) { int maxDate = archive.maxDate(); time = maxDate != 0 ? Lang.timeOrDateShort(maxDate, TimeUnit.SECONDS) : ""; @@ -977,15 +1029,25 @@ public void setTime () { time = Lang.getPsaType(((TdApi.ChatSourcePublicServiceAnnouncement) source)); break; } + default: { + Td.assertChatSource_12b21238(); + throw Td.unsupported(source); + } } } else { int date = chat.draftMessage != null && showDraft() ? chat.draftMessage.date : chat.lastMessage != null ? chat.lastMessage.date : 0; time = date != 0 ? Lang.timeOrDateShort(date, TimeUnit.SECONDS) : ""; } } - timeWidth = (int) U.measureText(time, ChatView.getTimePaint()); + boolean changed = !time.equals(this.time); + if (changed) { + this.time = time; + this.timeWidth = (int) U.measureText(time, ChatView.getTimePaint()); + } layoutTime(); + setReactions(UI.inUiThread()); setViews(); + return changed; } private int getViewCount () { @@ -999,6 +1061,12 @@ private void setViews () { } } + public void updateDate () { + if (setTime()) { + layoutTitle(false); + } + } + public void updateLocale (boolean forceText) { setTime(); setCounter(true); @@ -1015,6 +1083,7 @@ public void updateLocale (boolean forceText) { private void layoutTime () { timeLeft = currentWidth - ChatView.getTimePaddingRight() - timeWidth; checkRight = timeLeft - ChatView.getTimePaddingLeft(); + reactionsRight = checkRight - ChatView.getTimePaddingLeft(); } public String getTime () { @@ -1050,13 +1119,17 @@ public int getVerifyLeft () { } public int getEmojiStatusLeft () { - return emojiStatusLeft + (isSecretChat() ? Screen.dp(12): 0); + return emojiStatusLeft + (isSecretChat() ? Screen.dp(12) : 0); } public int getChecksRight () { return checkRight; } + public int getReactionsWidth () { + return reactionsCounterDrawable.getMinimumWidth(); + } + private int getCounterAddWidth () { return Math.round( counter.getScaledWidth(ChatView.getTimePaddingLeft()) + @@ -1150,7 +1223,7 @@ private void setPrefix () { } else if (isOutgoing()) { prefix = Lang.getString(listMode != Settings.CHAT_MODE_2LINE && tdlib.isMultiChat(chat) && Td.getSenderId(chat.lastMessage) == chat.id ? R.string.FromYouAnonymous : R.string.FromYou); flags |= FLAG_CONTENT_STRING; - } else if (chat.lastMessage != null && chat.lastMessage.content.getConstructor() != TdApi.MessageProximityAlertTriggered.CONSTRUCTOR) { + } else if (chat.lastMessage != null && !Td.isProximityAlertTriggered(chat.lastMessage.content)) { prefix = listMode == Settings.CHAT_MODE_2LINE && Td.getMessageAuthorId(chat.lastMessage) == chat.lastMessage.chatId && StringUtils.isEmpty(chat.lastMessage.authorSignature) ? Lang.getString(R.string.FromAnonymous) : tdlib.senderName(chat.lastMessage, false, listMode == Settings.CHAT_MODE_2LINE); @@ -1212,20 +1285,22 @@ public void setText () { flags &= ~FLAG_TEXT_DRAFT; flags &= ~FLAG_CONTENT_HIDDEN; flags &= ~FLAG_CONTENT_STRING; + flags &= ~FLAG_MESSAGE; if (textIconIds != null) { textIconIds.clear(); } + visibleMessage = null; currentPreview = null; if (tdlib.hasPasscode(chat)) { flags |= FLAG_CONTENT_HIDDEN; - setContentPreview(new TD.ContentPreview(TD.EMOJI_LOCK, R.string.ChatContentProtected)); + setContentPreview(new ContentPreview(ContentPreview.EMOJI_LOCK, R.string.ChatContentProtected)); return; } String restrictionReason = tdlib.chatRestrictionReason(chat); if (restrictionReason != null) { - setContentPreview(new TD.ContentPreview(TD.EMOJI_ERROR, 0, restrictionReason, false)); + setContentPreview(new ContentPreview(ContentPreview.EMOJI_ERROR, 0, restrictionReason, false)); return; } @@ -1263,7 +1338,7 @@ public void setText () { TdApi.ChatSource source = getSource(); String psaText = source instanceof TdApi.ChatSourcePublicServiceAnnouncement ? ((TdApi.ChatSourcePublicServiceAnnouncement) source).text : null; if (!StringUtils.isEmpty(psaText)) { - setContentPreview(new TD.ContentPreview(TD.EMOJI_INFO, 0, psaText, false)); + setContentPreview(new ContentPreview(ContentPreview.EMOJI_INFO, 0, psaText, false)); return; } @@ -1302,14 +1377,35 @@ public void setText () { TdApi.Message msg = chat.lastMessage; if (msg != null) { - TD.ContentPreview preview = TD.getChatListPreview(tdlib, msg.chatId, msg); + flags |= FLAG_MESSAGE; + // No need to check tdlib.chatRestrictionReason, because it's already handled above + ContentPreview preview = ContentPreview.getChatListPreview(tdlib, msg.chatId, msg, false); setContentPreview(preview); + visibleMessage = msg; } else { setTextValue(R.string.DeletedMessage); setPrefix(); } } + private TdApi.Message visibleMessage; + + @Override + public boolean isMediaGroup () { + return currentPreview != null && currentPreview.getAlbum() != null; + } + + @Override + public List getVisibleMediaGroup () { + Tdlib.Album album = currentPreview != null ? currentPreview.getAlbum() : null; + return album != null ? album.messages : null; + } + + @Override + public TdApi.Message getVisibleMessage () { + return visibleMessage; + } + private void addIcon (int icon) { if (icon != 0) { if (textIconIds == null) @@ -1318,35 +1414,45 @@ private void addIcon (int icon) { } } - private void setContentPreview (TD.ContentPreview preview) { + private void setContentPreview (ContentPreview preview) { if (textIconIds != null) textIconIds.clear(); setTextValue(preview.buildText(true), preview.formattedText != null ? preview.formattedText.entities : null, preview.isTranslatable); this.currentPreview = preview; + TdApi.Message lastMessage = chat != null ? chat.lastMessage : null; if (preview.parentEmoji != null) { addIcon(preview.parentEmoji.iconRepresentation); - } else if (chat.lastMessage != null && chat.lastMessage.forwardInfo != null && (chat.lastMessage.isChannelPost || getPrefixIconCount() == 0)) { - TdApi.MessageForwardInfo forwardInfo = chat.lastMessage.forwardInfo; - switch (forwardInfo.origin.getConstructor()) { - case TdApi.MessageForwardOriginChannel.CONSTRUCTOR: - if (chat.id != ((TdApi.MessageForwardOriginChannel) forwardInfo.origin).chatId) { - addIcon(R.drawable.baseline_share_arrow_16); - } - break; - case TdApi.MessageForwardOriginHiddenUser.CONSTRUCTOR: - addIcon(R.drawable.baseline_share_arrow_16); - break; - case TdApi.MessageForwardOriginUser.CONSTRUCTOR: - if (Td.getSenderUserId(chat.lastMessage) != ((TdApi.MessageForwardOriginUser) forwardInfo.origin).senderUserId) - addIcon(R.drawable.baseline_share_arrow_16); - break; - case TdApi.MessageForwardOriginChat.CONSTRUCTOR: - if (Td.getSenderId(chat.lastMessage) != ((TdApi.MessageForwardOriginChat) forwardInfo.origin).senderChatId) - addIcon(R.drawable.baseline_share_arrow_16); - break; - case TdApi.MessageForwardOriginMessageImport.CONSTRUCTOR: - addIcon(R.drawable.templarian_baseline_import_16); - break; + } else if (lastMessage != null && (lastMessage.isChannelPost || getPrefixIconCount() == 0)) { + boolean needShareIcon = false; + if (lastMessage.forwardInfo != null) { + TdApi.MessageOrigin origin = lastMessage.forwardInfo.origin; + switch (origin.getConstructor()) { + case TdApi.MessageOriginChannel.CONSTRUCTOR: + needShareIcon = chat.id != ((TdApi.MessageOriginChannel) origin).chatId; + break; + case TdApi.MessageOriginHiddenUser.CONSTRUCTOR: + needShareIcon = true; + break; + case TdApi.MessageOriginUser.CONSTRUCTOR: + needShareIcon = Td.getSenderUserId(chat.lastMessage) != ((TdApi.MessageOriginUser) origin).senderUserId; + break; + case TdApi.MessageOriginChat.CONSTRUCTOR: + needShareIcon = Td.getSenderId(chat.lastMessage) != ((TdApi.MessageOriginChat) origin).senderChatId; + break; + default: + Td.assertMessageOrigin_f2224a59(); + throw Td.unsupported(origin); + } + } + if (needShareIcon) { + addIcon(R.drawable.baseline_share_arrow_16); + } else if (lastMessage.importInfo != null) { + addIcon(R.drawable.templarian_baseline_import_16); + } else if (lastMessage.replyTo != null && lastMessage.replyTo.getConstructor() == TdApi.MessageReplyToMessage.CONSTRUCTOR) { + TdApi.MessageReplyToMessage replyToMessage = (TdApi.MessageReplyToMessage) lastMessage.replyTo; + if (replyToMessage.chatId != lastMessage.chatId) { + addIcon(R.drawable.baseline_reply_16); + } } } if (preview.emoji != null) { @@ -1356,7 +1462,7 @@ private void setContentPreview (TD.ContentPreview preview) { if ((isGroup() || isSupergroup()) && !preview.hideAuthor) { flags |= FLAG_HAS_PREFIX; - } else if (chat.lastMessage != null && chat.lastMessage.content.getConstructor() == TdApi.MessageCall.CONSTRUCTOR) { + } else if (chat.lastMessage != null && Td.isCall(chat.lastMessage.content)) { if (textIconIds != null) textIconIds.clear(); addIcon(CallItem.getSubtitleIcon((TdApi.MessageCall) chat.lastMessage.content, TD.isOut(chat.lastMessage))); @@ -1371,7 +1477,7 @@ private void setContentPreview (TD.ContentPreview preview) { } @Override - public void onContentPreviewChanged (long chatId, long messageId, TD.ContentPreview newPreview, TD.ContentPreview oldPreview) { + public void onContentPreviewChanged (long chatId, long messageId, ContentPreview newPreview, ContentPreview oldPreview) { tdlib.ui().post(() -> { if (currentPreview == oldPreview) { setContentPreview(newPreview); @@ -1480,9 +1586,97 @@ public boolean canAnimate () { return ((flags & FLAG_ATTACHED) != 0) && currentViews.hasAnyTargetToInvalidate() && context.getParentOrSelf().getAttachState(); } + private boolean isDestroyed; + @Override public void performDestroy () { currentViews.detachFromAllViews(); setViewAttached(false); + isDestroyed = true; + if (awaitingReactions != null) { + for (String reactionKey : awaitingReactions) { + tdlib.listeners().removeReactionLoadListener(reactionKey, this); + } + awaitingReactions.clear(); + } + } + + public ReactionsCounterDrawable getReactionsCounterDrawable () { + return reactionsCounterDrawable; + } + + private boolean setReactions (boolean animated) { + return setReactions(chat != null && chat.lastMessage != null && chat.lastMessage.interactionInfo != null ? + chat.lastMessage.interactionInfo.reactions : null, animated); + } + + private boolean setReactions (TdApi.MessageReaction[] reactions, boolean animated) { + this.reactionsListEntry.clear(); + + if (reactions != null && !isDestroyed) { + for (TdApi.MessageReaction reaction : reactions) { + String reactionKey = TD.makeReactionKey(reaction.type); + + TGReaction reactionObj = tdlib.getReaction(reaction.type); + if (reactionObj == null) { + if (awaitingReactions == null) { + awaitingReactions = new LinkedHashSet<>(); + } + if (awaitingReactions.add(reactionKey)) { + tdlib.listeners().addReactionLoadListener(reactionKey, this); + } + continue; + } + TGReactions.MessageReactionEntry entry = getMessageReactionEntry(reactionObj); + entry.setMessageReaction(reaction); + reactionsListEntry.add(entry); + if (awaitingReactions != null && awaitingReactions.remove(reactionKey)) { + tdlib.listeners().removeReactionLoadListener(reactionKey, this); + } + } + } + + boolean r = !reactionsAnimator.compareContents(reactionsListEntry); + reactionsAnimator.reset(reactionsListEntry, animated); + layoutTitle(false); + currentViews.invalidate(); + invalidateReactionsReceiver(); + return r; + } + + private TGReactions.MessageReactionEntry getMessageReactionEntry (TGReaction reactionObj) { + final TGReactions.MessageReactionEntry entry; + if (!reactionsMapEntry.containsKey(reactionObj.key)) { + entry = new TGReactions.MessageReactionEntry(tdlib, null, null, reactionObj, null) { + @Override + public int getBubbleTargetWidth () { + return 0; + } + + @Override + public int getBubbleWidth () { + return 0; + } + + @Override + public void invalidate () { + currentViews.invalidate(); + } + }; + reactionsMapEntry.put(reactionObj.key, entry); + } else { + entry = reactionsMapEntry.get(reactionObj.key); + } + return entry; + } + + @Override + public void onReactionLoaded (String reactionKey) { + if (awaitingReactions != null && awaitingReactions.remove(reactionKey)) { + UI.post(() -> { + setReactions(true); + invalidateReactionsReceiver(); + }); + } } } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGCommentButton.java b/app/src/main/java/org/thunderdog/challegram/data/TGCommentButton.java index 272cfd4fac..705ff79360 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGCommentButton.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGCommentButton.java @@ -2,6 +2,7 @@ import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; @@ -28,6 +29,7 @@ import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; @@ -136,7 +138,7 @@ public void onInvalidateMedia (TGAvatars avatars) { } public void requestResources (@Nullable ComplexReceiver complexReceiver, boolean isUpdate) { - this.avatars.requestFiles(complexReceiver, isUpdate); + this.avatars.requestFiles(complexReceiver, isUpdate, false); } public void setViewMode (@ViewMode int viewMode, boolean animated) { @@ -240,6 +242,10 @@ public boolean contains (float x, float y) { return rect.contains(Math.round(x), Math.round(y)); } + public void getRect (Rect rect) { + rect.set(this.rect); + } + private static final int FLAG_CAUGHT = 0x01; private static final int FLAG_BLOCKED = 0x02; @@ -277,11 +283,11 @@ public boolean onTouchEvent (View view, MotionEvent event) { @Override public void onClickAt (View view, float x, float y) { if (context.isRepliesChat()) { TdApi.MessageForwardInfo forwardInfo = context.msg.forwardInfo; - MessageId replyToMessageId = new MessageId(context.msg.replyInChatId, context.msg.replyToMessageId); + MessageId replyToMessageId = MessageId.valueOf(context.msg.replyTo); if (forwardInfo != null && forwardInfo.fromChatId != 0 && forwardInfo.fromMessageId != 0) { MessageId replyMessageId = new MessageId(forwardInfo.fromChatId, forwardInfo.fromMessageId); context.openMessageThread(replyMessageId, replyToMessageId); - } else { + } else if (replyToMessageId != null) { context.openMessageThread(replyToMessageId); } } else { @@ -411,11 +417,19 @@ private void drawInline (@NonNull MessageView view, @NonNull Canvas c, @NonNull drawSelection(c, selectionFactor, selectionColor); } - int iconColorId = ColorId.inlineIcon; + TdlibAccentColor accentColor = context.getContentAccentColor(); + long complexIconColor = accentColor != null ? accentColor.getNameComplexColor() : 0; + int iconColorId = complexIconColor != 0 ? (Theme.isColorId(complexIconColor) ? Theme.extractColorValue(complexIconColor) : ColorId.NONE) : ColorId.inlineIcon; Drawable icon = drawableProvider.getSparseDrawable(R.drawable.baseline_forum_18, iconColorId); float iconX = useBubbles ? left + Screen.dp(16f) : (TGMessage.getContentLeft() - icon.getMinimumWidth()) / 2f; float iconY = rect.centerY() - icon.getMinimumHeight() / 2f; - Drawables.draw(c, icon, iconX, iconY, PorterDuffPaint.get(iconColorId, alpha)); + Paint iconPaint; + if (complexIconColor != 0) { + iconPaint = Theme.getComplexPorterDuffPaint(complexIconColor, alpha); + } else { + iconPaint = PorterDuffPaint.get(iconColorId, alpha); + } + Drawables.draw(c, icon, iconX, iconY, iconPaint); float textX = useBubbles ? left + Screen.dp(46f) : TGMessage.getContentLeft(); float textY = rect.centerY(); @@ -436,7 +450,7 @@ private void drawInline (@NonNull MessageView view, @NonNull Canvas c, @NonNull int avatarsX = right - (useBubbles ? Screen.dp(16f) : Screen.dp(38f)); int avatarsY = rect.centerY(); - avatars.draw(view, c, avatarsX, avatarsY, Gravity.RIGHT, alpha); + avatars.draw(view, c, view.getAvatarsReceiver(), avatarsX, avatarsY, Gravity.RIGHT, alpha); int badgeX = avatarsX - Math.round(avatars.getAnimatedWidth()) - Screen.dp(8f) - Screen.dp(BADGE_RADIUS); int badgeY = rect.centerY(); @@ -497,7 +511,7 @@ private void drawBubble (@NonNull MessageView view, @NonNull Canvas c, @NonNull int avatarsX = right - Screen.dp(6f); int avatarsY = rect.centerY(); - avatars.draw(view, c, avatarsX, avatarsY, Gravity.RIGHT, alpha); + avatars.draw(view, c, view.getAvatarsReceiver(), avatarsX, avatarsY, Gravity.RIGHT, alpha); float badgeX = avatarsX - avatars.getAnimatedWidth() - Screen.dp(8f) - Screen.dp(BADGE_RADIUS); float badgeY = rect.centerY(); @@ -507,7 +521,7 @@ private void drawBubble (@NonNull MessageView view, @NonNull Canvas c, @NonNull } private void drawText (@NonNull Canvas c, float x, float cy, float alpha) { - DrawAlgorithms.drawCounter(c, x, cy, Gravity.LEFT, counterAnimator, getTextSize(), false, this, null, Gravity.LEFT, 0, 0, alpha, 0, 1f); + DrawAlgorithms.drawCounter(c, x, cy, Gravity.LEFT, counterAnimator, getTextSize(), alpha, /* colorSet */ this, /* scale */ 1f); } private void drawSelection (@NonNull Canvas c, float selectionFactor, int selectionColor) { @@ -542,6 +556,10 @@ private void drawBadge (@NonNull Canvas c, float cx, float cy, float alpha) { @Override public int defaultTextColor () { if (isInline()) { + TdlibAccentColor accentColor = context.getContentAccentColor(); + if (accentColor != null) { + return accentColor.getNameColor(); + } return Theme.inlineTextColor(false); } if (isBubble()) { @@ -626,11 +644,12 @@ private boolean useDarkTheme () { private void openCommentsPreviewAsync (int x, int y) { MessageId messageId, fallbackMessageId; if (context.isRepliesChat()) { + MessageId replyToMessageId = MessageId.valueOf(context.msg.replyTo); if (context.msg.forwardInfo != null) { messageId = new MessageId(context.msg.forwardInfo.fromChatId, context.msg.forwardInfo.fromMessageId); - fallbackMessageId = new MessageId(context.msg.replyInChatId, context.msg.replyToMessageId); + fallbackMessageId = replyToMessageId; } else { - messageId = new MessageId(context.msg.replyInChatId, context.msg.replyToMessageId); + messageId = replyToMessageId; fallbackMessageId = null; } } else { @@ -642,37 +661,36 @@ private void openCommentsPreviewAsync (int x, int y) { return; } } - openCommentsPreviewAsync(messageId, fallbackMessageId, x, y); + if (messageId != null) { + openCommentsPreviewAsync(messageId, fallbackMessageId, x, y); + } } private void openCommentsPreviewAsync (@NonNull MessageId messageId, @Nullable MessageId fallbackMessageId, int x, int y) { cancelAsyncPreview(); TdApi.GetMessageThread messageThreadQuery = new TdApi.GetMessageThread(messageId.getChatId(), messageId.getMessageId()); currentMessageThreadQuery = messageThreadQuery; - context.tdlib().send(messageThreadQuery, (result) -> context.runOnUiThreadOptional(() -> { + context.tdlib().send(messageThreadQuery, (messageThreadInfo, error) -> context.runOnUiThreadOptional(() -> { if (messageThreadQuery != currentMessageThreadQuery) { return; } currentMessageThreadQuery = null; - switch (result.getConstructor()) { - case TdApi.MessageThreadInfo.CONSTRUCTOR: - openCommentsPreviewAsync((TdApi.MessageThreadInfo) result, x, y); - break; - case TdApi.Error.CONSTRUCTOR: - if ("MSG_ID_INVALID".equals(TD.errorText(result))) { - if (context.isChannel()) { - UI.showToast(R.string.ChannelPostDeleted, Toast.LENGTH_SHORT); - } else { - UI.showError(result); - } - break; - } - if (fallbackMessageId != null) { - openCommentsPreviewAsync(fallbackMessageId, null, x, y); - break; + if (error != null) { + if ("MSG_ID_INVALID".equals(TD.errorText(error))) { + if (context.isChannel()) { + UI.showToast(R.string.ChannelPostDeleted, Toast.LENGTH_SHORT); + } else { + UI.showError(error); } - UI.showError(result); - break; + return; + } + if (fallbackMessageId != null) { + openCommentsPreviewAsync(fallbackMessageId, null, x, y); + return; + } + UI.showError(error); + } else { + openCommentsPreviewAsync(messageThreadInfo, x, y); } })); } @@ -776,7 +794,7 @@ private void openPreview (MessagesController controller, int x, int y) { messageThread.getChatId(), messageIds, new TdApi.MessageSourceMessageThreadHistory(), true - ), c.tdlib().okHandler()); + ), c.tdlib().typedOkHandler()); } }; forceTouchContext.setButtons(actionListener, controller, ids, icons, hints); diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGDefaultEmoji.java b/app/src/main/java/org/thunderdog/challegram/data/TGDefaultEmoji.java new file mode 100644 index 0000000000..77a2c53787 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/data/TGDefaultEmoji.java @@ -0,0 +1,42 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 18/08/2023 + */ +package org.thunderdog.challegram.data; + +import org.thunderdog.challegram.tool.EmojiData; + +public class TGDefaultEmoji { + public final int strRes; + public final String emoji; + public final int emojiColorState; + public final boolean isRecent; + + public TGDefaultEmoji (String emoji) { + this.emoji = emoji; + this.emojiColorState = EmojiData.instance().getEmojiColorState(emoji); + this.strRes = 0; + this.isRecent = false; + } + + public TGDefaultEmoji (String emoji, boolean isRecent) { + this.emoji = emoji; + this.emojiColorState = EmojiData.instance().getEmojiColorState(emoji); + this.strRes = 0; + this.isRecent = isRecent; + } + + public boolean canBeColored () { + return emojiColorState != EmojiData.STATE_NO_COLORS; + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGFoundChat.java b/app/src/main/java/org/thunderdog/challegram/data/TGFoundChat.java index ae86bdfc17..f7c97f7700 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGFoundChat.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGFoundChat.java @@ -37,6 +37,7 @@ public class TGFoundChat { private static final int FLAG_SELF = 1 << 2; private static final int FLAG_USE_TME = 1 << 3; private static final int FLAG_FORCE_USERNAME = 1 << 4; + private static final int FLAG_NO_ANONYMOUS_BADGE = 1 << 5; private int flags; @@ -62,6 +63,31 @@ public TGFoundChat (Tdlib tdlib) { setTitleImpl(Lang.getString(R.string.Saved), null); } + public TGFoundChat (Tdlib tdlib, TdApi.MessageSender sender, boolean isGlobal) { + this.tdlib = tdlib; + this.chatList = null; + switch (sender.getConstructor()) { + case TdApi.MessageSenderUser.CONSTRUCTOR: { + TdApi.MessageSenderUser user = (TdApi.MessageSenderUser) sender; + this.chatId = 0; + this.userId = user.userId; + setUser(tdlib.cache().userStrict(user.userId), null); + break; + } + case TdApi.MessageSenderChat.CONSTRUCTOR: { + TdApi.MessageSenderChat chat = (TdApi.MessageSenderChat) sender; + this.chatId = chat.chatId; + this.userId = ChatId.toUserId(chat.chatId); + setChat(tdlib.chatStrict(chat.chatId), null, isGlobal); + break; + } + default: { + Td.assertMessageSender_439d4c9c(); + throw Td.unsupported(sender); + } + } + } + public TGFoundChat (Tdlib tdlib, TdApi.ChatList chatList, long chatId, boolean isGlobal) { this.tdlib = tdlib; this.chatList = chatList; @@ -131,6 +157,11 @@ public TGFoundChat setNoUnread () { return this; } + public TGFoundChat setNoAnonymousBadge () { + this.flags |= FLAG_NO_ANONYMOUS_BADGE; + return this; + } + private boolean noSubscription; public void setNoSubscription () { @@ -410,10 +441,13 @@ public boolean isSelfChat () { } public @Nullable TdApi.MessageSender getMessageSenderId () { - return chat != null ? chat.messageSenderId: null; + return chat != null ? chat.messageSenderId : null; } public boolean isAnonymousAdmin () { + if (BitwiseUtils.hasFlag(flags, FLAG_NO_ANONYMOUS_BADGE)) { + return false; + } TdApi.ChatMemberStatus status = tdlib.chatStatus(getChatId()); return status != null && Td.isAnonymous(status); } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGFoundMessage.java b/app/src/main/java/org/thunderdog/challegram/data/TGFoundMessage.java index 6d8a8bbec7..ff5bc8bf22 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGFoundMessage.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGFoundMessage.java @@ -28,7 +28,7 @@ public TGFoundMessage (Tdlib tdlib, TdApi.ChatList chatList, TdApi.Chat chat, Td this.chat = new TGFoundChat(tdlib, chatList, chat, null); this.message = message; // Strings.highlightWords(Strings.replaceNewLines(copyText), query, 0, InlineResultEmojiSuggestion.SPECIAL_SPLITTERS); - TD.ContentPreview preview = TD.getChatListPreview(tdlib, message.chatId, message); + ContentPreview preview = ContentPreview.getChatListPreview(tdlib, message.chatId, message, true); this.text = preview.buildFormattedText(false); this.highlight = Highlight.valueOf(text.text, query); } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGInlineKeyboard.java b/app/src/main/java/org/thunderdog/challegram/data/TGInlineKeyboard.java index 58bb9d054d..f861a23cd1 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGInlineKeyboard.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGInlineKeyboard.java @@ -33,7 +33,6 @@ import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.U; import org.thunderdog.challegram.component.chat.MessageView; @@ -43,6 +42,7 @@ import org.thunderdog.challegram.navigation.TooltipOverlayView; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.ViewSupport; +import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; @@ -606,6 +606,7 @@ public void draw (MessageView view, Canvas c, int cx, int cy, int buttonWidth, i if (type != null) { int iconColor = Theme.inlineIconColor(isOutBubble); + //noinspection SwitchIntDef switch (type.getConstructor()) { case TdApi.InlineKeyboardButtonTypeSwitchInline.CONSTRUCTOR: case TdApi.InlineKeyboardButtonTypeCallbackWithPassword.CONSTRUCTOR: @@ -626,7 +627,7 @@ public void draw (MessageView view, Canvas c, int cx, int cy, int buttonWidth, i paddingDp = 4f; break; default: - throw new UnsupportedOperationException(); + throw new RuntimeException(); } Drawable icon = getSparseDrawable(iconRes, ColorId.NONE); int padding = Screen.dp(paddingDp); @@ -1086,7 +1087,7 @@ public void onOwnershipTransferConfirmed (String password) { cancelDelayedProgress(); animateProgressFactor(1f); } - context.context.tdlib.client().send(new TdApi.GetCallbackQueryAnswer(parent.getChatId(), context.messageId, new TdApi.CallbackQueryPayloadDataWithPassword(password, data)), getAnswerCallback(currentContextId, view,false)); + context.context.tdlib.send(new TdApi.GetCallbackQueryAnswer(parent.getChatId(), context.messageId, new TdApi.CallbackQueryPayloadDataWithPassword(password, data)), getAnswerCallback(currentContextId, view,false)); } }); }; @@ -1140,12 +1141,12 @@ public void onOwnershipTransferConfirmed (String password) { showProgressDelayed(); final byte[] data = ((TdApi.InlineKeyboardButtonTypeCallback) type).data; - context.context.tdlib().client().send(new TdApi.GetCallbackQueryAnswer(parent.getChatId(), context.messageId, new TdApi.CallbackQueryPayloadData(data)), getAnswerCallback(currentContextId, view,false)); + context.context.tdlib().send(new TdApi.GetCallbackQueryAnswer(parent.getChatId(), context.messageId, new TdApi.CallbackQueryPayloadData(data)), getAnswerCallback(currentContextId, view,false)); break; } case TdApi.InlineKeyboardButtonTypeCallbackGame.CONSTRUCTOR: { - if (parent.getMessage().content.getConstructor() != TdApi.MessageGame.CONSTRUCTOR) { + if (!Td.isGame(parent.getMessage().content)) { break; } @@ -1158,7 +1159,7 @@ public void onOwnershipTransferConfirmed (String password) { makeActive(); showProgressDelayed(); - context.context.tdlib().client().send(new TdApi.GetCallbackQueryAnswer(parent.getChatId(), context.messageId, new TdApi.CallbackQueryPayloadGame(data)), getAnswerCallback(currentContextId, view, true)); + context.context.tdlib().send(new TdApi.GetCallbackQueryAnswer(parent.getChatId(), context.messageId, new TdApi.CallbackQueryPayloadGame(data)), getAnswerCallback(currentContextId, view, true)); break; } case TdApi.InlineKeyboardButtonTypeUser.CONSTRUCTOR: { @@ -1332,80 +1333,62 @@ private Client.ResultHandler getLoginUrlCallback (final int currentContextId, fi }); } - private Client.ResultHandler getAnswerCallback (final int currentContextId, final View view, final boolean isGame) { - return object -> { - switch (object.getConstructor()) { - case TdApi.CallbackQueryAnswer.CONSTRUCTOR: { - final TdApi.CallbackQueryAnswer answer = (TdApi.CallbackQueryAnswer) object; - - final CharSequence answerText; - if (answer.text.isEmpty()) { - answerText = null; - } else { - answerText = Emoji.instance().replaceEmoji(answer.text); + private Tdlib.ResultHandler getAnswerCallback (final int currentContextId, final View view, final boolean isGame) { + return (answer, error) -> { + if (error != null) { + if (error.code == 502) { + UI.showBotDown(context.context.tdlib().messageUsername(parent.getMessage())); + return; + } + UI.showError(error); + context.context.tdlib().ui().post(() -> { + if (currentContextId == contextId) { + makeInactive(); } + }); + } else { + final CharSequence answerText; + if (answer.text.isEmpty()) { + answerText = null; + } else { + answerText = Emoji.instance().replaceEmoji(answer.text); + } - final boolean showAlert = answer.showAlert; - final String url = answer.url; - - context.context.tdlib().ui().post(() -> { - if (currentContextId == contextId) { - makeInactive(); - } - - if (parent.isDestroyed()) { - return; - } + final boolean showAlert = answer.showAlert; + final String url = answer.url; - ViewController c = parent.context().navigation().getCurrentStackItem(); - boolean isMessagesController = c instanceof MessagesController; + context.context.tdlib().ui().post(() -> { + if (currentContextId == contextId) { + makeInactive(); + } - if (c == null || c.getChatId() != parent.getChatId()) { - return; - } + if (parent.isDestroyed()) { + return; + } - if (!StringUtils.isEmpty(url)) { - if (isGame && isMessagesController) { - TdApi.Message msg = parent.getMessage(); - ((MessagesController) c).openGame(msg.viaBotUserId != 0 ? msg.viaBotUserId : Td.getSenderUserId(msg), ((TdApi.MessageGame) msg.content).game, url, msg); - } else { - c.openLinkAlert(url, openParameters(currentContextId, view)); - } - } - if (answerText != null) { - if (showAlert || !isMessagesController) { - c.openOkAlert(context.context.tdlib().messageUsername(parent.getMessage()), answerText); - } else { - ((MessagesController) c).showCallbackToast(answerText); - } - } - }); + ViewController c = parent.context().navigation().getCurrentStackItem(); + boolean isMessagesController = c instanceof MessagesController; - break; - } - case TdApi.Error.CONSTRUCTOR: { - TdApi.Error error = (TdApi.Error) object; - if (error.code == 502) { - UI.showBotDown(context.context.tdlib().messageUsername(parent.getMessage())); + if (c == null || c.getChatId() != parent.getChatId()) { return; } - UI.showError(object); - context.context.tdlib().ui().post(() -> { - if (currentContextId == contextId) { - makeInactive(); + + if (!StringUtils.isEmpty(url)) { + if (isGame && isMessagesController) { + TdApi.Message msg = parent.getMessage(); + ((MessagesController) c).openGame(msg.viaBotUserId != 0 ? msg.viaBotUserId : Td.getSenderUserId(msg), ((TdApi.MessageGame) msg.content).game, url, msg); + } else { + c.openLinkAlert(url, openParameters(currentContextId, view)); } - }); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.GetCallbackQueryAnswer.class, TdApi.CallbackQueryAnswer.class); - context.context.tdlib().ui().post(() -> { - if (currentContextId == contextId) { - makeInactive(); + } + if (answerText != null) { + if (showAlert || !isMessagesController) { + c.openOkAlert(context.context.tdlib().messageUsername(parent.getMessage()), answerText); + } else { + ((MessagesController) c).showCallbackToast(answerText); } - }); - break; - } + } + }); } }; } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java index fe8e12ea26..26e75ab427 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java @@ -30,6 +30,9 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.SystemClock; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; import android.util.SparseIntArray; @@ -70,6 +73,7 @@ import org.thunderdog.challegram.loader.DoubleImageReceiver; import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.loader.Receiver; +import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.mediaview.MediaViewThumbLocation; import org.thunderdog.challegram.mediaview.data.MediaItem; @@ -78,10 +82,13 @@ import org.thunderdog.challegram.navigation.TooltipOverlayView; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibDelegate; +import org.thunderdog.challegram.telegram.TdlibEmojiManager; import org.thunderdog.challegram.telegram.TdlibSender; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.PropertyId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ThemeManager; @@ -100,6 +107,7 @@ import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.util.EmojiStatusHelper; import org.thunderdog.challegram.util.LanguageDetector; +import org.thunderdog.challegram.util.NonBubbleEmojiLayout; import org.thunderdog.challegram.util.ReactionsCounterDrawable; import org.thunderdog.challegram.util.TranslationCounterDrawable; import org.thunderdog.challegram.util.text.Counter; @@ -146,16 +154,20 @@ import me.vkryl.core.collection.LongList; import me.vkryl.core.collection.LongSet; import me.vkryl.core.lambda.CancellableRunnable; +import me.vkryl.core.lambda.FutureBool; +import me.vkryl.core.lambda.RunnableBool; import me.vkryl.core.lambda.RunnableData; import me.vkryl.core.reference.ReferenceList; import me.vkryl.td.ChatId; import me.vkryl.td.MessageId; import me.vkryl.td.Td; -public abstract class TGMessage implements InvalidateContentProvider, TdlibDelegate, FactorAnimator.Target, Comparable, Counter.Callback, TranslationsManager.Translatable { +public abstract class TGMessage implements InvalidateContentProvider, TdlibDelegate, FactorAnimator.Target, Comparable, Counter.Callback, TGAvatars.Callback, TranslationsManager.Translatable { private static final int MAXIMUM_CHANNEL_MERGE_TIME_DIFF = 150; private static final int MAXIMUM_COMMON_MERGE_TIME_DIFF = 900; + protected static final long TEXT_CROSS_FADE_DURATION_MS = 200L; + private static final int MAXIMUM_CHANNEL_MERGE_COUNT = 19; private static final int MAXIMUM_COMMON_MERGE_COUNT = 14; @@ -192,6 +204,7 @@ public abstract class TGMessage implements InvalidateContentProvider, TdlibDeleg private static final int FLAG_BEING_ADDED = 1 << 31; protected TdApi.Message msg; + protected final TdApi.SponsoredMessage sponsoredMessage; private int flags; protected int mergeTime, mergeIndex; @@ -214,6 +227,7 @@ public abstract class TGMessage implements InvalidateContentProvider, TdlibDeleg // header values private String date; + private @Nullable TdlibAccentColor hAuthorAccentColor; private @Nullable Text hAuthorNameT, hPsaTextT, hAuthorChatMark; private @Nullable Text hAdminNameT; private @Nullable Letters uBadge; @@ -221,20 +235,27 @@ public abstract class TGMessage implements InvalidateContentProvider, TdlibDeleg // counters - private final Counter viewCounter, replyCounter, shareCounter, isPinned; + private final Counter viewCounter, replyCounter, shareCounter, isPinned, isEdited, isRestricted, isUnsupported; private Counter shrinkedReactionsCounter, reactionsCounter; private final ReactionsCounterDrawable reactionsCounterDrawable; private final Counter isChannelHeaderCounter; - private float isChannelHeaderCounterX, isChannelHeaderCounterY; private boolean translatedCounterForceShow; private final Counter isTranslatedCounter; private final TranslationCounterDrawable isTranslatedCounterDrawable; - private float isTranslatedCounterX, isTranslatedCounterY; + + // counter last-draw positions + + private final RectF isChannelHeaderCounterLastDrawRect = new RectF(); + private final RectF isTranslatedCounterLastDrawRect = new RectF(); + private final RectF isRestrictedCounterLastDrawRect = new RectF(); + private final RectF isEditedCounterLastDrawRect = new RectF(); + // forward values private String fTime; + private TdlibAccentColor fAuthorNameAccentColor; private Text fAuthorNameT, fPsaTextT; private float fTimeWidth; @@ -271,7 +292,7 @@ public void onFactorChanged (int id, float factor, float fraction, FactorAnimato private final Path bubblePath, bubbleClipPath; private float topRightRadius, topLeftRadius, bottomLeftRadius, bottomRightRadius; - private final RectF bubblePathRect, bubbleClipPathRect; + protected final RectF bubblePathRect, bubbleClipPathRect; private boolean needSponsorSmallPadding; @@ -291,6 +312,14 @@ public void onFactorChanged (int id, float factor, float fraction, FactorAnimato private final TranslationsManager mTranslationsManager; protected TGMessage (MessagesManager manager, TdApi.Message msg) { + this(manager, msg, null); + } + + protected TGMessage (MessagesManager manager, long inChatId, TdApi.SponsoredMessage sponsoredMessage) { + this(manager, SponsoredMessageUtils.sponsoredToTd(inChatId, sponsoredMessage, manager.controller().tdlib()), sponsoredMessage); + } + + private TGMessage (MessagesManager manager, TdApi.Message msg, @Nullable TdApi.SponsoredMessage sponsoredMessage) { if (!initialized) { synchronized (TGMessage.class) { if (!initialized) { @@ -314,11 +343,14 @@ protected TGMessage (MessagesManager manager, TdApi.Message msg) { this.currentViews = new MultipleViewProvider(); this.currentViews.setContentProvider(this); this.msg = msg; + this.sponsoredMessage = sponsoredMessage; this.messageReactions = new TGReactions(this, tdlib, msg.interactionInfo != null ? msg.interactionInfo.reactions : null, new TGReactions.MessageReactionsDelegate() { @Override public void onClick (View v, TGReactions.MessageReactionEntry entry) { boolean hasReaction = messageReactions.hasReaction(entry.getReactionType()); - if (hasReaction || messagesController().callNonAnonymousProtection(getId() + entry.hashCode(), TGMessage.this, getReactionBubbleLocationProvider(entry))) { + if (Config.DISABLE_ANONYMOUS_NON_OWNER_REACTIONS && !hasReaction && tdlib.isAnonymousAdminNonCreator(msg.chatId)) { + showReactionBubbleTooltip(v, entry, Lang.getString(R.string.error_ANONYMOUS_REACTIONS_DISABLED)); + } else if (!Config.PROTECT_ANONYMOUS_REACTIONS || hasReaction || messagesController().callNonAnonymousProtection(getId() + entry.hashCode(), TGMessage.this, getReactionBubbleLocationProvider(entry))) { boolean needAnimation = messageReactions.toggleReaction(entry.getReactionType(), false, false, handler(v, entry, () -> { })); if (needAnimation) { @@ -349,34 +381,47 @@ public void onRebuildRequested () { } }); } + + @Override + public void onInvalidateReceiversRequested () { + runOnUiThreadOptional(() -> { + invalidateReactionFilesReceiver(); + }); + } }); this.commentButton = new TGCommentButton(this); - TdApi.MessageSender sender = msg.senderId; - if (tdlib.isSelfChat(msg.chatId)) { - flags |= FLAG_SELF_CHAT; - if (msg.forwardInfo != null) { - switch (msg.forwardInfo.origin.getConstructor()) { - case TdApi.MessageForwardOriginUser.CONSTRUCTOR: - sender = new TdApi.MessageSenderUser(((TdApi.MessageForwardOriginUser) msg.forwardInfo.origin).senderUserId); - break; - case TdApi.MessageForwardOriginChat.CONSTRUCTOR: - sender = new TdApi.MessageSenderChat(((TdApi.MessageForwardOriginChat) msg.forwardInfo.origin).senderChatId); - break; - case TdApi.MessageForwardOriginChannel.CONSTRUCTOR: - TdApi.MessageForwardOriginChannel info = (TdApi.MessageForwardOriginChannel) msg.forwardInfo.origin; - if ((msg.forwardInfo.fromChatId == 0 && msg.forwardInfo.fromMessageId == 0)) { - msg.forwardInfo.fromChatId = info.chatId; - msg.forwardInfo.fromMessageId = info.messageId; - } - break; - case TdApi.MessageForwardOriginHiddenUser.CONSTRUCTOR: - case TdApi.MessageForwardOriginMessageImport.CONSTRUCTOR: - break; + if (isSponsoredMessage()) { + this.sender = new TdlibSender(tdlib, msg.chatId, sponsoredMessage.sponsor); + } else { + TdApi.MessageSender sender = msg.senderId; + if (sender == null) { + throw new IllegalArgumentException(); + } + if (tdlib.isSelfChat(msg.chatId)) { + flags |= FLAG_SELF_CHAT; + if (msg.forwardInfo != null) { + switch (msg.forwardInfo.origin.getConstructor()) { + case TdApi.MessageOriginUser.CONSTRUCTOR: + sender = new TdApi.MessageSenderUser(((TdApi.MessageOriginUser) msg.forwardInfo.origin).senderUserId); + break; + case TdApi.MessageOriginChat.CONSTRUCTOR: + sender = new TdApi.MessageSenderChat(((TdApi.MessageOriginChat) msg.forwardInfo.origin).senderChatId); + break; + case TdApi.MessageOriginChannel.CONSTRUCTOR: + TdApi.MessageOriginChannel info = (TdApi.MessageOriginChannel) msg.forwardInfo.origin; + if ((msg.forwardInfo.fromChatId == 0 && msg.forwardInfo.fromMessageId == 0)) { + msg.forwardInfo.fromChatId = info.chatId; + msg.forwardInfo.fromMessageId = info.messageId; + } + break; + case TdApi.MessageOriginHiddenUser.CONSTRUCTOR: + break; + } } } + this.sender = new TdlibSender(tdlib, msg.chatId, sender, manager, !msg.isOutgoing && isDemoChat()); } - this.sender = new TdlibSender(tdlib, msg.chatId, sender, manager, !msg.isOutgoing && isDemoChat()); this.isPinned = new Counter.Builder() .noBackground() @@ -384,6 +429,28 @@ public void onRebuildRequested () { .callback(this) .drawable(R.drawable.deproko_baseline_pin_14, 14f, 0f, Gravity.CENTER_HORIZONTAL) .build(); + this.isEdited = new Counter.Builder() + .noBackground() + .allBold(false) + .callback(this) + .drawable(R.drawable.baseline_edit_12, 12f, 0f, Gravity.CENTER_HORIZONTAL) + .build(); + this.isEdited.showHide(true, false); + this.isRestricted = new Counter.Builder() + .noBackground() + .allBold(false) + .callback(this) + .drawable(R.drawable.baseline_warning_14, 14f, 0f, Gravity.CENTER_HORIZONTAL) + .colorSet(() -> Theme.getColor(ColorId.messageNegativeLine)) + .build(); + this.isRestricted.showHide(true, false); + this.isUnsupported = new Counter.Builder() + .noBackground() + .allBold(false) + .callback(this) + .drawable(R.drawable.baseline_info_14, 14f, 0f, Gravity.CENTER_HORIZONTAL) + .build(); + this.isUnsupported.showHide(true, false); this.isChannelHeaderCounter = new Counter.Builder() .noBackground() .allBold(false) @@ -391,7 +458,7 @@ public void onRebuildRequested () { .drawable(R.drawable.baseline_bullhorn_16, 16f, 0, Gravity.CENTER_HORIZONTAL) .build(); if (msg.isChannelPost || (msg.forwardInfo != null && ( - msg.forwardInfo.origin.getConstructor() == TdApi.MessageForwardOriginChannel.CONSTRUCTOR || + msg.forwardInfo.origin.getConstructor() == TdApi.MessageOriginChannel.CONSTRUCTOR || TD.getViewCount(msg.interactionInfo) > 1 || tdlib.isChannel(msg.forwardInfo.fromChatId) || this.sender.isChannel() @@ -441,9 +508,9 @@ public void onRebuildRequested () { this.isTranslatedCounterDrawable = new TranslationCounterDrawable(Drawables.get(R.drawable.baseline_translate_14)); this.isTranslatedCounterDrawable.setColors( - msg.isOutgoing ? ColorId.bubbleOut_time: ColorId.bubbleIn_time, - msg.isOutgoing ? ColorId.bubbleOut_time: ColorId.bubbleIn_time, - msg.isOutgoing ? ColorId.bubbleOut_textLink: ColorId.bubbleIn_textLink + msg.isOutgoing ? ColorId.bubbleOut_time : ColorId.bubbleIn_time, + msg.isOutgoing ? ColorId.bubbleOut_time : ColorId.bubbleIn_time, + msg.isOutgoing ? ColorId.bubbleOut_textLink : ColorId.bubbleIn_textLink ); this.isTranslatedCounter = new Counter.Builder() .noBackground() @@ -474,21 +541,32 @@ public void onRebuildRequested () { overlayViews = null; } - if (useForward() || forceForwardedInfo()) { + if (useForward() || forceForwardOrImportInfo()) { loadForward(); } ThreadInfo messageThread = messagesController().getMessageThread(); - if (msg.replyToMessageId != 0 && (messageThread == null || !messageThread.isRootMessage(msg.replyToMessageId))) { - loadReply(); + if (msg.replyTo != null && (messageThread == null || !messageThread.isRootMessage(msg.replyTo))) { + if (msg.replyTo.getConstructor() == TdApi.MessageReplyToMessage.CONSTRUCTOR) { // TODO: support replies to stories + loadReply(); + } } - if (isHot() && needHotTimer() && msg.selfDestructIn < msg.selfDestructTime) { + if (isHot() && needHotTimer() && isHotOpened()) { startHotTimer(false); } computeQuickButtons(); checkHighlightedText(); + + UI.post(() -> updateReactionAvatars(false)); + + this.isHiddenByFilter = new BoolAnimator(IS_HIDDEN_BY_MESSAGE_FILTER_ANIMATOR_ID, (a, b, c, d) -> { + if (BitwiseUtils.hasFlag(flags, FLAG_LAYOUT_BUILT)) { + notifyBubbleChanged(); + invalidate(); + } + }, AnimatorUtils.DECELERATE_INTERPOLATOR, 320L); } private static @NonNull T nonNull (@Nullable T value) { @@ -530,20 +608,20 @@ public final void navigateTo (ViewController c) { private String genTime () { if (isEventLog()) { return Lang.getRelativeTimestampShort(msg.date, TimeUnit.SECONDS); - } else if (isSponsored()) { - return Lang.getString(R.string.SponsoredSign); + } else if (isSponsoredMessage()) { + return Lang.getString(sponsoredMessage.isRecommended ? R.string.RecommendedSign : R.string.SponsoredSign); } StringBuilder b = new StringBuilder(); String signature; if (isChannel() && !StringUtils.isEmpty(msg.authorSignature)) { signature = msg.authorSignature; - } else if (forceForwardedInfo() && msg.forwardInfo != null) { + } else if (forceForwardOrImportInfo() && msg.forwardInfo != null) { switch (msg.forwardInfo.origin.getConstructor()) { - case TdApi.MessageForwardOriginChannel.CONSTRUCTOR: - signature = ((TdApi.MessageForwardOriginChannel) msg.forwardInfo.origin).authorSignature; + case TdApi.MessageOriginChannel.CONSTRUCTOR: + signature = ((TdApi.MessageOriginChannel) msg.forwardInfo.origin).authorSignature; break; - case TdApi.MessageForwardOriginChat.CONSTRUCTOR: - signature = ((TdApi.MessageForwardOriginChat) msg.forwardInfo.origin).authorSignature; + case TdApi.MessageOriginChat.CONSTRUCTOR: + signature = ((TdApi.MessageOriginChat) msg.forwardInfo.origin).authorSignature; break; default: signature = null; @@ -565,7 +643,7 @@ private String genTime () { if (!useBubbles() && needAdminSign()) { b.append(getAdministratorSign()).append(" "); } - if (msg.forwardInfo != null && msg.forwardInfo.origin.getConstructor() == TdApi.MessageForwardOriginMessageImport.CONSTRUCTOR) { + if (isImported()) { b.append(Lang.getString(R.string.ImportedSign)).append(" "); } if (TD.isFailed(msg)) { @@ -577,13 +655,15 @@ private String genTime () { } } else if ((flags & FLAG_SELF_CHAT) != 0 && !isOutgoing() && msg.forwardInfo != null) { int date = replaceTimeWithEditTime() ? msg.editDate : msg.date; - if (msg.forwardInfo.date != 0) - date = msg.forwardInfo.date; + final int forwardOrImportDate = getForwardOrImportDate(); + if (forwardOrImportDate != 0) + date = forwardOrImportDate; if (date != 0) { b.append(Lang.getRelativeTimestampShort(date, TimeUnit.SECONDS)); } - } else if (forceForwardedInfo() && msg.forwardInfo.date != 0) { - b.append(DateUtils.isSameDay(msg.forwardInfo.date, msg.date) ? Lang.time(msg.forwardInfo.date, TimeUnit.SECONDS) : Lang.getRelativeTimestampShort(msg.forwardInfo.date, TimeUnit.SECONDS)); + } else if (forceForwardOrImportInfo() && getForwardOrImportDate() != 0) { + int date = getForwardOrImportDate(); + b.append(DateUtils.isSameDay(date, msg.date) ? Lang.time(date, TimeUnit.SECONDS) : Lang.getRelativeTimestampShort(date, TimeUnit.SECONDS)); } else { int date = replaceTimeWithEditTime() ? msg.editDate : msg.date; if (date != 0) { @@ -647,7 +727,7 @@ public final void forceAvatarWhenMerging (boolean value) { public final boolean mergeWith (@Nullable TGMessage top, boolean isBottom) { if (top != null) { top.setNeedExtraPadding(false); - top.setNeedExtraPresponsoredPadding(isSponsored()); + top.setNeedExtraPresponsoredPadding(isSponsoredMessage()); flags |= MESSAGE_FLAG_HAS_OLDER_MESSAGE; } else { flags &= ~MESSAGE_FLAG_HAS_OLDER_MESSAGE; @@ -668,7 +748,7 @@ public final boolean mergeWith (@Nullable TGMessage top, boolean isBottom) { top.setIsBottom(true); } setHeaderEnabled(!headerDisabled()); - if ((top != null || getDate() != 0 || isScheduled()) && !isSponsored() && (!isBelowHeader || messagesController().areScheduledOnly())) { + if ((top != null || getDate() != 0 || isScheduled()) && !isSponsoredMessage() && (!isBelowHeader || messagesController().areScheduledOnly())) { flags |= FLAG_SHOW_DATE; setDate(genDate()); } else { @@ -683,7 +763,7 @@ public final boolean mergeWith (@Nullable TGMessage top, boolean isBottom) { boolean isChannel = isChannel(); TdApi.Message topMessage = top.getMessage(); - if (top.headerDisabled() || (flags & FLAG_SHOW_BADGE) != 0 || !tdlib.isSameSender(topMessage, msg) || !TD.isSameSource(topMessage, msg, forceForwardedInfo()) || topMessage.viaBotUserId != msg.viaBotUserId || !StringUtils.equalsOrBothEmpty(topMessage.authorSignature, msg.authorSignature) || mergeDisabled() || (useBubbles ? top.isOutgoingBubble() != isOutgoingBubble() : top.getMessage().mediaAlbumId != msg.mediaAlbumId || msg.mediaAlbumId != 0)) { + if (top.headerDisabled() || (flags & FLAG_SHOW_BADGE) != 0 || !tdlib.isSameSender(topMessage, msg) || !TD.isSameSource(topMessage, msg, forceForwardOrImportInfo()) || topMessage.viaBotUserId != msg.viaBotUserId || !StringUtils.equalsOrBothEmpty(topMessage.authorSignature, msg.authorSignature) || mergeDisabled() || (useBubbles ? top.isOutgoingBubble() != isOutgoingBubble() : top.getMessage().mediaAlbumId != msg.mediaAlbumId || msg.mediaAlbumId != 0)) { setHeaderEnabled(!headerDisabled()); top.setIsBottom(true); return false; @@ -705,7 +785,7 @@ public final boolean mergeWith (@Nullable TGMessage top, boolean isBottom) { maxIndex = MAXIMUM_COMMON_MERGE_COUNT; } - if (!(useBubbles && isChannel && msg.forwardInfo != null && msg.forwardInfo.origin.getConstructor() == TdApi.MessageForwardOriginUser.CONSTRUCTOR) && + if (!(useBubbles && isChannel && msg.forwardInfo != null && msg.forwardInfo.origin.getConstructor() == TdApi.MessageOriginUser.CONSTRUCTOR) && msg.date - top.getMergeTime() < maxTimeDiff && top.getMergeIndex() < maxIndex) { flags &= ~FLAG_HEADER_ENABLED; mergeTime = top.getMergeTime(); @@ -793,7 +873,7 @@ private int computeBubbleTop () { private int getAuthorWidth () { return hAuthorNameT != null ? - hAuthorNameT.getWidth() + (hAuthorEmojiStatus != null ? hAuthorEmojiStatus.getWidth(Screen.dp(3)): 0) + (hAuthorChatMark != null ? hAuthorChatMark.getWidth() + Screen.dp(16f) : 0) : + hAuthorNameT.getWidth() + (hAuthorEmojiStatus != null ? hAuthorEmojiStatus.getWidth(Screen.dp(3)) : 0) + (hAuthorChatMark != null ? hAuthorChatMark.getWidth() + Screen.dp(16f) : 0) : needName(true) ? -Screen.dp(3f) : 0; } @@ -821,7 +901,7 @@ private int computeBubbleWidth () { if (useForward()) { if (allowMessageHorizontalExtend()) { - boolean isPsa = isPsa() && !forceForwardedInfo(); + boolean isPsa = isPsa() && !forceForwardOrImportInfo(); float forwardWidth = Math.max((isPsa && fPsaTextT != null ? fAuthorNameT.getWidth() : fAuthorNameT != null ? fAuthorNameT.getWidth() : 0) + fTimeWidth + Screen.dp(6f) + (getViewCountMode() == VIEW_COUNT_FORWARD ? viewCounter.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN + COUNTER_ADD_MARGIN)) + shareCounter.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN + COUNTER_ADD_MARGIN)) : 0), @@ -846,17 +926,37 @@ private int computeBubbleWidth () { } protected final boolean useForward () { - // && !((flags & FLAG_SELF_CHAT) == 0 && msg.forwardInfo.origin.getConstructor() != TdApi.MessageForwardOriginChannel.CONSTRUCTOR && msg.content != null && msg.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR) - return msg.forwardInfo != null && (!useBubbles() || !separateReplyFromBubble()) && !forceForwardedInfo(); + // && !((flags & FLAG_SELF_CHAT) == 0 && msg.forwardInfo.origin.getConstructor() != TdApi.MessageOriginChannel.CONSTRUCTOR && msg.content != null && msg.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR) + return msg.forwardInfo != null && (!useBubbles() || !separateReplyFromBubble()) && !forceForwardOrImportInfo(); + } + + + + // + + private static final int IS_HIDDEN_BY_MESSAGE_FILTER_ANIMATOR_ID = 2; + + private static final int HIDDEN_BY_MESSAGE_FILTER_HEIGHT = 35; + + private final BoolAnimator isHiddenByFilter; + + public void setIsHiddenByMessagesFilter (boolean hidden, boolean animated) { + isHiddenByFilter.setValue(hidden && !isSponsoredMessage(), BitwiseUtils.hasFlag(flags, FLAG_LAYOUT_BUILT) && currentViews.hasAnyTargetToInvalidate() && UI.inUiThread() && controller() != null && controller().isFocused() && animated); } + public boolean isHiddenByMessagesFilter () { + return isHiddenByFilter.getValue(); + } + + + private static final int VIEW_COUNT_HIDDEN = 0; private static final int VIEW_COUNT_MAIN = 1; private static final int VIEW_COUNT_FORWARD = 2; private int getViewCountMode () { if (viewCounter != null) { - if (useForward() && !msg.isChannelPost && msg.forwardInfo != null && msg.forwardInfo.origin.getConstructor() == TdApi.MessageForwardOriginChannel.CONSTRUCTOR) { + if (useForward() && !msg.isChannelPost && msg.forwardInfo != null && msg.forwardInfo.origin.getConstructor() == TdApi.MessageOriginChannel.CONSTRUCTOR) { return VIEW_COUNT_FORWARD; } if (useBubbles() || BitwiseUtils.hasFlag(flags, FLAG_HEADER_ENABLED)) { @@ -889,7 +989,7 @@ protected final int getCommentButtonViewMode () { } protected final boolean needCommentButton () { - if (isScheduled() || isSponsored() || !allowInteraction()) { + if (isScheduled() || isSponsoredMessage() || !allowInteraction()) { return false; } if (isChannel()) { @@ -957,12 +1057,11 @@ public final void openMessageThread (@NonNull TdApi.GetMessageThread query, @Nul TdApi.MessageThreadInfo messageThread = (TdApi.MessageThreadInfo) result; ThreadInfo threadInfo = ThreadInfo.openedFromChat(tdlib, messageThread, getChatId()); if (Config.SHOW_CHANNEL_POST_REPLY_INFO_IN_COMMENTS && isChannel() && - msg.replyInChatId != 0 && msg.replyToMessageId != 0 && + msg.replyTo != null && msg.chatId == query.chatId && isDescendantOrSelf(query.messageId)) { TdApi.Message message = threadInfo.getOldestMessage(); - if (message != null && message.replyToMessageId == 0 && tdlib.isChannelAutoForward(message)) { - message.replyInChatId = msg.replyInChatId; - message.replyToMessageId = msg.replyToMessageId; + if (message != null && message.replyTo == null && tdlib.isChannelAutoForward(message)) { + message.replyTo = msg.replyTo; } } TdlibUi.ChatOpenParameters params = new TdlibUi.ChatOpenParameters().keepStack().messageThread(threadInfo).after(chatId -> { @@ -990,9 +1089,9 @@ public final void openMessageThread (@NonNull TdApi.GetMessageThread query, @Nul boolean needAnimateChanges = needAnimateChanges(); openingComments.setValue(false, needAnimateChanges); if (isChannel()) { - UI.showToast(R.string.ChannelPostDeleted, Toast.LENGTH_SHORT); + showCommentButtonError(Lang.getString(R.string.ChannelPostDeleted)); } else { - UI.showError(result); + showCommentButtonError(TD.toErrorString(result)); } break; } @@ -1002,13 +1101,24 @@ public final void openMessageThread (@NonNull TdApi.GetMessageThread query, @Nul break; } openingComments.setValue(false, needAnimateChanges()); - UI.showError(result); + showCommentButtonError(TD.toErrorString(result)); break; } } })); } + private void showCommentButtonError (String text) { + View view = findCurrentView(); + if (!needCommentButton() || view == null) { + UI.showToast(text, Toast.LENGTH_SHORT); + return; + } + buildContentHint(view, (targetView, outRect) -> + commentButton.getRect(outRect), false + ).show(tdlib, text).hideDelayed(); + } + private int computeBubbleHeight () { int height = getContentHeight(); if (replyData != null && !alignReplyHorizontally()) { @@ -1263,8 +1373,10 @@ protected final int getExtraPadding () { } public int computeHeight () { + final int headerPadding = getHeaderPadding(); + final int extraPadding = getExtraPadding(); if (useBubbles()) { - int height = bottomContentEdge + getPaddingBottom() + getExtraPadding(); + int height = bottomContentEdge + getPaddingBottom() + extraPadding; if (inlineKeyboard != null && !inlineKeyboard.isEmpty()) { height += inlineKeyboard.getHeight() + TGInlineKeyboard.getButtonSpacing(); } @@ -1278,9 +1390,9 @@ public int computeHeight () { if (commentButton.isBubble()) { height += commentButton.getAnimatedHeight(Screen.dp(5f), commentButton.getVisibility()); } - return height; + return MathUtils.fromTo(height, Screen.dp(HIDDEN_BY_MESSAGE_FILTER_HEIGHT) + extraPadding + headerPadding, isHiddenByFilter.getFloatValue()); } else { - int height = pContentY + getContentHeight() + getPaddingBottom() + getExtraPadding(); + int height = pContentY + getContentHeight() + getPaddingBottom() + extraPadding; if (inlineKeyboard != null && !inlineKeyboard.isEmpty()) { height += inlineKeyboard.getHeight() + xPaddingBottom; } @@ -1294,7 +1406,7 @@ public int computeHeight () { if (commentButton.isVisible() && commentButton.isInline()) { height += commentButton.getAnimatedHeight(useReactionBubbles ? -Screen.dp(2f) : 0, commentButton.getVisibility()); } - return height; + return MathUtils.fromTo(height, Screen.dp(HIDDEN_BY_MESSAGE_FILTER_HEIGHT) + extraPadding + headerPadding, isHiddenByFilter.getFloatValue()); } } @@ -1336,10 +1448,28 @@ private boolean shouldShowEdited () { return !headerDisabled() && (isEdited() || isBeingEdited()) && msg.viaBotUserId == 0 && !sender.isBot() && !sender.isServiceAccount() && (useBubbles() ? useBubbleTime() : (!isOutgoing() || hasHeader() || !shouldShowTicks())) && (getViewCount() > 0 || !isEventLog()); } + private boolean shouldShowMessageRestrictedWarning () { + return BitwiseUtils.hasFlag(flags, FLAG_UNSUPPORTED) || isRestrictedByTelegram(); + } + private boolean needAvatar () { if (!useBubbles()) { return true; } + if (isSponsoredMessage()) { + switch (sponsoredMessage.sponsor.type.getConstructor()) { + case TdApi.MessageSponsorTypeWebsite.CONSTRUCTOR: + case TdApi.MessageSponsorTypeWebApp.CONSTRUCTOR: + case TdApi.MessageSponsorTypePrivateChannel.CONSTRUCTOR: + return false; + case TdApi.MessageSponsorTypeBot.CONSTRUCTOR: + case TdApi.MessageSponsorTypePublicChannel.CONSTRUCTOR: + return true; + default: + Td.assertMessageSponsorType_cdabde01(); + throw Td.unsupported(sponsoredMessage.sponsor.type); + } + } if (isThreadHeader() && messagesController().getMessageThread().areComments()) { return false; } @@ -1387,9 +1517,9 @@ private boolean needName (boolean allowVia) { if (!useBubble() || separateReplyFromBubble()) { return false; } - if (isSponsored() && useBubbles()) + if (isSponsoredMessage() && useBubbles()) return true; - if (isPsa() && forceForwardedInfo()) + if (isPsa() && forceForwardOrImportInfo()) return true; if (isOutgoing() && (sender.isAnonymousGroupAdmin() || sender.isChannel())) return true; @@ -1489,7 +1619,7 @@ private void drawBubbleShadow (Canvas c, float factor) { paint.setAlpha(255); - c.save(); + final int restoreToCount = Views.save(c); offset = Screen.dp(18f); float shadowLeft, shadowTop, shadowRight, shadowBottom; @@ -1541,7 +1671,7 @@ private void drawBubbleShadow (Canvas c, float factor) { cx = tx; cy = ty; } - c.restore(); + Views.restore(c, restoreToCount); } public static void drawCornerFixes (Canvas c, TGMessage source, float factor, float left, float top, float right, float bottom, float topLeftRadius, float topRightRadius, float bottomRightRadius, float bottomLeftRadius) { @@ -1812,6 +1942,12 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat checkEdges(); + final float isHiddenFactor = isHiddenByFilter.getFloatValue(); + if (isHiddenFactor == 1f) { + drawHiddenMessage(view, c, isHiddenFactor); + return; + } + // "Unread messages" / "Discussion started" badge if ((flags & FLAG_SHOW_BADGE) != 0) { int top = 0; @@ -1908,7 +2044,7 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat int top = bottom - commentButton.getAnimatedHeight(0, commentButton.getVisibility()); if (needCommentButtonSeparator()) { int separatorColor = ColorUtils.alphaColor(0.15f * commentButton.getVisibility(), getDecentColor()); - Paint separatorPaint = Config.COMMENTS_INLINE_BUTTON_SEPARATOR_1PX ? Paints.strokeSeparatorPaint(separatorColor) : Paints.strokeSmallPaint(separatorColor); + Paint separatorPaint = Paints.strokeSmallPaint(separatorColor); c.drawLine(left + Screen.dp(7f), top, right - Screen.dp(7f), top, separatorPaint); } commentButton.draw(view, c, view, left, top, right, bottom); @@ -1937,7 +2073,7 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat if (needName(true)) { int left = useBubbles ? getActualLeftContentEdge() + xBubblePadding + xBubblePaddingSmall : xContentLeft; int top = useBubbles ? topContentEdge + xBubbleNameTop : xNameTop + getHeaderPadding(); - boolean isPsa = isPsa() && forceForwardedInfo(); + boolean isPsa = isPsa() && forceForwardOrImportInfo(); if (hAuthorNameT != null) { int newTop = useBubbles ? topContentEdge + Screen.dp(9f) : getHeaderPadding() + Screen.dp(1f); if (isPsa && hPsaTextT != null) { @@ -1949,7 +2085,8 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat hAuthorEmojiStatus.draw(c, left + hAuthorNameT.getWidth() + Screen.dp(3), newTop, 1f, view.getEmojiStatusReceiver()); } if (sender.hasChatMark() && hAuthorChatMark != null) { - int cmLeft = left + hAuthorNameT.getWidth() + Screen.dp(6f); + int cmLeft = left + hAuthorNameT.getWidth() + Screen.dp(3f) + + (hAuthorEmojiStatus != null ? hAuthorEmojiStatus.getWidth(Screen.dp(3)) : 0); RectF rct = Paints.getRectF(); rct.set(cmLeft, newTop, cmLeft + hAuthorChatMark.getWidth() + Screen.dp(8f), newTop + hAuthorNameT.getLineHeight(false)); c.drawRoundRect(rct, Screen.dp(2f), Screen.dp(2f), Paints.getProgressPaint(Theme.getColor(ColorId.textNegative), Screen.dp(1.5f))); @@ -1964,7 +2101,7 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat hAdminNameT.draw(c, right, top - Screen.dp(12f)); } if (useBubbles && needDrawChannelIconInHeader() && hAuthorNameT != null) { - isChannelHeaderCounter.draw(c, isChannelHeaderCounterX = (right - Screen.dp(6)), isChannelHeaderCounterY = (top - Screen.dp(5)), Gravity.RIGHT | Gravity.BOTTOM, 1f, view, isOutgoing() ? ColorId.bubbleOut_time: ColorId.bubbleIn_time); + isChannelHeaderCounter.draw(c, (right - Screen.dp(6)), (top - Screen.dp(5)), Gravity.RIGHT | Gravity.BOTTOM, 1f, view, isOutgoing() ? ColorId.bubbleOut_time : ColorId.bubbleIn_time, isChannelHeaderCounterLastDrawRect); } } } @@ -1984,7 +2121,7 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat int viewsX = pTicksLeft - Icons.getSingleTickWidth() + ((flags & FLAG_HEADER_ENABLED) != 0 ? 0 : Screen.dp(1f)) - Screen.dp(Icons.TICKS_SHIFT_X); if (needDrawChannelIconInHeader() && hAuthorNameT != null) { - isChannelHeaderCounter.draw(c, isChannelHeaderCounterX = ((isSending() ? clockX: viewsX) + Screen.dp(7)), isChannelHeaderCounterY = (pTicksTop + Screen.dp(5)), Gravity.LEFT, 1f, view, ColorId.iconLight); + isChannelHeaderCounter.draw(c, ((isSending() ? clockX : viewsX) + Screen.dp(7)), (pTicksTop + Screen.dp(5)), Gravity.LEFT, 1f, view, ColorId.iconLight, isChannelHeaderCounterLastDrawRect); clockX -= isChannelHeaderCounter.getScaledWidth(Screen.dp(1)); viewsX -= isChannelHeaderCounter.getScaledWidth(Screen.dp(1)); } @@ -2006,14 +2143,24 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat // Edited if (shouldShowEdited()) { - // right -= Icons.getEditedIconWidth(); - right -= Icons.getEditedIconWidth(); if (isBeingEdited()) { + right -= Icons.getEditedIconWidth(); + right -= Screen.dp(COUNTER_ADD_MARGIN); Drawables.draw(c, Icons.getClockIcon(ColorId.iconLight), pTicksLeft - (shouldShowTicks() ? Icons.getSingleTickWidth() + Screen.dp(2.5f) : 0) - Icons.getEditedIconWidth() - Screen.dp(6f), pTicksTop - Screen.dp(5f), Paints.getIconLightPorterDuffPaint()); } else { - Drawables.draw(c, view.getSparseDrawable(R.drawable.baseline_edit_12, ColorId.NONE), right, pTicksTop, Paints.getIconLightPorterDuffPaint()); + isEdited.draw(c, right, top, Gravity.RIGHT, 1f, view, ColorId.iconLight, isEditedCounterLastDrawRect); + right -= isEdited.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)); + } + } + + if (shouldShowMessageRestrictedWarning()) { + if (isRestrictedByTelegram()) { + isRestricted.draw(c, right, top, Gravity.RIGHT, 1f, view, 0, isRestrictedCounterLastDrawRect); + right -= isRestricted.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)); + } else { + isUnsupported.draw(c, right, top, Gravity.RIGHT, 1f, view, ColorId.iconLight, isRestrictedCounterLastDrawRect); + right -= isUnsupported.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)); } - right -= Screen.dp(COUNTER_ADD_MARGIN); } isPinned.draw(c, right, top, Gravity.RIGHT, 1f, view, getTimePartIconColorId()); @@ -2033,8 +2180,7 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat } if (translationStyleMode() == Settings.TRANSLATE_MODE_INLINE) { - isTranslatedCounter.draw(c, right, isTranslatedCounterY = top, Gravity.RIGHT, 1f); - isTranslatedCounterX = right - Screen.dp(10); + isTranslatedCounter.draw(c, right, top, Gravity.RIGHT, 1f, isTranslatedCounterLastDrawRect); right -= isTranslatedCounter.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN + COUNTER_ADD_MARGIN)); } if (reactionsDrawMode == REACTIONS_DRAW_MODE_FLAT) { @@ -2172,13 +2318,15 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat int forwardTextTop = useBubbles ? pContentY - getBubbleForwardOffset() + Screen.dp(15f) : forwardY + Screen.dp(16f); // forward author and time - boolean isPsa = isPsa() && !forceForwardedInfo(); + boolean isPsa = isPsa() && !forceForwardOrImportInfo(); int nameColor = isPsa ? getChatAuthorPsaColor() : getChatAuthorColor(); int forwardTextLeft = getForwardAuthorNameLeft(); int forwardX = forwardTextLeft + (isPsa ? (fPsaTextT != null ? fPsaTextT.getWidth() : 0) : (fAuthorNameT != null ? fAuthorNameT.getWidth() : 0)) + Screen.dp(6f); TextPaint mTimePaint = useBubbles ? Paints.colorPaint(mTimeBubble(), getDecentColor()) : mTime(true); - c.drawText(fTime, forwardX, forwardTextTop, mTimePaint); + if (fTime != null) { + c.drawText(fTime, forwardX, forwardTextTop, mTimePaint); + } if (getViewCountMode() == VIEW_COUNT_FORWARD) { forwardX += Screen.dp(2f) + fTimeWidth + Screen.dp(COUNTER_ADD_MARGIN); int iconTop = forwardTextTop - Screen.dp(3f); @@ -2233,8 +2381,8 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat RectF rectF = Paints.getRectF(); rectF.set(lineLeft, lineTop, lineRight, lineBottom); - final int lineColor = getVerticalLineColor(); - c.drawRoundRect(rectF, lineWidth / 2, lineWidth / 2, Paints.fillingPaint(lineColor)); + final int lineColor = APPLY_ACCENT_TO_FORWARDS && !isOutgoingBubble() && fAuthorNameAccentColor != null ? fAuthorNameAccentColor.getVerticalLineColor() : getVerticalLineColor(); + c.drawRoundRect(rectF, lineWidth / 2f, lineWidth / 2f, Paints.fillingPaint(lineColor)); if (mergeTop) { c.drawRect(lineLeft, lineTop, lineRight, lineTop + lineWidth, Paints.fillingPaint(lineColor)); @@ -2274,7 +2422,7 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat endX = viewWidth - startX; } - if (useBubbles && isForward() && !forceForwardedInfo()) { + if (useBubbles && isForward() && !forceForwardOrImportInfo()) { startX += xTextPadding; } @@ -2296,10 +2444,23 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat startSetReactionAnimationIfReady(); highlightUnreadReactionsIfNeeded(); + if (isHiddenFactor > 0f) { + drawHiddenMessage(view, c, isHiddenFactor); + } + } + + public final void drawHiddenMessage (MessageView view, Canvas c, float isHiddenFactor) { + final int viewWidth = view.getMeasuredWidth(); + final int viewHeight = view.getMeasuredHeight(); + final int y = getHeaderPadding(); + + c.drawRect(0, y, viewWidth, viewHeight, Paints.fillingPaint(ColorUtils.alphaColor(isHiddenFactor, Theme.getColor(ColorId.filling)))); + // c.drawRect(0, y, viewWidth, y + 1, Paints.fillingPaint(ColorUtils.alphaColor(isHiddenFactor, Theme.getColor(ColorId.separator)))); + c.drawRect(0, viewHeight - 1, viewWidth, viewHeight, Paints.fillingPaint(ColorUtils.alphaColor(isHiddenFactor, Theme.getColor(ColorId.separator)))); } protected final boolean needColoredNames () { - return !msg.isOutgoing && (TD.isMultiChat(chat) || isDemoGroupChat()); + return !isOutgoingBubble(); } private int getInternalBubbleStartX () { @@ -2414,7 +2575,7 @@ private void setViewAttached (boolean isAttached) { flags = BitwiseUtils.setFlag(flags, FLAG_ATTACHED, isAttached); onMessageAttachStateChange(isAttached); if (isAttached) { - manager.viewMessages(); + manager.viewMessages(false); } } } @@ -2625,6 +2786,10 @@ public final void invalidateAvatarsReceiver () { performWithViews(view -> requestCommentsResources(view.getAvatarsReceiver(), true)); } + public final void invalidateReactionFilesReceiver () { + performWithViews(view -> requestReactions(view.getReactionsComplexReceiver())); + } + public final void invalidateTextMediaReceiver (@NonNull Text text, @Nullable TextMedia textMedia) { performWithViews(view -> view.invalidateTextMediaReceiver(this, text, textMedia)); } @@ -2648,7 +2813,7 @@ public boolean allowLongPress (float x, float y) { @CallSuper public boolean performLongPress (View view, float x, float y) { - if (isSponsored()) { + if (isSponsoredMessage()) { return false; } boolean result = false; @@ -2662,6 +2827,12 @@ public boolean performLongPress (View view, float x, float y) { result = footerText.performLongPress(view) || result; } clickHelper.cancel(view, x, y); + if (hAuthorNameT != null) { + hAuthorNameT.cancelTouch(); + } + if (fAuthorNameT != null) { + fAuthorNameT.cancelTouch(); + } return result; } @@ -2669,19 +2840,38 @@ public boolean shouldIgnoreTap (MotionEvent e) { return e.getY() < findTopEdge(); } + private static boolean checkClickOnRect (RectF rectF, float x, float y, float accuracy) { + RectF rect = Paints.getRectF(); + rect.set(rectF); + rect.inset(-accuracy, -accuracy); + return rect.contains(x, y); + } + private int getClickType (MessageView view, float x, float y) { if (isTranslated()) { - if (MathUtils.distance(isTranslatedCounterX, isTranslatedCounterY, x, y) < Screen.dp(8)) { + if (checkClickOnRect(isTranslatedCounterLastDrawRect, x, y, Screen.dp(4))) { return CLICK_TYPE_TRANSLATE_MESSAGE_ICON; } } if (needDrawChannelIconInHeader() && hAuthorNameT != null) { - if (MathUtils.distance(isChannelHeaderCounterX, isChannelHeaderCounterY, x, y) < Screen.dp(8)) { + if (checkClickOnRect(isChannelHeaderCounterLastDrawRect, x, y, Screen.dp(4))) { return CLICK_TYPE_CHANNEL_MESSAGE_ICON; } } + if (shouldShowMessageRestrictedWarning()) { + if (checkClickOnRect(isRestrictedCounterLastDrawRect, x, y, Screen.dp(4))) { + return CLICK_TYPE_MESSAGE_RESTRICTED_ICON; + } + } + + if (shouldShowEdited()) { + if (checkClickOnRect(isEditedCounterLastDrawRect, x, y, Screen.dp(4))) { + return CLICK_TYPE_MESSAGE_EDITED_ICON; + } + } + if (replyData != null && replyData.isInside(x, y, useBubbles() && !useBubble())) { return CLICK_TYPE_REPLY; } @@ -2709,24 +2899,51 @@ public void onClickAt (View view, float x, float y) { openMessageFromChannel(); break; } + case CLICK_TYPE_MESSAGE_RESTRICTED_ICON: { + showMessageTooltip((targetView, outRect) -> { + isRestrictedCounterLastDrawRect.round(outRect); + outRect.top -= Screen.dp(6); + }, Lang.getString(isRestrictedByTelegram() ? + R.string.MessageRestrictedByTelegram : + R.string.MessageUnsupportedHint), 2500); + break; + } + case CLICK_TYPE_MESSAGE_EDITED_ICON: { + showMessageTooltip((targetView, outRect) -> { + isEditedCounterLastDrawRect.round(outRect); + outRect.top -= Screen.dp(6); + }, + Lang.getRelativeDate( + getEditDate(), TimeUnit.SECONDS, + tdlib.currentTimeMillis(), TimeUnit.MILLISECONDS, + true, 60, R.string.message_edited, false + ), + 2500); + break; + } case CLICK_TYPE_REPLY: { - if (replyData != null && replyData.hasValidMessage()) { - if (msg.replyInChatId != msg.chatId) { - if (isMessageThread() && isThreadHeader()) { - tdlib.ui().openMessage(controller(), msg.replyInChatId, new MessageId(msg.replyInChatId, msg.replyToMessageId), openParameters()); + if (msg.replyTo != null && msg.replyTo.getConstructor() == TdApi.MessageReplyToMessage.CONSTRUCTOR) { + TdApi.MessageReplyToMessage replyToMessage = (TdApi.MessageReplyToMessage) msg.replyTo; + if (replyData != null && replyData.getError() != null) { + buildContentHint(view, getReplyLocationProvider(), false).show(tdlib, replyData.toErrorText()); + } else { + if (replyToMessage.chatId != msg.chatId) { + if (replyToMessage.chatId == 0 || replyToMessage.messageId == 0) { + buildContentHint(view, getReplyLocationProvider(), false).show(tdlib, Lang.getString(R.string.MessageReplyPrivate)); + } else { + tdlib.ui().openMessage(controller(), replyToMessage.chatId, new MessageId(replyToMessage), openParameters()); + } + } else if (isScheduled()) { + tdlib.ui().openMessage(controller(), replyToMessage.chatId, new MessageId(replyToMessage), openParameters()); } else { - openMessageThread(new MessageId(msg.replyInChatId, msg.replyToMessageId)); + highlightOtherMessage(new MessageId(replyToMessage)); } - } else if (isScheduled()) { - tdlib.ui().openMessage(controller(), msg.replyInChatId, new MessageId(msg.replyInChatId, msg.replyToMessageId), openParameters()); - } else { - highlightOtherMessage(msg.replyToMessageId); } } break; } case CLICK_TYPE_AVATAR: { - openProfile(view, null, null, null, ((MessageView) view).getAvatarReceiver()); + onAvatarClick(view); break; } } @@ -2789,6 +3006,9 @@ public boolean onTouchEvent (MessageView view, MotionEvent e) { private static final int CLICK_TYPE_AVATAR = 2; private static final int CLICK_TYPE_CHANNEL_MESSAGE_ICON = 3; private static final int CLICK_TYPE_TRANSLATE_MESSAGE_ICON = 4; + private static final int CLICK_TYPE_MESSAGE_RESTRICTED_ICON = 5; + private static final int CLICK_TYPE_MESSAGE_EDITED_ICON = 6; + private static final int CLICK_TYPE_CHANNEL_MESSAGE_SENDER_ICON = 7; private int clickType = CLICK_TYPE_NONE; @@ -2907,16 +3127,30 @@ private void buildMarkup () { } } - public final boolean forceForwardedInfo () { + public final int getForwardOrImportDate () { + return msg.forwardInfo != null && msg.forwardInfo.date != 0 ? + msg.forwardInfo.date : + msg.importInfo != null && msg.importInfo.date != 0 ? + msg.importInfo.date : + 0; + } + + public final boolean forceForwardOrImportInfo () { + if (msg.importInfo != null) { + return true; + } return msg.forwardInfo != null && !isOutgoing() && ( BitwiseUtils.hasFlag(flags, FLAG_SELF_CHAT) || - (isChannelAutoForward() && msg.forwardInfo.origin.getConstructor() == TdApi.MessageForwardOriginChannel.CONSTRUCTOR && - msg.forwardInfo.fromChatId == ((TdApi.MessageForwardOriginChannel) msg.forwardInfo.origin).chatId) || - msg.forwardInfo.origin.getConstructor() == TdApi.MessageForwardOriginMessageImport.CONSTRUCTOR || + (isChannelAutoForward() && msg.forwardInfo.origin.getConstructor() == TdApi.MessageOriginChannel.CONSTRUCTOR && + msg.forwardInfo.fromChatId == ((TdApi.MessageOriginChannel) msg.forwardInfo.origin).chatId) || (isPsa() && !sender.isUser() && useBubbles()) || isRepliesChat()); } + public boolean isImported () { + return msg.importInfo != null; + } + public final boolean isChannelAutoForward () { return tdlib.isChannelAutoForward(msg); } @@ -2946,6 +3180,10 @@ private void layoutAvatar () { protected static final float LETTERS_SIZE = 16f; protected static final float LETTERS_SIZE_SMALL = 15f; + private boolean onAvatarClick (View view) { + return openProfile(view, null, null, null, ((MessageView) view).getAvatarReceiver()); + } + private boolean onNameClick (View view, Text text, TextPart part, @Nullable TdlibUi.UrlOpenParameters openParameters) { if (part.getEntity() != null && part.getEntity().getTag() instanceof Long) { manager.controller().setInputInlineBot(msg.viaBotUserId, viaBotUsername); @@ -2958,8 +3196,10 @@ private boolean onNameClick (View view, Text text, TextPart part, @Nullable Tdli } private boolean openProfile (View view, @Nullable Text text, TextPart part, @Nullable TdlibUi.UrlOpenParameters openParameters, @Nullable Receiver receiver) { - if (forceForwardedInfo()) { + if (forceForwardOrImportInfo()) { forwardInfo.open(view, text, part, openParameters, receiver); + } else if (isSponsoredMessage()) { + openSponsoredMessage(); } else if (sender.isUser()) { tdlib.ui().openPrivateProfile(controller(), sender.getUserId(), openParameters); } else if (sender.isChat()) { @@ -2993,7 +3233,9 @@ private Text makeChatMark (int maxWidth) { .build(); } - private Text makeName (String authorName, int nameColorId, boolean available, boolean isPsa, boolean hideName, long viaBotUserId, int maxWidth, boolean isForward) { + private static final boolean APPLY_ACCENT_TO_FORWARDS = true; + + private Text makeName (String authorName, TdlibAccentColor accentColor, boolean available, boolean isPsa, boolean hideName, long viaBotUserId, int maxWidth, boolean isForward) { if (maxWidth <= 0) return null; boolean hasBot = viaBotUserId != 0; @@ -3004,7 +3246,9 @@ private Text makeName (String authorName, int nameColorId, boolean available, bo if (hideName) { return null; } - text = authorName; + SpannableStringBuilder b = new SpannableStringBuilder(authorName); + b.setSpan(new TextEntityCustom(controller(), tdlib, authorName, 0, authorName.length(), TextEntityCustom.FLAG_CLICKABLE, openParameters()), 0, b.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text = b; allActive = true; allBold = available; } else if (textRes == R.string.PsaFromXViaBot || textRes == R.string.message_nameViaBot) { // author via bot @@ -3018,34 +3262,32 @@ private Text makeName (String authorName, int nameColorId, boolean available, bo TextColorSet colorTheme; if (isPsa) { colorTheme = getChatAuthorPsaColorSet(); - } else if (!isForward && needColoredNames() && nameColorId != 0) { + } else if ((APPLY_ACCENT_TO_FORWARDS || !isForward) && needColoredNames() && accentColor != null) { colorTheme = new TextColorSetOverride(getChatAuthorColorSet()) { @Override public int clickableTextColor (boolean isPressed) { - return Theme.getColor(nameColorId); + return accentColor.getNameColor(); + } + + @Override + public long mediaTextComplexColor () { + return accentColor.getNameComplexColor(); } @Override public int backgroundColor (boolean isPressed) { - return isPressed ? ColorUtils.alphaColor(.2f, Theme.getColor(nameColorId)) : 0; + return isPressed ? ColorUtils.alphaColor(.2f, accentColor.getNameColor()) : 0; } @Override public int backgroundColorId (boolean isPressed) { - return isPressed ? nameColorId : 0; + return isPressed ? Theme.extractColorValue(accentColor.getNameComplexColor()) : 0; } }; } else { colorTheme = getChatAuthorColorSet(); } - colorTheme = new TextColorSetOverride(colorTheme) { - @Override - public int emojiStatusColor () { - return clickableTextColor(false); - } - }; - if (!(tdlib.isSelfChat(chat) && forwardInfo != null) && !hasBot && !isForward && sender.isUser()) { hAuthorEmojiStatus = EmojiStatusHelper.makeDrawable(null, tdlib, tdlib.cache().user(sender.getUserId()), colorTheme, (text1, specificMedia) -> invalidateEmojiStatusReceiver()); hAuthorEmojiStatus.invalidateTextMedia(); @@ -3064,7 +3306,7 @@ public int emojiStatusColor () { private void layoutInfo () { final int reactionsDrawMode = getReactionsDrawMode(); - boolean isPsa = isPsa() && forceForwardedInfo(); + boolean isPsa = isPsa() && forceForwardOrImportInfo(); if (useBubbles()) { // time part @@ -3080,14 +3322,9 @@ private void layoutInfo () { hAdminNameT = null; } - final String authorName; - if (forceForwardedInfo()) { - authorName = forwardInfo.getAuthorName(); - } else { - authorName = sender.getName(); - } + final String authorName = getDisplayAuthor(); if (needName(true) && maxWidth > 0) { - if (!forceForwardedInfo() && sender.hasChatMark()) { + if (!forceForwardOrImportInfo() && sender.hasChatMark()) { hAuthorChatMark = makeChatMark(maxWidth); maxWidth -= hAuthorChatMark.getWidth(); } @@ -3095,8 +3332,10 @@ private void layoutInfo () { if (needDrawChannelIconInHeader()) { maxWidth -= isChannelHeaderCounter.getScaledWidth(Screen.dp(5)); } - hAuthorNameT = makeName(authorName, forceForwardedInfo() ? forwardInfo.getAuthorNameColorId() : sender.getNameColorId(), !(forceForwardedInfo() && forwardInfo instanceof TGSourceHidden), isPsa, !needName(false), msg.forwardInfo == null || forceForwardedInfo() ? msg.viaBotUserId : 0, maxWidth, false); + hAuthorAccentColor = forceForwardOrImportInfo() ? forwardInfo.getAuthorAccentColor() : sender.getAccentColor(); + hAuthorNameT = makeName(authorName, hAuthorAccentColor, !(forceForwardOrImportInfo() && forwardInfo instanceof TGSourceHidden), isPsa, !needName(false), msg.forwardInfo == null || forceForwardOrImportInfo() ? msg.viaBotUserId : 0, maxWidth, false); } else { + hAuthorAccentColor = null; hAuthorNameT = null; hAuthorChatMark = null; isChannelHeaderCounter.showHide(false, false); @@ -3134,17 +3373,28 @@ private void layoutInfo () { } if (shouldShowEdited()) { - max -= Screen.dp(5f) + Icons.getEditedIconWidth(); + if (isBeingEdited()) { + max -= Screen.dp(6f) + Icons.getEditedIconWidth(); + } else { + max -= isEdited.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)) + Screen.dp(COUNTER_ADD_MARGIN); + } } String authorName; - if (forceForwardedInfo()) { + if (forceForwardOrImportInfo()) { authorName = forwardInfo.getAuthorName(); } else { authorName = sender.getName(); } max -= isPinned.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)) + Screen.dp(COUNTER_ADD_MARGIN); + if (shouldShowMessageRestrictedWarning()) { + if (isRestrictedByTelegram()) { + max -= isRestricted.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)) + Screen.dp(COUNTER_ADD_MARGIN); + } else { + max -= isUnsupported.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)) + Screen.dp(COUNTER_ADD_MARGIN); + } + } if (replyCounter.getVisibility() > 0f) { max -= replyCounter.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN + COUNTER_ADD_MARGIN)); } @@ -3178,7 +3428,7 @@ private void layoutInfo () { nameMaxWidth = max; } if (nameMaxWidth > 0) { - if (!forceForwardedInfo() && sender.hasChatMark()) { + if (!forceForwardOrImportInfo() && sender.hasChatMark()) { hAuthorChatMark = makeChatMark(totalMaxWidth); nameMaxWidth -= hAuthorChatMark.getWidth() + Screen.dp(8f); } @@ -3186,39 +3436,52 @@ private void layoutInfo () { if (needDrawChannelIconInHeader()) { nameMaxWidth -= isChannelHeaderCounter.getScaledWidth(Screen.dp(1)); } - hAuthorNameT = makeName(authorName, forceForwardedInfo() ? forwardInfo.getAuthorNameColorId() : sender.getNameColorId(), !(forceForwardedInfo() && forwardInfo instanceof TGSourceHidden), isPsa, !needName(false), msg.forwardInfo == null || forceForwardedInfo() ? msg.viaBotUserId : 0, nameMaxWidth, false); + hAuthorAccentColor = forceForwardOrImportInfo() ? forwardInfo.getAuthorAccentColor() : sender.getAccentColor(); + hAuthorNameT = makeName(authorName, hAuthorAccentColor, !(forceForwardOrImportInfo() && forwardInfo instanceof TGSourceHidden), isPsa, !needName(false), msg.forwardInfo == null || forceForwardOrImportInfo() ? msg.viaBotUserId : 0, nameMaxWidth, false); } else { hAuthorNameT = null; + hAuthorAccentColor = null; hAuthorChatMark = null; isChannelHeaderCounter.showHide(false, false); } } - private void loadForward () { - if (msg.forwardInfo == null) { - return; + private String getDisplayAuthor () { + if (forceForwardOrImportInfo()) { + return forwardInfo.getAuthorName(); + } else { + return sender.getName(); } - switch (msg.forwardInfo.origin.getConstructor()) { - case TdApi.MessageForwardOriginUser.CONSTRUCTOR: { - forwardInfo = new TGSourceUser(this, (TdApi.MessageForwardOriginUser) msg.forwardInfo.origin); - break; - } - case TdApi.MessageForwardOriginChat.CONSTRUCTOR: { - forwardInfo = new TGSourceChat(this, (TdApi.MessageForwardOriginChat) msg.forwardInfo.origin); - break; - } - case TdApi.MessageForwardOriginChannel.CONSTRUCTOR: { - forwardInfo = new TGSourceChat(this, (TdApi.MessageForwardOriginChannel) msg.forwardInfo.origin); - break; - } - case TdApi.MessageForwardOriginHiddenUser.CONSTRUCTOR: { - forwardInfo = new TGSourceHidden(this, (TdApi.MessageForwardOriginHiddenUser) msg.forwardInfo.origin); - break; - } - case TdApi.MessageForwardOriginMessageImport.CONSTRUCTOR: { - forwardInfo = new TGSourceHidden(this, (TdApi.MessageForwardOriginMessageImport) msg.forwardInfo.origin); - break; + } + + private void loadForward () { + if (msg.forwardInfo != null) { + switch (msg.forwardInfo.origin.getConstructor()) { + case TdApi.MessageOriginUser.CONSTRUCTOR: { + forwardInfo = new TGSourceUser(this, (TdApi.MessageOriginUser) msg.forwardInfo.origin); + break; + } + case TdApi.MessageOriginChat.CONSTRUCTOR: { + forwardInfo = new TGSourceChat(this, (TdApi.MessageOriginChat) msg.forwardInfo.origin); + break; + } + case TdApi.MessageOriginChannel.CONSTRUCTOR: { + forwardInfo = new TGSourceChat(this, (TdApi.MessageOriginChannel) msg.forwardInfo.origin); + break; + } + case TdApi.MessageOriginHiddenUser.CONSTRUCTOR: { + forwardInfo = new TGSourceHidden(this, (TdApi.MessageOriginHiddenUser) msg.forwardInfo.origin); + break; + } + default: { + Td.assertMessageOrigin_f2224a59(); + throw Td.unsupported(msg.forwardInfo.origin); + } } + } else if (msg.importInfo != null) { + forwardInfo = new TGSourceHidden(this, msg.importInfo); + } else { + return; } buildForwardTime(); forwardInfo.load(); @@ -3254,8 +3517,9 @@ private void buildForward () { shareCounter.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN + COUNTER_ADD_MARGIN)); } - boolean isPsa = isPsa() && !forceForwardedInfo(); - fAuthorNameT = makeName(forwardInfo.getAuthorName(), 0, !(forwardInfo instanceof TGSourceHidden), isPsa, false, msg.viaBotUserId, (int) (isPsa ? totalMax : max), true); + boolean isPsa = isPsa() && !forceForwardOrImportInfo(); + fAuthorNameAccentColor = forwardInfo.getAuthorAccentColor(); + fAuthorNameT = makeName(forwardInfo.getAuthorName(), fAuthorNameAccentColor, !(forwardInfo instanceof TGSourceHidden), isPsa, false, msg.viaBotUserId, (int) (isPsa ? totalMax : max), true); if (isPsa) { CharSequence text = Lang.getPsaNotificationType(controller(), msg.forwardInfo.publicServiceAnnouncementType); fPsaTextT = new Text.Builder(tdlib, text, openParameters(), (int) max, getNameStyleProvider(), getChatAuthorPsaColorSet(), null) @@ -3274,25 +3538,63 @@ private int getForwardAuthorNameLeft () { private void loadReply () { replyData = new ReplyComponent(this); + replyData.setUseColorize(!isOutgoingBubble()); replyData.setViewProvider(currentViews); replyData.load(); } - public final void replaceReplyContent (long messageId, TdApi.MessageContent newContent) { - if (msg.replyToMessageId == messageId && replyData != null) { - replyData.replaceMessageContent(messageId, newContent); - } + public void onReplyLoaded () { + } - public final void replaceReplyTranslation (long messageId, @Nullable TdApi.FormattedText translation) { - if (msg.replyToMessageId == messageId && replyData != null) { - replyData.replaceMessageTranslation(messageId, translation); + @MessageChangeType + private int performContentfulUpdate (FutureBool act) { + int height = getHeight(); + int width = getWidth(); + int contentWidth = getContentWidth(); + boolean updated = act.getBoolValue(); + if (updated) { + if (width != getWidth() || contentWidth != getContentWidth()) { + buildMarkup(); + } + return height == getHeight() ? MESSAGE_INVALIDATED : MESSAGE_CHANGED; } + return MESSAGE_NOT_CHANGED; + } + + @MessageChangeType + public final int replaceMessagePreview (long chatId, long messageId, TdApi.MessageContent newContent) { + return performContentfulUpdate(() -> { + boolean replyUpdated = Td.equalsTo(msg.replyTo, chatId, messageId) && replyData != null; + if (replyUpdated) { + replyData.replaceMessageContent(messageId, newContent); + } + return handleMessagePreviewChange(chatId, messageId, newContent) || replyUpdated; + }); + } + + protected boolean handleMessagePreviewChange (long chatId, long messageId, TdApi.MessageContent newContent) { + return false; + } + + @MessageChangeType + public final int removeMessagePreview (long chatId, long messageId) { + return performContentfulUpdate(() -> { + boolean replyUpdated = Td.equalsTo(msg.replyTo, chatId, messageId) && replyData != null; + if (replyUpdated) { + replyData.deleteMessageContent(messageId); + } + return handleMessagePreviewDelete(chatId, messageId) || replyUpdated; + }); + } + + protected boolean handleMessagePreviewDelete (long chatId, long messageId) { + return false; } - public final void removeReply (long messageId) { - if (msg.replyToMessageId == messageId && replyData != null) { - replyData.deleteMessageContent(messageId); + public final void replaceReplyTranslation (long chatId, long messageId, @Nullable TdApi.FormattedText translation) { + if (Td.equalsTo(msg.replyTo, chatId, messageId) && replyData != null) { + replyData.replaceMessageTranslation(messageId, translation); } } @@ -3352,11 +3654,11 @@ protected final boolean needExpandBubble (int bottomLineContentWidth, int bubble return bottomLineContentWidth > 0 && (bottomLineContentWidth + bubbleTimePartWidth > maxLineWidth); } - protected float getBubbleExpandFactor () { + protected float getIntermediateBubbleExpandFactor () { throw new RuntimeException(); } - protected int getAnimatedBottomLineWidth () { + protected int getAnimatedBottomLineWidth (int bubbleTimePartWidth) { throw new RuntimeException(); } @@ -3384,10 +3686,10 @@ protected final void buildBubble (boolean force) { case BOTTOM_LINE_KEEP_WIDTH: break; case BOTTOM_LINE_DEFINE_BY_FACTOR: { - final int extendedWidth = getAnimatedBottomLineWidth() + bubbleTimePartWidth; - final int fitBubbleWidth = Math.max(bubbleWidth, extendedWidth); + final int extendedWidth = getAnimatedBottomLineWidth(bubbleTimePartWidth); + final int fitBubbleWidth = Math.max(bubbleWidth, extendedWidth != -1 ? extendedWidth + bubbleTimePartWidth : bubbleWidth); - float factor = getBubbleExpandFactor(); + float factor = getIntermediateBubbleExpandFactor(); if (factor > 0f) { bubbleWidth = MathUtils.fromTo(fitBubbleWidth, expandedBubbleWidth, factor); int newBubbleHeight = MathUtils.fromTo(bubbleHeight, expandedBubbleHeight, factor); @@ -3691,18 +3993,28 @@ protected void drawBubbleTimePart (Canvas c, MessageView view) { isPinned.draw(c, startX, counterY, Gravity.LEFT, 1f, view, iconColorId); startX += isPinned.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)); + if (shouldShowMessageRestrictedWarning()) { + if (isRestrictedByTelegram()) { + isRestricted.draw(c, startX, counterY, Gravity.LEFT, 1f, view, 0, isRestrictedCounterLastDrawRect); + startX += isRestricted.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)); + } else { + isUnsupported.draw(c, startX, counterY, Gravity.LEFT, 1f, view, iconColorId, isRestrictedCounterLastDrawRect); + startX += isUnsupported.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)); + } + } + if (shouldShowEdited()) { if (isBeingEdited()) { Drawables.draw(c, Icons.getClockIcon(iconColorId), startX - Screen.dp(6f), startY + Screen.dp(4.5f) - Screen.dp(5f), iconPaint); + startX += Icons.getEditedIconWidth() + Screen.dp(3f); } else { - Drawables.draw(c, view.getSparseDrawable(R.drawable.baseline_edit_12, ColorId.NONE), startX, startY + Screen.dp(4.5f), iconPaint); + isEdited.draw(c, startX, counterY, Gravity.LEFT, 1f, view, iconColorId, isEditedCounterLastDrawRect); + startX += isEdited.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN)); } - startX += Icons.getEditedIconWidth() + Screen.dp(2f); } if (translationStyleMode() == Settings.TRANSLATE_MODE_INLINE) { - isTranslatedCounter.draw(c, startX, isTranslatedCounterY = counterY, Gravity.LEFT, 1f); - isTranslatedCounterX = startX + Screen.dp(7); + isTranslatedCounter.draw(c, startX, counterY, Gravity.LEFT, 1f, isTranslatedCounterLastDrawRect); startX += isTranslatedCounter.getScaledWidth(Screen.dp(COUNTER_ICON_MARGIN + COUNTER_ADD_MARGIN)); } @@ -3753,7 +4065,11 @@ protected final int computeBubbleTimePartWidth (boolean includePadding, boolean width = (int) U.measureText(time, mTimeBubble()); } if (shouldShowEdited()) { - width += Icons.getEditedIconWidth() + Screen.dp(2f); + if (isBeingEdited()) { + width += Icons.getEditedIconWidth() + Screen.dp(2f); + } else { + width += isEdited.getScaledOrTargetWidth(Screen.dp(COUNTER_ICON_MARGIN), isTarget); + } } if (translationStyleMode() == Settings.TRANSLATE_MODE_INLINE) { width += isTranslatedCounter.getScaledOrTargetWidth(Screen.dp(COUNTER_ICON_MARGIN + COUNTER_ADD_MARGIN), isTarget); @@ -3773,6 +4089,13 @@ protected final int computeBubbleTimePartWidth (boolean includePadding, boolean width += replyCounter.getScaledOrTargetWidth(Screen.dp(COUNTER_ICON_MARGIN + COUNTER_ADD_MARGIN), isTarget); } width += isPinned.getScaledOrTargetWidth(Screen.dp(COUNTER_ICON_MARGIN), isTarget); + if (shouldShowMessageRestrictedWarning()) { + if (isRestrictedByTelegram()) { + width += isRestricted.getScaledOrTargetWidth(Screen.dp(COUNTER_ICON_MARGIN), isTarget); + } else { + width += isUnsupported.getScaledOrTargetWidth(Screen.dp(COUNTER_ICON_MARGIN), isTarget); + } + } if (reactionsDrawMode == REACTIONS_DRAW_MODE_FLAT) { width += reactionsCounterDrawable.getMinimumWidth() + messageReactions.getVisibility() * Screen.dp(3); width += reactionsCounter.getScaledOrTargetWidth(Screen.dp(COUNTER_ICON_MARGIN + COUNTER_ADD_MARGIN), isTarget); @@ -3859,8 +4182,34 @@ public final void requestAvatar (AvatarReceiver receiver) { public final void requestAvatar (AvatarReceiver receiver, boolean force) { if (hasAvatar || force) { - final float avatarRadiusDp = useBubbles() ? BUBBLE_AVATAR_RADIUS : AVATAR_RADIUS; - if (forceForwardedInfo()) { + if (isSponsoredMessage()) { + if (sponsoredMessage.sponsor.photo != null) { + receiver.requestSpecific(tdlib, sponsoredMessage.sponsor.photo, AvatarReceiver.Options.NONE); + } else { + TdApi.MessageSponsor sponsor = sponsoredMessage.sponsor; + switch (sponsor.type.getConstructor()) { + case TdApi.MessageSponsorTypeBot.CONSTRUCTOR: { + TdApi.MessageSponsorTypeBot bot = (TdApi.MessageSponsorTypeBot) sponsor.type; + receiver.requestUser(tdlib, bot.botUserId, AvatarReceiver.Options.NONE); + break; + } + case TdApi.MessageSponsorTypePublicChannel.CONSTRUCTOR: { + TdApi.MessageSponsorTypePublicChannel publicChannel = (TdApi.MessageSponsorTypePublicChannel) sponsor.type; + receiver.requestChat(tdlib, publicChannel.chatId, AvatarReceiver.Options.NONE); + break; + } + case TdApi.MessageSponsorTypePrivateChannel.CONSTRUCTOR: + case TdApi.MessageSponsorTypeWebsite.CONSTRUCTOR: + case TdApi.MessageSponsorTypeWebApp.CONSTRUCTOR: { + receiver.requestPlaceholder(tdlib, sender.getPlaceholderMetadata(), AvatarReceiver.Options.NONE); + break; + } + default: + Td.assertMessageSponsorType_cdabde01(); + throw Td.unsupported(sponsor.type); + } + } + } else if (forceForwardOrImportInfo()) { forwardInfo.requestAvatar(receiver); } else if (sender.isDemo()) { receiver.requestPlaceholder(tdlib, sender.getPlaceholderMetadata(), AvatarReceiver.Options.NONE); @@ -3876,7 +4225,7 @@ public final void requestAvatar (AvatarReceiver receiver, boolean force) { public final void requestReactions (ComplexReceiver complexReceiver) { currentComplexReceiver = complexReceiver; - messageReactions.setReceiversPool(complexReceiver); + messageReactions.requestReactionFiles(complexReceiver); computeQuickButtons(); } @@ -3886,6 +4235,13 @@ public final void requestCommentsResources (ComplexReceiver complexReceiver, boo } } + public final void requestReactionsResources (ComplexReceiver complexReceiver, boolean isUpdate) { + if (messageReactions != null) { + messageReactions.requestAvatarFiles(complexReceiver, isUpdate); + } + } + + public final void requestAllTextMedia (MessageView view) { requestTextMedia(view.getTextMediaReceiver()); requestAuthorTextMedia(view.getEmojiStatusReceiver()); @@ -4011,7 +4367,7 @@ private static int getForwardHeaderHeight () { } private int getForwardHeight () { - return getContentHeight() + getForwardHeaderHeight() + (isPsa() && !forceForwardedInfo() ? getPsaTitleHeight() : 0); + return getContentHeight() + getForwardHeaderHeight() + (isPsa() && !forceForwardOrImportInfo() ? getPsaTitleHeight() : 0); } public int getHeaderPadding () { @@ -4178,6 +4534,10 @@ public final String getInReplyTo () { return replyData != null ? replyData.getAuthor() : null; } + public final TdApi.MessageSender getInReplyToSender () { + return replyData != null ? replyData.getSender() : null; + } + public final int getViewCount () { if (isSending() || isFailed()) { return 0; @@ -4211,7 +4571,7 @@ public boolean isFakeMessage () { if (msg.content.getConstructor() == TdApiExt.MessageChatEvent.CONSTRUCTOR) { return true; } - return isSponsored() || isDemoChat(); + return isDemoChat(); } public final void getIds (@NonNull LongSet ids, long afterMessageId, long beforeMessageId) { @@ -4247,7 +4607,7 @@ public final long[] getIds () { } public final boolean isMessageThreadRoot () { - return canGetMessageThread() && (isChannel() || (isMessageThread() && isThreadHeader()) || (msg.messageThreadId != 0 && msg.replyToMessageId == 0)); + return canGetMessageThread() && (isChannel() || (isMessageThread() && isThreadHeader()) || (msg.messageThreadId != 0 && msg.replyTo == null)); } public final long getMessageThreadId () { @@ -4321,17 +4681,6 @@ public final boolean containsUnreadReactions () { return msg.unreadReactions != null && msg.unreadReactions.length > 0; } - public final void readReactions () { - synchronized (this) { - if (combinedMessages != null) { - for (TdApi.Message message : combinedMessages) { - message.unreadReactions = new TdApi.UnreadReaction[0]; - } - } - msg.unreadReactions = new TdApi.UnreadReaction[0]; - } - } - public final boolean containsUnreadMention () { synchronized (this) { if (combinedMessages != null) { @@ -4379,7 +4728,11 @@ protected void onMessageCombinationRemoved (TdApi.Message message, int index) { @AnyThread public final boolean wouldCombineWith (TdApi.Message message) { - if (msg.mediaAlbumId == 0 || msg.mediaAlbumId != message.mediaAlbumId || msg.selfDestructTime != message.selfDestructTime || isHot() || isEventLog() || isSponsored()) { + if (msg.mediaAlbumId == 0 || msg.mediaAlbumId != message.mediaAlbumId || + !Td.equalsTo(msg.selfDestructType, message.selfDestructType) || + ((msg.forwardInfo == null) != (message.forwardInfo == null)) || + ((msg.forwardInfo != null && message.forwardInfo != null && !Td.equalsTo(msg.forwardInfo.origin, message.forwardInfo.origin, false))) || + isHot() || isEventLog() || isSponsoredMessage()) { return false; } int combineMode = TD.getCombineMode(msg); @@ -4478,7 +4831,7 @@ public final long getChatId () { private String getAdministratorSign () { String result = null; - if (isSponsored()) { + if (isSponsoredMessage()) { return null; } else if (administrator != null) { if (!StringUtils.isEmpty(administrator.customTitle)) @@ -4563,10 +4916,6 @@ public final boolean useStickerBubbleReactions () { return (isWhite && isTransparent); } - public boolean isSponsored () { - return false; - } - public final int getPinnedMessageCount () { if (isThreadHeader()) { return 0; @@ -4654,13 +5003,13 @@ public final String[] getFailureMessages () { for (TdApi.Message msg : combinedMessages) { if (msg.sendingState instanceof TdApi.MessageSendingStateFailed) { TdApi.MessageSendingStateFailed failed = (TdApi.MessageSendingStateFailed) msg.sendingState; - errors.add(TD.toErrorString(new TdApi.Error(failed.errorCode, failed.errorMessage))); + errors.add(TD.toErrorString(failed.error)); } } } else { if (msg.sendingState instanceof TdApi.MessageSendingStateFailed) { TdApi.MessageSendingStateFailed failed = (TdApi.MessageSendingStateFailed) msg.sendingState; - errors.add(TD.toErrorString(new TdApi.Error(failed.errorCode, failed.errorMessage))); + errors.add(TD.toErrorString(failed.error)); } } } @@ -4708,7 +5057,7 @@ public final boolean canGetAddedReactions () { return msg.canGetAddedReactions; - //return !isChannel() && messageReactions.getTotalCount() > 0 && (msg.forwardInfo == null || msg.forwardInfo.origin.getConstructor() != TdApi.MessageForwardOriginChannel.CONSTRUCTOR); + //return !isChannel() && messageReactions.getTotalCount() > 0 && (msg.forwardInfo == null || msg.forwardInfo.origin.getConstructor() != TdApi.MessageOriginChannel.CONSTRUCTOR); } public final boolean canBeDeletedOnlyForSelf () { @@ -4720,11 +5069,11 @@ public final boolean canBeDeletedForEveryone () { } public boolean canBeSelected () { - return (!isNotSent() || canResend()) && (flags & FLAG_UNSUPPORTED) == 0 && allowInteraction() && !isSponsored() && !messagesController().inSearchMode(); + return (!isNotSent() || canResend()) && (flags & FLAG_UNSUPPORTED) == 0 && allowInteraction() && !isSponsoredMessage() && !messagesController().inSearchMode(); } public boolean canBePinned () { - return !isNotSent() && allowInteraction() && !isSponsored(); + return !isNotSent() && allowInteraction() && !isSponsoredMessage(); } public boolean canEditText () { @@ -4736,7 +5085,7 @@ public boolean canBeForwarded () { } public boolean canBeReacted () { - return !isSponsored() && !isEventLog() && !(msg.content instanceof TdApi.MessageCall) && !Td.isEmpty(messageAvailableReactions); + return !isSponsoredMessage() && !isEventLog() && !(msg.content instanceof TdApi.MessageCall) && !Td.isEmpty(messageAvailableReactions); } public boolean canBeSaved () { @@ -4806,29 +5155,42 @@ public boolean markAsViewed () { } result = true; } - if (containsUnreadReactions()) { - if (!BitwiseUtils.hasFlag(flags, FLAG_IGNORE_REACTIONS_VIEW)) { - highlightUnreadReactions(); - highlight(true); - tdlib.ui().postDelayed(() -> { - flags = BitwiseUtils.setFlag(flags, FLAG_IGNORE_REACTIONS_VIEW, false); - }, 500L); - tdlib().ui().post(this::readReactions); - } + if (containsUnreadReactions() && !BitwiseUtils.hasFlag(flags, FLAG_IGNORE_REACTIONS_VIEW)) { flags |= FLAG_IGNORE_REACTIONS_VIEW; + + highlightUnreadReactions(); + highlight(true); + tdlib.ui().postDelayed(() -> { + flags = BitwiseUtils.setFlag(flags, FLAG_IGNORE_REACTIONS_VIEW, false); + }, 500L); + result = true; } return result; } public boolean needRefreshViewCount () { - return viewCounter != null && !isSending(); + return !isSponsoredMessage() && viewCounter != null && !isSending(); } public void markAsUnread () { flags &= ~FLAG_VIEWED; } + public int getEditDate () { + synchronized (this) { + if (combinedMessages != null && !combinedMessages.isEmpty()) { + int result = 0; + for (TdApi.Message message : combinedMessages) { + result = Math.max(result, message.editDate); + } + return result; + } + } + + return msg.editDate; + } + public boolean isEdited () { if (msg.editDate > 0) { return true; @@ -4845,6 +5207,20 @@ public boolean isEdited () { return false; } + public boolean isRestrictedByTelegram () { + synchronized (this) { + if (combinedMessages != null) { + for (TdApi.Message message : combinedMessages) { + if (!StringUtils.isEmpty(message.restrictionReason)) { + return true; + } + } + } + } + + return !StringUtils.isEmpty(msg.restrictionReason); + } + protected boolean replaceTimeWithEditTime () { return false; } @@ -4932,7 +5308,7 @@ public final void openSourceMessage () { if (msg.forwardInfo != null) { if (isRepliesChat()) { MessageId replyMessageId = new MessageId(msg.forwardInfo.fromChatId, msg.forwardInfo.fromMessageId); - MessageId replyToMessageId = new MessageId(msg.replyInChatId, msg.replyToMessageId); + MessageId replyToMessageId = Td.toMessageId(msg.replyTo); openMessageThread(replyMessageId, replyToMessageId); } else { tdlib.ui().openMessage(controller(), msg.forwardInfo.fromChatId, new MessageId(msg.forwardInfo.fromChatId, msg.forwardInfo.fromMessageId), openParameters()); @@ -4977,7 +5353,10 @@ protected boolean isSupportedMessageContent (TdApi.Message message, TdApi.Messag } @MessageChangeType - public int setMessageContent (long messageId, TdApi.MessageContent newContent) { + public int replaceMessageContent (long chatId, long messageId, TdApi.MessageContent newContent) { + if (msg.chatId != chatId || !isDescendantOrSelf(messageId)) { + return replaceMessagePreview(chatId, messageId, newContent); + } TdApi.Message message; boolean isBottomMessage; synchronized (this) { @@ -4989,9 +5368,13 @@ public int setMessageContent (long messageId, TdApi.MessageContent newContent) { message = msg; isBottomMessage = true; } else { - return MESSAGE_NOT_CHANGED; + message = null; + isBottomMessage = false; } } + if (message == null) { + return MESSAGE_NOT_CHANGED; + } if ((flags & FLAG_UNSUPPORTED) != 0) { if (message.content.getConstructor() == TdApi.MessageUnsupported.CONSTRUCTOR && newContent.getConstructor() != TdApi.MessageUnsupported.CONSTRUCTOR) { message.content = newContent; @@ -5008,6 +5391,7 @@ public int setMessageContent (long messageId, TdApi.MessageContent newContent) { if (width != getWidth() || contentWidth != getContentWidth()) { buildMarkup(); } + updateReactionAvatars(UI.inUiThread()); return height == getHeight() ? MESSAGE_INVALIDATED : MESSAGE_CHANGED; } return MESSAGE_REPLACE_REQUIRED; @@ -5083,8 +5467,20 @@ public boolean isHot () { // return msg.ttl > 0 && ((chat != null && chat.type.getConstructor() == TdApi.ChatTypePrivate.CONSTRUCTOR) || msg.ttl <= 60) && (flags & FLAG_EVENT_LOG) == 0 && !isEventLog(); } + public boolean isViewOnce () { + return msg.selfDestructType != null && msg.selfDestructType.getConstructor() == TdApi.MessageSelfDestructTypeImmediately.CONSTRUCTOR; + } + public boolean isHotDone () { - return isOutgoing() && msg.selfDestructIn < msg.selfDestructTime; + return isOutgoing() && isHotOpened(); + } + + public boolean isHotOpened () { + if (msg.selfDestructType != null && msg.selfDestructType.getConstructor() == TdApi.MessageSelfDestructTypeTimer.CONSTRUCTOR) { + TdApi.MessageSelfDestructTypeTimer timer = (TdApi.MessageSelfDestructTypeTimer) msg.selfDestructType; + return msg.selfDestructIn != 0 && msg.selfDestructIn < timer.selfDestructTime; + } + return false; } protected boolean needHotTimer () { @@ -5108,7 +5504,7 @@ public void readContent () { } public boolean isContentRead () { - return TD.isMessageOpened(msg); + return Td.isListenedOrViewed(msg.content); } private static @Nullable HotHandler __hotHandler; @@ -5127,7 +5523,7 @@ private static HotHandler getHotHandler () { private void startHotTimer (boolean byEvent) { if (isHot() && needHotTimer() && hotTimerStart == 0) { HotHandler hotHandler = getHotHandler(); - hotTimerStart = System.currentTimeMillis(); + hotTimerStart = SystemClock.uptimeMillis(); hotHandler.sendMessageDelayed(Message.obtain(hotHandler, HotHandler.MSG_HOT_CHECK, this), HOT_CHECK_DELAY); onHotTimerStarted(byEvent); } @@ -5149,12 +5545,16 @@ private void stopHotTimer () { } private void checkHotTimer () { - long now = System.currentTimeMillis(); - long elapsed = now - hotTimerStart; - double prevTtl = msg.selfDestructIn; + if (msg.selfDestructType == null || msg.selfDestructType.getConstructor() != TdApi.MessageSelfDestructTypeTimer.CONSTRUCTOR) { + return; + } + TdApi.MessageSelfDestructTypeTimer timer = (TdApi.MessageSelfDestructTypeTimer) msg.selfDestructType; + double preSelfDestructIn = msg.selfDestructIn != 0 ? msg.selfDestructIn : timer.selfDestructTime; + long now = SystemClock.uptimeMillis(); + long elapsedMs = now - hotTimerStart; hotTimerStart = now; - msg.selfDestructIn = Math.max(0, prevTtl - (double) elapsed / 1000.0d); - boolean secondsChanged = Math.round(prevTtl) != Math.round(msg.selfDestructIn); + msg.selfDestructIn = Math.max(0, preSelfDestructIn - (double) elapsedMs / 1000.0d); + boolean secondsChanged = Math.round(preSelfDestructIn) != Math.round(msg.selfDestructIn); onHotInvalidate(secondsChanged); if (hotListener != null) { hotListener.onHotInvalidate(secondsChanged); @@ -5166,13 +5566,35 @@ private void checkHotTimer () { } public float getHotExpiresFactor () { - return (float) (msg.selfDestructIn / msg.selfDestructTime); + if (msg.selfDestructType != null && msg.selfDestructType.getConstructor() == TdApi.MessageSelfDestructTypeTimer.CONSTRUCTOR) { + TdApi.MessageSelfDestructTypeTimer timer = (TdApi.MessageSelfDestructTypeTimer) msg.selfDestructType; + return msg.selfDestructIn == 0 ? 1f : (float) (msg.selfDestructIn / timer.selfDestructTime); + } + return 0f; } public String getHotTimerText () { - return TdlibUi.getDuration((int) Math.round(msg.selfDestructIn), TimeUnit.SECONDS, false); - } - + double selfDestructIn = msg.selfDestructIn; + if (msg.selfDestructType != null) { + switch (msg.selfDestructType.getConstructor()) { + case TdApi.MessageSelfDestructTypeImmediately.CONSTRUCTOR: + return Lang.getString(R.string.ViewOnce); + case TdApi.MessageSelfDestructTypeTimer.CONSTRUCTOR: { + TdApi.MessageSelfDestructTypeTimer timer = (TdApi.MessageSelfDestructTypeTimer) msg.selfDestructType; + if (selfDestructIn == 0) { + selfDestructIn = timer.selfDestructTime; + } + break; + } + default: { + Td.assertMessageSelfDestructType_58882d8c(); + throw Td.unsupported(msg.selfDestructType); + } + } + } + return TdlibUi.getDuration(Math.round(selfDestructIn), TimeUnit.SECONDS, false); + } + public interface HotListener { void onHotInvalidate (boolean secondsChanged); } @@ -5222,7 +5644,7 @@ public boolean onMessageSendAcknowledged (long messageId) { } public boolean allowInteraction () { - return !isEventLog() && !isThreadHeader(); + return !isFakeMessage() && !isEventLog() && !isThreadHeader(); } public boolean canReplyTo () { @@ -6106,6 +6528,20 @@ public void onCounterAppearanceChanged (Counter counter, boolean sizeChanged) { } } + @Override + public void onSizeChanged () { + if (UI.inUiThread()) { // FIXME remove this after reworking combineWith method + invalidate(); + } else { + postInvalidate(); + } + } + + @Override + public void onInvalidateMedia (TGAvatars avatars) { + performWithViews(view -> requestReactionsResources(view.getReactionAvatarsReceiver(), true)); + } + @Override public final void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { if (id >= 0) { @@ -6235,7 +6671,7 @@ private void setHighlightedText (@Nullable String text) { android.util.Log.i("HIGHLIGHT", "UPDATE"); } - return searchResultsHighlightPool.isMostRelevant(key) ? highlight: null; + return searchResultsHighlightPool.isMostRelevant(key) ? highlight : null; } public void checkHighlightedText () { @@ -6561,7 +6997,7 @@ public void drawTranslate (View view, Canvas c) { } int positionOffset = -(int) (verticalFactor * height); - c.save(); + final int restoreToCount = Views.save(c); c.clipRect(0, startY, view.getMeasuredWidth(), endY); for (int a = 0; a < actions.size(); a++) { SwipeQuickAction action = actions.get(a); @@ -6574,7 +7010,7 @@ public void drawTranslate (View view, Canvas c) { height > Screen.dp(256) ? (int) (mInitialTouchY - getHeaderPadding() + xHeaderPadding) : (height / 2) ); } - c.restore(); + Views.restore(c, restoreToCount); } private float getTranslatePositionFactor (boolean isLeft, int position) { @@ -6601,12 +7037,12 @@ private void drawTranslateRound (Canvas c, float cx, float cy, float readyFactor c.drawCircle(cx, cy, radius2, Paints.fillingPaint(ColorUtils.alphaColor(alpha, getBubbleButtonBackgroundColor()))); if (icon != null) { - c.save(); + final int restoreToCount = Views.save(c); c.scale((Lang.rtl() ? -.8f : .8f) * scale, .8f * scale, cx, cy); icon.setAlpha((int) (alpha * 255)); Paint paint = Paints.getInlineBubbleIconPaint(ColorUtils.alphaColor(alpha, getBubbleButtonTextColor())); Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2f, cy - icon.getMinimumHeight() / 2f, paint); - c.restore(); + Views.restore(c, restoreToCount); } } @@ -6829,16 +7265,74 @@ public final TextColorSet getDecentColorSet () { return pick(TextColorSets.Regular.LIGHT, TextColorSets.BubbleOut.LIGHT, TextColorSets.BubbleIn.LIGHT); } + @Nullable + public final TdlibAccentColor getContentAccentColor () { + if (true) { + return null; + } + if (needColoredNames()) { + if (fAuthorNameAccentColor != null) { + return fAuthorNameAccentColor; + } else if (forwardInfo != null) { + return forwardInfo.getAuthorAccentColor(); + } + if (hAuthorAccentColor != null) { + return hAuthorAccentColor; + } else { + return forceForwardOrImportInfo() ? forwardInfo.getAuthorAccentColor() : sender.getAccentColor(); + } + } + return null; + } + + public final TextColorSet overrideWithAccent (TextColorSet colorSet, boolean onlyClickable) { + TdlibAccentColor accentColor = getContentAccentColor(); + if (accentColor != null) { + return new TextColorSetOverride(colorSet) { + @Override + public int defaultTextColor () { + return onlyClickable ? super.defaultTextColor() : accentColor.getNameColor(); + } + + @Override + public int clickableTextColor (boolean isPressed) { + return accentColor.getNameColor(); + } + + @Override + public int backgroundColor (boolean isPressed) { + return isPressed ? ColorUtils.alphaColor(.2f, accentColor.getNameColor()) : super.backgroundColor(false); + } + + @Override + public int backgroundColorId (boolean isPressed) { + long complexColor = accentColor.getNameComplexColor(); + return Theme.extractColorValue(complexColor); + } + }; + } + return colorSet; + } + public final TextColorSet getLinkColorSet () { - return pick(TextColorSets.Regular.LINK, TextColorSets.BubbleOut.LINK, TextColorSets.BubbleIn.LINK); + return overrideWithAccent( + pick(TextColorSets.Regular.LINK, TextColorSets.BubbleOut.LINK, TextColorSets.BubbleIn.LINK), + false + ); } public final TextColorSet getTextColorSet () { - return pick(TextColorSets.Regular.NORMAL, TextColorSets.BubbleOut.NORMAL, TextColorSets.BubbleIn.NORMAL); + return overrideWithAccent( + pick(TextColorSets.Regular.NORMAL, TextColorSets.BubbleOut.NORMAL, TextColorSets.BubbleIn.NORMAL), + true + ); } public final TextColorSet getChatAuthorColorSet () { - return pick(TextColorSets.Regular.MESSAGE_AUTHOR, TextColorSets.BubbleOut.MESSAGE_AUTHOR, TextColorSets.BubbleIn.MESSAGE_AUTHOR); + return overrideWithAccent( + pick(TextColorSets.Regular.MESSAGE_AUTHOR, TextColorSets.BubbleOut.MESSAGE_AUTHOR, TextColorSets.BubbleIn.MESSAGE_AUTHOR), + false + ); } public final TextColorSet getChatAuthorPsaColorSet () { @@ -6859,7 +7353,7 @@ public final TextColorSet getSearchHighlightColorSet () { } } - public final @ColorId int getDecentColorId (@ColorId int defaultColorId) { + public final @PorterDuffColorId int getDecentColorId (@ColorId int defaultColorId) { return useBubbles() ? (isOutgoingBubble() ? ColorId.bubbleOut_time : ColorId.bubbleIn_time) : defaultColorId; } @@ -6883,7 +7377,7 @@ public final int getProgressColor () { return useBubbles() ? (isOutgoingBubble() ? ColorId.bubbleOut_pressed : ColorId.bubbleIn_pressed) : ColorId.messageSelection; } - public final @ColorId int getDecentIconColorId () { + public final @PorterDuffColorId int getDecentIconColorId () { return getDecentColorId(ColorId.iconLight); } @@ -6924,7 +7418,14 @@ protected final int getTextTopOffset () { } protected final int getVerticalLineColor () { - return Theme.getColor(isOutgoingBubble() ? ColorId.bubbleOut_chatVerticalLine : ColorId.messageVerticalLine); + if (isOutgoingBubble()) { + return Theme.getColor(ColorId.bubbleOut_chatVerticalLine); + } + TdlibAccentColor accentColor = getContentAccentColor(); + if (accentColor != null) { + return accentColor.getVerticalLineColor(); + } + return Theme.getColor(ColorId.messageVerticalLine); } protected final int getVerticalLineContentColor () { @@ -7039,14 +7540,27 @@ public TooltipOverlayView.TooltipInfo showContentHint (View view, TooltipOverlay } public TooltipOverlayView.TooltipInfo showContentHint (View view, TooltipOverlayView.LocationProvider locationProvider, TdApi.FormattedText text) { - return buildContentHint(view, locationProvider).show(tdlib, text); + return buildContentHint(view, locationProvider, true).show(tdlib, text); } - public TooltipOverlayView.TooltipBuilder buildContentHint (View view, TooltipOverlayView.LocationProvider locationProvider) { + public TooltipOverlayView.TooltipBuilder buildContentHint (View view, TooltipOverlayView.LocationProvider locationProvider, boolean applyContentOffset) { return context().tooltipManager().builder(view, currentViews) .locate((v, outRect) -> { - locationProvider.getTargetBounds(v, outRect); - outRect.offset(getContentX(), getContentY()); + if (locationProvider != null) { + locationProvider.getTargetBounds(v, outRect); + if (applyContentOffset) { + outRect.offset(getContentX(), getContentY()); + } + } else { + if (applyContentOffset) { + outRect.left = getContentX(); + outRect.top = getContentY(); + } else { + outRect.left = outRect.top = 0; + } + outRect.right = outRect.left + getContentWidth(); + outRect.bottom = outRect.top + getContentHeight(); + } }) .chatTextSize(-2f) .click(clickCallback()) @@ -7077,7 +7591,7 @@ public boolean onCommandClick (View view, Text text, TextPart part, String comma TdApi.Message m = getMessage(); TdApi.User user; if (m.forwardInfo != null) { - user = m.forwardInfo.origin.getConstructor() == TdApi.MessageForwardOriginUser.CONSTRUCTOR ? ((TGSourceUser) getForwardInfo()).getUser() : null; + user = m.forwardInfo.origin.getConstructor() == TdApi.MessageOriginUser.CONSTRUCTOR ? ((TGSourceUser) getForwardInfo()).getUser() : null; } else { user = sender.isUser() ? tdlib.cache().user(sender.getUserId()) : null; } @@ -7095,6 +7609,47 @@ public TdApi.WebPage findWebPage (String link) { public boolean forceInstantView (String link) { return hasInstantView(link); } + + @Override + public boolean onUsernameClick (String username) { + if (isSponsoredMessage()) { + TdApi.Usernames usernames = sender.getUsernames(); + if (usernames != null && Td.findUsername(usernames, username.substring(1), true)) { + openSponsoredMessage(); + return true; + } + } + trackSponsoredMessageClicked(); + return false; + } + + @Override + public boolean onUserClick (long userId) { + if (isSponsoredMessage() && userId != 0 && sender.getUserId() == userId) { + openSponsoredMessage(); + return true; + } + trackSponsoredMessageClicked(); + return false; + } + + @Override + public boolean onEmailClick (String email) { + trackSponsoredMessageClicked(); + return false; + } + + @Override + public boolean onPhoneNumberClick (String phoneNumber) { + trackSponsoredMessageClicked(); + return false; + } + + @Override + public boolean onUrlClick (View view, String link, boolean promptUser, @NonNull TdlibUi.UrlOpenParameters openParameters) { + trackSponsoredMessageClicked(); + return false; + } }; } @@ -7425,6 +7980,66 @@ public static TGMessage valueOf (MessagesManager context, TdApi.Message msg) { return valueOf(context, msg, msg.content); } + @Nullable + private static TGMessage checkPendingContent (MessagesManager context, TdApi.Message msg, TdApi.MessageContent oldContent, @Nullable TdApi.MessageContent pendingContent, boolean allowAnimatedEmoji, boolean allowNonBubbleEmoji) { + if (pendingContent == null || oldContent.getConstructor() != TdApi.MessageAnimatedEmoji.CONSTRUCTOR && oldContent.getConstructor() != TdApi.MessageText.CONSTRUCTOR) { + return null; + } + + final @EmojiMessageContentType int emojiPendingContentType = getEmojiMessageContentType(pendingContent, allowAnimatedEmoji, allowNonBubbleEmoji); + if (emojiPendingContentType == EmojiMessageContentType.NOT_EMOJI) { + final TdApi.MessageText oldMessageText; + if (oldContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { + TdApi.MessageAnimatedEmoji oldEmoji = nonNull((TdApi.MessageAnimatedEmoji) oldContent); + oldMessageText = new TdApi.MessageText(Td.textOrCaption(oldEmoji), null, null); + } else if (oldContent.getConstructor() == TdApi.MessageText.CONSTRUCTOR) { + oldMessageText = nonNull((TdApi.MessageText) oldContent); + } else { + throw new IllegalArgumentException("Wrong content type"); + } + + final TdApi.MessageText newMessageText; + if (pendingContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { + TdApi.MessageAnimatedEmoji newEmoji = nonNull((TdApi.MessageAnimatedEmoji) pendingContent); + newMessageText = new TdApi.MessageText(Td.textOrCaption(newEmoji), null, null); + } else if (pendingContent.getConstructor() == TdApi.MessageText.CONSTRUCTOR) { + newMessageText = (TdApi.MessageText) pendingContent; + } else { + throw new IllegalArgumentException("Wrong content type"); + } + + return new TGMessageText(context, msg, oldMessageText, newMessageText); + } else { + return new TGMessageSticker(context, msg, oldContent, pendingContent); + } + /* + final TdApi.MessageText pendingMessageText = pendingContent.getConstructor() == TdApi.MessageText.CONSTRUCTOR ? + ((TdApi.MessageText) pendingContent) : null; + final TdApi.MessageAnimatedEmoji pendingMessageEmoji = pendingContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR ? + ((TdApi.MessageAnimatedEmoji) pendingContent) : null; + final boolean pendingContentIsCustomEmoji = allowCustomEmoji && ( + (pendingMessageText != null && NonBubbleEmojiLayout.isValidEmojiText(pendingMessageText.text)) || (pendingMessageEmoji != null)); + if (oldContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { + TdApi.MessageAnimatedEmoji oldEmoji = nonNull((TdApi.MessageAnimatedEmoji) oldContent); + if (pendingContentIsCustomEmoji) { + return new TGMessageSticker(context, msg, oldEmoji, pendingContent); + } else { + return new TGMessageText(context, msg, new TdApi.MessageText(Td.textOrCaption(oldEmoji), null, null), + new TdApi.MessageText(Td.textOrCaption(pendingContent), null, null)); + } + } else if (oldContent.getConstructor() == TdApi.MessageText.CONSTRUCTOR) { + TdApi.MessageText oldText = nonNull((TdApi.MessageText) oldContent); + if (pendingContentIsCustomEmoji) { + return new TGMessageSticker(context, msg, oldContent, pendingContent); + } else { + return new TGMessageText(context, msg, oldText, pendingMessageText); + } + } + + return null; + */ + } + public static TGMessage valueOf (MessagesManager context, TdApi.Message msg, TdApi.MessageContent content) { final Tdlib tdlib = context.controller().tdlib(); try { @@ -7432,7 +8047,9 @@ public static TGMessage valueOf (MessagesManager context, TdApi.Message msg, TdA return new TGMessageText(context, msg, new TdApi.FormattedText(Lang.getString(R.string.DeletedMessage), null)); } if (!StringUtils.isEmpty(msg.restrictionReason) && Settings.instance().needRestrictContent()) { - TGMessageText text = new TGMessageText(context, msg, new TdApi.FormattedText(msg.restrictionReason, null)); + TGMessageText text = new TGMessageText(context, msg, new TdApi.FormattedText(msg.restrictionReason, new TdApi.TextEntity[]{ + new TdApi.TextEntity(0, msg.restrictionReason.length(), new TdApi.TextEntityTypeItalic()) + })); text.addMessageFlags(FLAG_UNSUPPORTED); return text; } @@ -7444,31 +8061,30 @@ public static TGMessage valueOf (MessagesManager context, TdApi.Message msg, TdA int unsupportedStringRes = R.string.UnsupportedMessage; + final boolean allowAnimatedEmoji = !Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_EMOJI); + final boolean allowNonBubbleEmoji = Settings.instance().useBigEmoji(); + final TdApi.MessageContent pendingContent = tdlib.getPendingMessageText(msg.chatId, msg.id); + + TGMessage message = checkPendingContent(context, msg, content, pendingContent, allowAnimatedEmoji, allowNonBubbleEmoji); + if (message != null) { + return message; + } + switch (content.getConstructor()) { case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: { TdApi.MessageAnimatedEmoji emoji = nonNull((TdApi.MessageAnimatedEmoji) content); - TdApi.MessageContent pendingContent = tdlib.getPendingMessageText(msg.chatId, msg.id); - if (pendingContent != null) { - if (pendingContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR && !Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_EMOJI)) { - return new TGMessageSticker(context, msg, emoji, (TdApi.MessageAnimatedEmoji) pendingContent); - } else { - return new TGMessageText(context, msg, new TdApi.MessageText(Td.textOrCaption(emoji), null), new TdApi.MessageText(Td.textOrCaption(pendingContent), null)); - } - } - if (Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_EMOJI)) { - return new TGMessageText(context, msg, new TdApi.MessageText(Td.textOrCaption(emoji), null), null); + if (getEmojiMessageContentType(content, allowAnimatedEmoji, allowNonBubbleEmoji) == EmojiMessageContentType.NOT_EMOJI) { + return new TGMessageText(context, msg, new TdApi.MessageText(Td.textOrCaption(emoji), null, null), null); } else { return new TGMessageSticker(context, msg, emoji, null); } } - case TdApi.MessageText.CONSTRUCTOR: { - TdApi.MessageContent pendingContent = tdlib.getPendingMessageText(msg.chatId, msg.id); - if (pendingContent != null && pendingContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { - TdApi.MessageAnimatedEmoji animatedEmoji = (TdApi.MessageAnimatedEmoji) pendingContent; - return new TGMessageSticker(context, msg, null, animatedEmoji); + TdApi.MessageText messageText = nonNull((TdApi.MessageText) content); + if (getEmojiMessageContentType(content, allowAnimatedEmoji, allowNonBubbleEmoji) != EmojiMessageContentType.NOT_EMOJI) { + return new TGMessageSticker(context, msg, messageText, null); } - return new TGMessageText(context, msg, nonNull((TdApi.MessageText) content), (TdApi.MessageText) pendingContent); + return new TGMessageText(context, msg, nonNull((TdApi.MessageText) content), null); } case TdApi.MessageCall.CONSTRUCTOR: { return new TGMessageCall(context, msg, nonNull(((TdApi.MessageCall) content))); @@ -7530,6 +8146,9 @@ public static TGMessage valueOf (MessagesManager context, TdApi.Message msg, TdA case TdApi.MessageGiftedPremium.CONSTRUCTOR: { return new TGMessageService(context, msg, (TdApi.MessageGiftedPremium) content); } + case TdApi.MessagePremiumGiftCode.CONSTRUCTOR: { + return new TGMessageService(context, msg, (TdApi.MessagePremiumGiftCode) content); + } case TdApi.MessageChatSetTheme.CONSTRUCTOR: { return new TGMessageService(context, msg, (TdApi.MessageChatSetTheme) content); } @@ -7593,8 +8212,8 @@ public static TGMessage valueOf (MessagesManager context, TdApi.Message msg, TdA case TdApi.MessageSupergroupChatCreate.CONSTRUCTOR: { return new TGMessageService(context, msg, (TdApi.MessageSupergroupChatCreate) content); } - case TdApi.MessageWebsiteConnected.CONSTRUCTOR: { - return new TGMessageService(context, msg, (TdApi.MessageWebsiteConnected) content); + case TdApi.MessageBotWriteAccessAllowed.CONSTRUCTOR: { + return new TGMessageService(context, msg, (TdApi.MessageBotWriteAccessAllowed) content); } case TdApi.MessageChatUpgradeTo.CONSTRUCTOR: { return new TGMessageService(context, msg, (TdApi.MessageChatUpgradeTo) content); @@ -7614,9 +8233,28 @@ public static TGMessage valueOf (MessagesManager context, TdApi.Message msg, TdA case TdApi.MessageForumTopicIsHiddenToggled.CONSTRUCTOR: { return new TGMessageService(context, msg, (TdApi.MessageForumTopicIsHiddenToggled) content); } + case TdApi.MessagePremiumGiveawayCreated.CONSTRUCTOR: { + return new TGMessageService(context, msg, (TdApi.MessagePremiumGiveawayCreated) content); + } + case TdApi.MessagePremiumGiveawayCompleted.CONSTRUCTOR: { + return new TGMessageService(context, msg, (TdApi.MessagePremiumGiveawayCompleted) content); + } + case TdApi.MessagePremiumGiveaway.CONSTRUCTOR: { + if (BuildConfig.DEBUG) { + // uncomment once finished + return new TGMessageGiveaway(context, msg, (TdApi.MessagePremiumGiveaway) content); + } + break; + } // unsupported case TdApi.MessageInvoice.CONSTRUCTOR: case TdApi.MessagePassportDataSent.CONSTRUCTOR: + case TdApi.MessageStory.CONSTRUCTOR: + case TdApi.MessageChatSetBackground.CONSTRUCTOR: + case TdApi.MessageSuggestProfilePhoto.CONSTRUCTOR: + case TdApi.MessageUsersShared.CONSTRUCTOR: + case TdApi.MessageChatShared.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayWinners.CONSTRUCTOR: break; case TdApi.MessageUnsupported.CONSTRUCTOR: unsupportedStringRes = R.string.UnsupportedMessageType; @@ -7624,15 +8262,19 @@ public static TGMessage valueOf (MessagesManager context, TdApi.Message msg, TdA // bots only case TdApi.MessagePassportDataReceived.CONSTRUCTOR: case TdApi.MessagePaymentSuccessfulBot.CONSTRUCTOR: - case TdApi.MessageWebAppDataReceived.CONSTRUCTOR: + case TdApi.MessageWebAppDataReceived.CONSTRUCTOR: { Log.e("Received bot message for a regular user:\n%s", msg); break; + } default: { - Log.i("Weird content type: %s", msg.content.getClass().getName()); - break; + Td.assertMessageContent_d40af239(); + throw Td.unsupported(msg.content); } } - TGMessageText text = new TGMessageText(context, msg, new TdApi.FormattedText(Lang.getString(unsupportedStringRes), null)); + String unsupportedText = Lang.getString(unsupportedStringRes); + TGMessageText text = new TGMessageText(context, msg, new TdApi.FormattedText(unsupportedText, new TdApi.TextEntity[]{ + new TdApi.TextEntity(0, unsupportedText.length(), new TdApi.TextEntityTypeItalic()) + })); text.addMessageFlags(FLAG_UNSUPPORTED); return text; } catch (Throwable t) { @@ -7644,11 +8286,11 @@ public static TGMessage valueOf (MessagesManager context, TdApi.Message msg, TdA public static TGMessage valueOfError (MessagesManager context, TdApi.Message msg, Throwable error) { String text = Lang.getString(R.string.FailureMessageText); - TdApi.Object entitiesObject = Client.execute(new TdApi.GetTextEntities(text)); TdApi.TextEntity[] entities = null; - if (entitiesObject != null && entitiesObject.getConstructor() == TdApi.TextEntities.CONSTRUCTOR) { - entities = ((TdApi.TextEntities) entitiesObject).entities; - } + try { + TdApi.TextEntities result = Client.execute(new TdApi.GetTextEntities(text)); + entities = result.entities; + } catch (Client.ExecutionException ignored) { } TdApi.TextEntity logEntity = new TdApi.TextEntity(-1, -1, new TdApi.TextEntityTypePreCode()); @@ -7752,7 +8394,7 @@ public final void checkAvailableReactions (Runnable after) { public final void checkMessageFlags (Runnable r) { TdApi.Message msg = getMessage(getSmallestId()); - if (msg == null || isFakeMessage()) { + if (msg == null || isFakeMessage() || isSponsoredMessage()) { r.run(); return; } @@ -7766,6 +8408,13 @@ public final void checkMessageFlags (Runnable r) { }); } + public final boolean isCustomEmojiReactionsAvailable () { + if (messageAvailableReactions == null) + return false; + + return messageAvailableReactions.allowCustomEmoji; + } + public final TdApi.AvailableReaction[] getMessageAvailableReactions () { if (messageAvailableReactions == null) return null; @@ -7816,7 +8465,39 @@ public final TdApi.AvailableReaction[] getMessageAvailableReactions () { return 0; }); } - return reactions.toArray(new TdApi.AvailableReaction[0]); + return prioritizeElements(reactions.toArray(new TdApi.AvailableReaction[0]), messageReactions.getChosen()); + } + + private static TdApi.AvailableReaction[] prioritizeElements(TdApi.AvailableReaction[] inputArray, Set set) { + if (inputArray == null) { + return null; + } + + List resultList = new ArrayList<>(); + + for (TdApi.AvailableReaction element : inputArray) { + if (set.contains(TD.makeReactionKey(element.type))) { + resultList.add(element); + } + } + + for (TdApi.AvailableReaction element : inputArray) { + if (!set.contains(TD.makeReactionKey(element.type))) { + resultList.add(element); + } + } + + TdApi.AvailableReaction[] resultArray = new TdApi.AvailableReaction[resultList.size()]; + resultList.toArray(resultArray); + + return resultArray; + } + + public final boolean needShowReactionPopupPicker () { + return messageAvailableReactions != null && ( + messageAvailableReactions.allowCustomEmoji || + (messageAvailableReactions.popularReactions.length + messageAvailableReactions.recentReactions.length + messageAvailableReactions.topReactions.length > 25) + ); } // Utils @@ -7894,7 +8575,7 @@ private void computeQuickButtons () { SwipeQuickAction replyButton = null; if (canReply) { replyButton = new SwipeQuickAction(replyText, iQuickReply, () -> { - messagesController().showReply(getNewestMessage(), true, true); + messagesController().showReply(getNewestMessage(), null, true, true); }, true, false); rightActions.add(replyButton); } @@ -7924,7 +8605,9 @@ private void computeQuickButtons () { final boolean isOdd = a % 2 == 1; final SwipeQuickAction quickReaction = new SwipeQuickAction(reactionObj.getTitle(), reactionDrawable, () -> { boolean hasReaction = messageReactions.hasReaction(reactionType); - if (hasReaction || !canGetAddedReactions() || messagesController().callNonAnonymousProtection(getId() + reactionObj.hashCode(), null)) { + if (Config.DISABLE_ANONYMOUS_NON_OWNER_REACTIONS && !hasReaction && tdlib.isAnonymousAdminNonCreator(msg.chatId)) { + showContentHint(findCurrentView(), null, R.string.error_ANONYMOUS_REACTIONS_DISABLED); + } else if (!Config.PROTECT_ANONYMOUS_REACTIONS || hasReaction || !canGetAddedReactions() || messagesController().callNonAnonymousProtection(getId() + reactionObj.hashCode(), null)) { if (messageReactions.toggleReaction(reactionType, false, false, handler(findCurrentView(), null, () -> {}))) { scheduleSetReactionAnimation(new NextReactionAnimation(reactionObj, NextReactionAnimation.TYPE_QUICK)); } @@ -7944,7 +8627,7 @@ private void computeQuickButtons () { if (canShare) { leftActions.add(new SwipeQuickAction(shareText, iQuickShare, () -> { - messagesController().shareMessages(getChatId(), getAllMessages()); + messagesController().shareMessages(getAllMessages(), false); }, true, false)); } } @@ -8186,6 +8869,7 @@ private void startSetReactionAnimationIfReady () { new ReactionsOverlayView.ReactionInfo(context().reactionsOverlayManager()) .setSticker(nextSetReactionAnimation.reaction.staticCenterAnimationSicker(), false) .setAnimationEndListener(this::onQuickReactionAnimationFinish) + .setRepaintingColorIds(ColorId.text, ColorId.text) .setAnimatedPosition( new Point(startX, startY), new Point(finishX, finishY), @@ -8203,6 +8887,7 @@ private void startSetReactionAnimationIfReady () { new ReactionsOverlayView.ReactionInfo(context().reactionsOverlayManager()) .setSticker(nextSetReactionAnimation.reaction.staticCenterAnimationSicker(), false) .setAnimationEndListener(this::onQuickReactionAnimationFinish) + .setRepaintingColorIds(ColorId.text, ColorId.text) .setAnimatedPosition( new Point(startX, startY), new Point(finishX, finishY), @@ -8254,6 +8939,7 @@ public void act () { } TGStickerObj activateAnimation = nextSetReactionAnimation.reaction.activateAnimationSicker(); + final GifFile activateFullAnimation = activateAnimation.getFullAnimation(); if (activateAnimation.getFullAnimation() != null) { if (!activateAnimation.isCustomReaction()) { activateAnimation.getFullAnimation().setPlayOnce(true); @@ -8268,6 +8954,15 @@ public void act () { } } }); + activateFullAnimation.setOnTotalFrameCountLoadListener(() -> { + if (nextSetReactionAnimation != null && !activateFullAnimation.hasFrame(1)) { + nextSetReactionAnimation.fullscreenEmojiFinished = true; + if (nextSetReactionAnimation.fullscreenEffectFinished) { + finishAnimation.cancel(); + tdlib().ui().postDelayed(finishRunnable, 180l); + } + } + }); } else { nextSetReactionAnimation.fullscreenEmojiFinished = true; } @@ -8314,8 +9009,9 @@ public void startReactionBubbleAnimation (TdApi.ReactionType reactionType) { context().reactionsOverlayManager().addOverlay( new ReactionsOverlayView.ReactionInfo(context().reactionsOverlayManager()) .setSticker(overlaySticker, true) + .setRepaintingColorIds(ColorId.text, ColorId.text) .setUseDefaultSprayAnimation(tgReaction.isCustom()) - .setEmojiStatusEffect(tgReaction.isCustom() ? tgReaction.newCenterAnimationSicker(): null) + .setEmojiStatusEffect(tgReaction.isCustom() ? tgReaction.newCenterAnimationSicker() : null) .setPosition(new Point(bubbleX, bubbleY), Screen.dp(90)) .setAnimatedPositionOffsetProvider(new QuickReactionAnimatedPositionOffsetProvider()) ); @@ -8495,13 +9191,20 @@ private TooltipOverlayView.LocationProvider getReactionBubbleLocationProvider (T }; } + private TooltipOverlayView.LocationProvider getReplyLocationProvider () { + return (targetView, outRect) -> { + if (replyData == null) { + return; + } + outRect.left = replyData.getLastX(); + outRect.top = replyData.getLastY(); + outRect.right = outRect.left + replyData.width(!useBubble()); + outRect.bottom = outRect.top + ReplyComponent.height(); + }; + } + private boolean openMessageFromChannel () { - TooltipOverlayView.TooltipBuilder tooltipBuilder = context().tooltipManager().builder(findCurrentView()).locate((targetView, outRect) -> { - outRect.left = (int) (isChannelHeaderCounterX - Screen.dp(7)); - outRect.top = (int) (isChannelHeaderCounterY - Screen.dp(7)); - outRect.right = (int) (isChannelHeaderCounterX + Screen.dp(7)); - outRect.bottom = (int) (isChannelHeaderCounterY + Screen.dp(7)); - }); + TooltipOverlayView.TooltipBuilder tooltipBuilder = context().tooltipManager().builder(findCurrentView()).locate((targetView, outRect) -> isChannelHeaderCounterLastDrawRect.round(outRect)); tdlib().ui().openChat(this, sender.getChatId(), new TdlibUi.ChatOpenParameters() .urlOpenParameters(new TdlibUi.UrlOpenParameters().tooltip(tooltipBuilder)).keepStack() @@ -8516,26 +9219,23 @@ private boolean needDrawChannelIconInHeader () { private TooltipOverlayView.TooltipInfo languageSelectorTooltip; private void openLanguageSelectorInlineMode () { - TooltipOverlayView.TooltipBuilder tooltipBuilder = context().tooltipManager().builder(findCurrentView()).locate((targetView, outRect) -> { - outRect.left = (int) (isTranslatedCounterX - Screen.dp(7)); - outRect.top = (int) (isTranslatedCounterY - Screen.dp(7)); - outRect.right = (int) (isTranslatedCounterX + Screen.dp(7)); - outRect.bottom = (int) (isTranslatedCounterY + Screen.dp(7)); - }); + TooltipOverlayView.TooltipBuilder tooltipBuilder = context().tooltipManager().builder(findCurrentView()) + .locate((targetView, outRect) -> isTranslatedCounterLastDrawRect.round(outRect)); languageSelectorTooltip = tooltipBuilder.show(this, this::showLanguageSelectorInlineMode);//.hideDelayed(3500, TimeUnit.MILLISECONDS); } private void showTranslateErrorMessageBubbleMode (String message) { - TooltipOverlayView.TooltipBuilder tooltipBuilder = context().tooltipManager().builder(findCurrentView()).locate((targetView, outRect) -> { - outRect.left = (int) (isTranslatedCounterX - Screen.dp(7)); - outRect.top = (int) (isTranslatedCounterY - Screen.dp(7)); - outRect.right = (int) (isTranslatedCounterX + Screen.dp(7)); - outRect.bottom = (int) (isTranslatedCounterY + Screen.dp(7)); - }); + TooltipOverlayView.TooltipBuilder tooltipBuilder = context().tooltipManager().builder(findCurrentView()) + .locate((targetView, outRect) -> isTranslatedCounterLastDrawRect.round(outRect)); languageSelectorTooltip = tooltipBuilder.show(tdlib, message).hideDelayed(3500, TimeUnit.MILLISECONDS); } + private TooltipOverlayView.TooltipInfo showMessageTooltip (TooltipOverlayView.LocationProvider locate, String message, long msDuration) { + TooltipOverlayView.TooltipBuilder tooltipBuilder = context().tooltipManager().builder(findCurrentView()).locate(locate); + return tooltipBuilder.show(tdlib, message).hideDelayed(msDuration, TimeUnit.MILLISECONDS); + } + private void showLanguageSelectorInlineMode (View v) { if (languageSelectorTooltip == null) return; @@ -8629,7 +9329,7 @@ public boolean isTranslatable () { public void checkTranslatableText (Runnable after) { final TdApi.FormattedText textToTranslate = getTextToTranslateImpl(); this.textToTranslate = textToTranslate; - textToTranslateOriginalLanguage = textToTranslate != null ? mTranslationsManager.getCachedTextLanguage(textToTranslate.text): null; + textToTranslateOriginalLanguage = textToTranslate != null ? mTranslationsManager.getCachedTextLanguage(textToTranslate.text) : null; if (textToTranslate != null && textToTranslateOriginalLanguage == null && translationStyleMode() != Settings.TRANSLATE_MODE_NONE) { LanguageDetector.detectLanguage(context(), textToTranslate.text, lang -> { mTranslationsManager.saveCachedTextLanguage(textToTranslate.text, textToTranslateOriginalLanguage = lang); @@ -8643,15 +9343,13 @@ public void checkTranslatableText (Runnable after) { } } - - protected @Nullable TdApi.FormattedText getTextToTranslateImpl () { return null; // override } private void setTranslatedStatus (int status, boolean animated) { boolean show = status != TranslationCounterDrawable.TRANSLATE_STATUS_DEFAULT || translatedCounterForceShow; - isTranslatedCounterDrawable.setInvalidateCallback(show? this::invalidate: null); + isTranslatedCounterDrawable.setInvalidateCallback(show ? this::invalidate : null); isTranslatedCounterDrawable.setStatus(status, animated); if (show) { isTranslatedCounter.show(animated); @@ -8664,16 +9362,271 @@ private void setTranslatedStatus (int status, boolean animated) { private void checkSelectLanguageWarning (boolean force) { String current = mTranslationsManager.getCurrentTranslatedLanguage(); if (current == null || StringUtils.equalsOrBothEmpty(current, getOriginalMessageLanguage()) || force) { - context().tooltipManager().builder(findCurrentView()).locate((targetView, outRect) -> { - outRect.left = (int) (isTranslatedCounterX - Screen.dp(7)); - outRect.top = (int) (isTranslatedCounterY - Screen.dp(7)); - outRect.right = (int) (isTranslatedCounterX + Screen.dp(7)); - outRect.bottom = (int) (isTranslatedCounterY + Screen.dp(7)); - }).show(tdlib, Lang.getString(R.string.TapToSelectLanguage)).hideDelayed(3500, TimeUnit.MILLISECONDS);; + context().tooltipManager().builder(findCurrentView()) + .locate((targetView, outRect) -> isTranslatedCounterLastDrawRect.round(outRect)) + .show(tdlib, Lang.getString(R.string.TapToSelectLanguage)).hideDelayed(3500, TimeUnit.MILLISECONDS);; } } protected void setTranslationResult (@Nullable TdApi.FormattedText text) { manager.updateMessageTranslation(getChatId(), getSmallestId(), text); }; + + /* */ + + public long getFirstEmojiId () { + TdApi.FormattedText text = getTextToTranslate(); + if (text == null || text.text == null || text.entities == null || text.entities.length == 0) return -1; + + for (TdApi.TextEntity entity : text.entities) { + if (Td.isCustomEmoji(entity.type)) { + return ((TdApi.TextEntityTypeCustomEmoji) entity.type).customEmojiId; + } + } + + return -1; + } + + public long[] getUniqueEmojiPackIdList () { + long[] emojiIds = TD.getUniqueEmojiIdList(getTextToTranslate()); + + LongSet emojiSets = new LongSet(); + for (long emojiId : emojiIds) { + TdlibEmojiManager.Entry entry = tdlib().emoji().find(emojiId); + if (entry == null || entry.value == null) continue; + emojiSets.add(entry.value.setId); + } + + return emojiSets.toArray(); + } + + /* Reaction Avatars */ + + // todo: update when supergroup updated + + public void updateReactionAvatars (boolean animated) { + messageReactions.updateCounterAnimators(animated); + if (BitwiseUtils.hasFlag(flags, FLAG_LAYOUT_BUILT)) { + buildReactions(animated); + } + } + + public boolean matchesReactionSenderAvatarFilter (TdApi.FormattedText messageText, TdApi.MessageReaction reaction, TdApi.MessageSender sender) { + final long currentChatId = getChatId(); + + if (Td.equalsTo(reaction.usedSenderId, sender)) { + return true; + } + if (tdlib.isSelfSender(sender) + || getChatId() == Td.getSenderId(sender) + || Td.equalsTo(sender, getInReplyToSender())) { + return true; + } + + long userId = Td.getSenderUserId(sender); + final TdApi.User user = userId != 0 ? tdlib.cache().user(userId) : null; + if (user != null && (user.isContact || user.isCloseFriend || TD.containsMention(messageText, user))) { + return true; + } + + final TdApi.Supergroup supergroup = tdlib.chatToSupergroup(currentChatId); + if (tdlib.chatMemberCount(currentChatId) < 50 && (supergroup == null || (!supergroup.hasLocation && !supergroup.hasLinkedChat && Td.isEmpty(supergroup.usernames)))) { + return true; + } + + return false; + } + + // Sponsored-related tools + + public final boolean isSponsoredMessage () { + return sponsoredMessage != null; + } + + public void trackSponsoredMessageClicked () { + if (isSponsoredMessage()) { + tdlib.client().send(new TdApi.ClickChatSponsoredMessage(msg.chatId, sponsoredMessage.messageId), tdlib.silentHandler()); + } + } + + public void openSponsoredMessage () { + if (!isSponsoredMessage()) { + return; + } + final RunnableBool after = ok -> { + if (ok) { + trackSponsoredMessageClicked(); + } + }; + TdlibUi.UrlOpenParameters openParameters = openParameters() + .requireOpenPrompt(); + TdApi.MessageSponsor sponsor = sponsoredMessage.sponsor; + switch (sponsor.type.getConstructor()) { + case TdApi.MessageSponsorTypeBot.CONSTRUCTOR: { + TdApi.MessageSponsorTypeBot bot = (TdApi.MessageSponsorTypeBot) sponsor.type; + tdlib.ui().openInternalLinkType(this, null, bot.link, openParameters, after); + break; + } + case TdApi.MessageSponsorTypeWebApp.CONSTRUCTOR: { + TdApi.MessageSponsorTypeWebApp webApp = (TdApi.MessageSponsorTypeWebApp) sponsor.type; + tdlib.ui().openInternalLinkType(this, null, webApp.link, openParameters, after); + break; + } + case TdApi.MessageSponsorTypePublicChannel.CONSTRUCTOR: { + TdApi.MessageSponsorTypePublicChannel publicChannel = (TdApi.MessageSponsorTypePublicChannel) sponsor.type; + if (publicChannel.link != null) { + tdlib.ui().openInternalLinkType(this, null, publicChannel.link, openParameters, after); + } else { + tdlib.ui().openChat(this, publicChannel.chatId, new TdlibUi.ChatOpenParameters().urlOpenParameters(openParameters).keepStack().after(chatId -> { + after.runWithBool(true); + })); + } + break; + } + case TdApi.MessageSponsorTypePrivateChannel.CONSTRUCTOR: { + TdApi.MessageSponsorTypePrivateChannel privateChannel = (TdApi.MessageSponsorTypePrivateChannel) sponsor.type; + tdlib.ui().openUrl(this, privateChannel.inviteLink, openParameters, after); + break; + } + case TdApi.MessageSponsorTypeWebsite.CONSTRUCTOR: { + TdApi.MessageSponsorTypeWebsite website = (TdApi.MessageSponsorTypeWebsite) sponsor.type; + tdlib.ui().openUrl(this, website.url, openParameters, after); + break; + } + default: + Td.assertMessageSponsorType_cdabde01(); + throw Td.unsupported(sponsor.type); + } + } + + public @StringRes int getSponsoredMessageButtonResId () { + if (!isSponsoredMessage()) { + return 0; + } + TdApi.MessageSponsor sponsor = sponsoredMessage.sponsor; + switch (sponsor.type.getConstructor()) { + case TdApi.MessageSponsorTypeBot.CONSTRUCTOR: + return R.string.AdOpenBot; + case TdApi.MessageSponsorTypePrivateChannel.CONSTRUCTOR: + return R.string.AdOpenChannel; + case TdApi.MessageSponsorTypePublicChannel.CONSTRUCTOR: { + TdApi.MessageSponsorTypePublicChannel publicChannel = (TdApi.MessageSponsorTypePublicChannel) sponsor.type; + if (publicChannel.link != null && publicChannel.link.getConstructor() == TdApi.InternalLinkTypeMessage.CONSTRUCTOR) { + return R.string.AdOpenPost; + } + return R.string.AdOpenChannel; + } + case TdApi.MessageSponsorTypeWebsite.CONSTRUCTOR: { + return R.string.AdOpenWebsite; + } + case TdApi.MessageSponsorTypeWebApp.CONSTRUCTOR: { + return R.string.AdOpenApp; + } + default: + Td.assertMessageSponsorType_cdabde01(); + throw Td.unsupported(sponsor.type); + } + } + + public TdApi.SponsoredMessage getSponsoredMessage () { + return sponsoredMessage; + } + + public String getSponsoredMessageUrl () { + if (!isSponsoredMessage() || !Config.ALLOW_SPONSORED_MESSAGE_LINK_COPY) { + return null; + } + + TdApi.MessageSponsor sponsor = sponsoredMessage.sponsor; + switch (sponsor.type.getConstructor()) { + case TdApi.MessageSponsorTypeBot.CONSTRUCTOR: { + TdApi.MessageSponsorTypeBot bot = (TdApi.MessageSponsorTypeBot) sponsor.type; + if (bot.link.getConstructor() == TdApi.InternalLinkTypeBotStart.CONSTRUCTOR) { + TdApi.InternalLinkTypeBotStart botStart = (TdApi.InternalLinkTypeBotStart) bot.link; + return tdlib.tMeStartUrl(botStart.botUsername, botStart.startParameter, false); + } + // Ignoring other types + break; + } + case TdApi.MessageSponsorTypeWebApp.CONSTRUCTOR: { + TdApi.MessageSponsorTypeWebApp webApp = (TdApi.MessageSponsorTypeWebApp) sponsor.type; + // No need in URL + break; + } + case TdApi.MessageSponsorTypePrivateChannel.CONSTRUCTOR: { + TdApi.MessageSponsorTypePrivateChannel privateChannel = (TdApi.MessageSponsorTypePrivateChannel) sponsor.type; + return privateChannel.inviteLink; + } + case TdApi.MessageSponsorTypePublicChannel.CONSTRUCTOR: { + TdApi.MessageSponsorTypePublicChannel publicChannel = (TdApi.MessageSponsorTypePublicChannel) sponsor.type; + if (publicChannel.link != null) { + if (publicChannel.link.getConstructor() == TdApi.InternalLinkTypeMessage.CONSTRUCTOR) { + return ((TdApi.InternalLinkTypeMessage) publicChannel.link).url; + } + } else { + return tdlib.tMeChatUrl(publicChannel.chatId); + } + } + case TdApi.MessageSponsorTypeWebsite.CONSTRUCTOR: { + TdApi.MessageSponsorTypeWebsite website = (TdApi.MessageSponsorTypeWebsite) sponsor.type; + return website.url; + } + default: + Td.assertMessageSponsorType_cdabde01(); + throw Td.unsupported(sponsor.type); + } + + return null; + } + + /* * */ + + @Nullable + public final TdApi.FormattedText getMessageText () { + synchronized (this) { + if (combinedMessages != null && !combinedMessages.isEmpty()) { + final TdApi.FormattedText sep = new TdApi.FormattedText(" ", new TdApi.TextEntity[0]); + TdApi.FormattedText result = new TdApi.FormattedText("", new TdApi.TextEntity[0]); + for (TdApi.Message msg : combinedMessages) { + final TdApi.FormattedText textPart = msg.content != null ? Td.textOrCaption(msg.content) : null; + if (!Td.isEmpty(textPart)) { + if (!Td.isEmpty(result)) { + result = Td.concat(result, sep); + } + result = Td.concat(result, textPart); + } + } + return !Td.isEmpty(result) ? result : null; + } + } + return msg.content != null ? Td.textOrCaption(msg.content) : null; + } + + + /* * */ + + public static @EmojiMessageContentType int getEmojiMessageContentType (TdApi.MessageContent content) { + final boolean allowAnimatedEmoji = !Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_EMOJI); + final boolean allowNonBubbleEmoji = Settings.instance().useBigEmoji(); + return getEmojiMessageContentType(content, allowAnimatedEmoji, allowNonBubbleEmoji); + } + + public static @EmojiMessageContentType int getEmojiMessageContentType (TdApi.MessageContent content, boolean allowAnimatedEmoji, boolean allowNonBubbleEmoji) { + if (content == null) { + return EmojiMessageContentType.NOT_EMOJI; + } + + if (content.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { + if (allowAnimatedEmoji && TD.isStickerFromAnimatedEmojiPack(content)) { + return EmojiMessageContentType.ANIMATED_EMOJI; + } else if (allowNonBubbleEmoji) { + return EmojiMessageContentType.NON_BUBBLE_EMOJI; + } + } else if (content.getConstructor() == TdApi.MessageText.CONSTRUCTOR) { + if (allowNonBubbleEmoji && NonBubbleEmojiLayout.isValidEmojiText(((TdApi.MessageText) content).text)) { + return EmojiMessageContentType.NON_BUBBLE_EMOJI; + } + } + return EmojiMessageContentType.NOT_EMOJI; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageBotInfo.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageBotInfo.java index 31253d508d..9be5f89152 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageBotInfo.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageBotInfo.java @@ -49,7 +49,7 @@ public TGMessageBotInfo (MessagesManager context, long chatId, String descriptio } private TGMessageBotInfo (MessagesManager context, long chatId, TdApi.FormattedText description) { - super(context, TD.newFakeMessage(chatId, context.controller().tdlib().sender(chatId), new TdApi.MessageText(description, null))); + super(context, TD.newFakeMessage(chatId, context.controller().tdlib().sender(chatId), new TdApi.MessageText(description, null, null))); if (!tdlib().isRepliesChat(ChatId.fromUserId(getSender().getUserId()))) { String text = Lang.getString(R.string.WhatThisBotCanDo); diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageCall.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageCall.java index 188e0a5cea..00e26ffe85 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageCall.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageCall.java @@ -28,6 +28,7 @@ import org.thunderdog.challegram.component.chat.MessagesManager; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; @@ -52,8 +53,7 @@ public TGMessageCall (MessagesManager context, TdApi.Message msg, TdApi.MessageC // private Drawable phoneIcon, callIcon; private @DrawableRes int callIconId; - private @ColorId - int callIconColorId; + private @PorterDuffColorId int callIconColorId; // private String title, subtitle; private String trimmedTitle, trimmedSubtitle; @@ -92,22 +92,22 @@ protected int getBubbleContentPadding () { @Override protected void drawContent (MessageView view, Canvas c, int startX, int startY, int maxWidth) { - Drawable phoneIcon = view.getSparseDrawable(callRaw.isVideo ? R.drawable.baseline_videocam_24 : R.drawable.baseline_phone_24, 0); - Drawable callIcon = view.getSparseDrawable(callIconId, 0); + Drawable phoneIcon = view.getSparseDrawable(callRaw.isVideo ? R.drawable.baseline_videocam_24 : R.drawable.baseline_phone_24, ColorId.NONE); + Drawable callIcon = view.getSparseDrawable(callIconId, ColorId.NONE); if (useBubbles()) { int colorId = isOutgoingBubble() ? ColorId.bubbleOut_file : ColorId.file; Drawables.draw(c, phoneIcon, startX + getContentWidth() - getContentHeight() / 2f - phoneIcon.getMinimumWidth() / 2f, startY + getContentHeight() / 2f - phoneIcon.getMinimumHeight() / 2f, PorterDuffPaint.get(colorId)); } else { int radius = Screen.dp(FileProgressComponent.DEFAULT_FILE_RADIUS); c.drawCircle(startX + radius, startY + radius, radius, Paints.fillingPaint(Theme.getColor(ColorId.file))); - Drawables.draw(c, phoneIcon, startX + radius - phoneIcon.getMinimumWidth() / 2f, startY + radius - phoneIcon.getMinimumHeight() / 2f, Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, phoneIcon, startX + radius - phoneIcon.getMinimumWidth() / 2f, startY + radius - phoneIcon.getMinimumHeight() / 2f, Paints.whitePorterDuffPaint()); startX += radius * 2 + Screen.dp(11f); } if (useBubbles()) { startY -= Screen.dp(4f); } c.drawText(trimmedTitle, startX, startY + Screen.dp(21f), Paints.getMediumTextPaint(15f, getTextColor(), needFakeTitle)); - Drawables.draw(c, callIcon, startX, startY + Screen.dp(callIconId == R.drawable.baseline_call_missed_18 ? 27.5f : callIconId == R.drawable.baseline_call_made_18 ? 26.5f : 27f), Paints.getPorterDuffPaint(Theme.getColor(callIconColorId))); + Drawables.draw(c, callIcon, startX, startY + Screen.dp(callIconId == R.drawable.baseline_call_missed_18 ? 27.5f : callIconId == R.drawable.baseline_call_made_18 ? 26.5f : 27f), PorterDuffPaint.get(callIconColorId)); c.drawText(trimmedSubtitle, startX + Screen.dp(20f), startY + Screen.dp(41f), Paints.getRegularTextPaint(13f, getDecentColor())); } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageContact.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageContact.java index ead7cdab05..b1973a7068 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageContact.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageContact.java @@ -25,8 +25,8 @@ import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.loader.Receiver; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibCache; -import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; @@ -44,9 +44,8 @@ public class TGMessageContact extends TGMessage implements TdlibCache.UserDataCh private TdApi.User user; private ImageFile avatar; - private @ColorId - int avatarColorId; - private Letters letters; + private final TdlibAccentColor accentColor; + private final Letters letters; private String tName; private String tPhone; @@ -70,7 +69,10 @@ public TGMessageContact (MessagesManager context, TdApi.Message msg, TdApi.Messa if (contact.userId != 0) { this.user = tdlib.cache().user(contact.userId); + this.accentColor = tdlib.cache().userAccentColor(contact.userId); tdlib.cache().addUserDataListener(contact.userId, this); + } else { + this.accentColor = tdlib.accentColorForString(phone); } } @@ -117,7 +119,6 @@ private void buildName () { private void buildAvatar () { if (user == null || TD.isPhotoEmpty(user.profilePhoto)) { avatar = null; - avatarColorId = TD.getAvatarColorId(TD.isUserDeleted(user) ? -1 : userId, tdlib.myUserId()); } else { avatar = new ImageFile(tdlib, user.profilePhoto.small); avatar.setSize(avatarSize); @@ -132,8 +133,8 @@ protected void drawContent (MessageView view, Canvas c, int startX, int startY, } startY += Screen.dp(1f); if (avatar == null) { - c.drawCircle(startX + avatarRadius, startY + avatarRadius, avatarRadius, Paints.fillingPaint(Theme.getColor(avatarColorId))); - Paints.drawLetters(c, letters, startX + avatarRadius - (int) (lettersWidth / 2f), startY + lettersTop, LETTERS_SIZE); + c.drawCircle(startX + avatarRadius, startY + avatarRadius, avatarRadius, Paints.fillingPaint(accentColor.getPrimaryColor())); + c.drawText(letters.text, startX + avatarRadius - (int) (lettersWidth / 2f), startY + lettersTop, Paints.getMediumTextPaint(LETTERS_SIZE, accentColor.getPrimaryContentColor(), letters.needFakeBold)); } else { receiver.setBounds(startX, startY, startX + avatarSize, startY + avatarSize); if (receiver.needPlaceholder()) { diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageFile.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageFile.java index 1ddba665d0..d593a2c57d 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageFile.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageFile.java @@ -37,6 +37,7 @@ import org.thunderdog.challegram.tool.DrawAlgorithms; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.util.text.Highlight; import org.thunderdog.challegram.util.text.Text; @@ -74,7 +75,7 @@ private class CaptionedFile implements ListAnimator.Measurable, Animatable { private TdApi.FormattedText effectiveCaption; public final ReplaceAnimator caption; private TextWrapper captionWrapper; - private int captionMediaKeyOffset; + private long captionMediaKeyOffset; private final VariableFloat lastLineWidth = new VariableFloat(0); private final VariableFloat needBottomLineExpand = new VariableFloat(1f); @@ -91,7 +92,7 @@ public CaptionedFile (TdApi.Message message, FileComponent component, TdApi.Form this.caption = new ReplaceAnimator<>(animator -> { files.measure(needAnimateChanges()); invalidate(); - }, AnimatorUtils.DECELERATE_INTERPOLATOR, 200l); + }, AnimatorUtils.DECELERATE_INTERPOLATOR, TEXT_CROSS_FADE_DURATION_MS); updateCaption(false); } @@ -118,7 +119,7 @@ private boolean updateCaption (boolean animated) { } private boolean updateCaption (boolean animated, boolean force) { - TdApi.FormattedText caption = translatedCaption != null ? translatedCaption: (this.pendingCaption != null ? this.pendingCaption : this.serverCaption); + TdApi.FormattedText caption = translatedCaption != null ? translatedCaption : (this.pendingCaption != null ? this.pendingCaption : this.serverCaption); if (!Td.equalsTo(this.effectiveCaption, caption) || force) { this.effectiveCaption = Td.isEmpty(caption) ? null : caption; if (this.captionWrapper != null) { @@ -252,6 +253,7 @@ private CaptionedFile newFile (TGMessage context, TdApi.Message message) { FileComponent component; TdApi.FormattedText caption; boolean disallowTouch = true; + //noinspection SwitchIntDef switch (message.content.getConstructor()) { case TdApi.MessageDocument.CONSTRUCTOR: { TdApi.MessageDocument document = (TdApi.MessageDocument) message.content; @@ -351,6 +353,7 @@ protected boolean updateMessageContent (TdApi.Message message, TdApi.MessageCont boolean fileChanged = false; TdApi.FormattedText serverCaption; FileComponent component = file.component; + //noinspection SwitchIntDef switch (newContent.getConstructor()) { case TdApi.MessageAudio.CONSTRUCTOR: { TdApi.MessageAudio audio = (TdApi.MessageAudio) newContent; @@ -482,9 +485,12 @@ protected void drawContent (MessageView view, Canvas c, final int startX, final final int backgroundColor = getContentBackgroundColor(); final int contentReplaceColor = getContentReplaceColor(); final boolean clip = useBubbles(); + final int restoreToCount; if (clip) { - c.save(); + restoreToCount = Views.save(c); c.clipRect(getActualLeftContentEdge(), getTopContentEdge(), getActualRightContentEdge(), getBottomContentEdge()); + } else { + restoreToCount = -1; } for (ListAnimator.Entry entry : files) { ImageReceiver imageReceiver = receiver.getImageReceiver(entry.item.receiverId); @@ -519,7 +525,7 @@ protected void drawContent (MessageView view, Canvas c, final int startX, final } } if (clip) { - c.restore(); + Views.restore(c, restoreToCount); } } @@ -550,12 +556,12 @@ public void requestMediaContent (ComplexReceiver receiver, boolean invalidate, i } @Override - protected float getBubbleExpandFactor () { + protected float getIntermediateBubbleExpandFactor () { return filesList.get(filesList.size() - 1).needBottomLineExpand.get(); } @Override - protected int getAnimatedBottomLineWidth () { + protected int getAnimatedBottomLineWidth (int bubbleTimePartWidth) { return Math.round(filesList.get(filesList.size() - 1).lastLineWidth.get()); } @@ -708,9 +714,12 @@ private TdApi.FormattedText getTranslationSafeText (TdApi.FormattedText text) { @Nullable @Override public TdApi.FormattedText getTextToTranslateImpl () { + if (filesList == null) { + return null; + } if (filesList.size() == 1) { CaptionedFile file = filesList.get(0); - return file.hasCaption() ? getTranslationSafeText(file.serverCaption): null; + return file.hasCaption() ? getTranslationSafeText(file.serverCaption) : null; } TdApi.FormattedText resultText = new TdApi.FormattedText("", new TdApi.TextEntity[0]); @@ -726,7 +735,7 @@ public TdApi.FormattedText getTextToTranslateImpl () { } } - return filesWithCaption > 0? Td.trim(resultText): null; + return filesWithCaption > 0 ? Td.trim(resultText) : null; } @Override @@ -735,10 +744,10 @@ protected void setTranslationResult (@Nullable TdApi.FormattedText text) { if (text != null) { translatedParts = new ArrayList<>(filesList.size()); String sep = "\uD83D\uDCC4"; - int indexStart = text.text.startsWith(sep) ? sep.length(): 0; + int indexStart = text.text.startsWith(sep) ? sep.length() : 0; while (true) { int index = text.text.indexOf(sep, indexStart); - TdApi.FormattedText part = (index == -1) ? Td.substring(text, indexStart): Td.substring(text, indexStart, index); + TdApi.FormattedText part = (index == -1) ? Td.substring(text, indexStart) : Td.substring(text, indexStart, index); translatedParts.add(Td.trim(part)); if (index == -1) { break; @@ -753,8 +762,8 @@ protected void setTranslationResult (@Nullable TdApi.FormattedText text) { for (int a = 0; a < filesList.size(); a++) { CaptionedFile file = filesList.get(a); - TdApi.FormattedText caption = translatedParts != null ? translatedParts.get(a): null; - file.translatedCaption = !Td.isEmpty(caption) ? caption: null; + TdApi.FormattedText caption = translatedParts != null ? translatedParts.get(a) : null; + file.translatedCaption = !Td.isEmpty(caption) ? caption : null; file.updateCaption(needAnimateChanges(), true); } rebuildAndUpdateContent(); diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageGiveaway.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageGiveaway.java new file mode 100644 index 0000000000..f651deebec --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageGiveaway.java @@ -0,0 +1,32 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 06/11/2023 + */ +package org.thunderdog.challegram.data; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.component.chat.MessagesManager; + +public class TGMessageGiveaway extends TGMessage { + private TdApi.MessagePremiumGiveaway premiumGiveaway; + + public TGMessageGiveaway (MessagesManager manager, TdApi.Message msg, TdApi.MessagePremiumGiveaway premiumGiveaway) { + super(manager, msg); + this.premiumGiveaway = premiumGiveaway; + } + + @Override + protected void buildContent (int maxWidth) { + // TODO + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageLocation.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageLocation.java index 20127096d6..e3132a6c1b 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageLocation.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageLocation.java @@ -41,6 +41,7 @@ import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.LiveLocationManager; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; @@ -51,6 +52,7 @@ import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.ui.MapController; import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.util.text.Letters; @@ -64,6 +66,7 @@ import me.vkryl.core.ColorUtils; import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; +import me.vkryl.td.Td; public class TGMessageLocation extends TGMessage implements LiveLocationManager.UserLocationChangeListener { private final TdApi.Location point; @@ -77,7 +80,7 @@ public class TGMessageLocation extends TGMessage implements LiveLocationManager. private ImageFile previewFile; private Letters previewLetters; private float previewLettersWidth; - private int previewAvatarColorId; + private TdlibAccentColor previewAccentColor; private int previewWidth; private int previewHeight; @@ -121,7 +124,7 @@ private void setLivePeriod (int livePeriod, int expiresInSeconds, boolean isInit updateTimer(); } if (!isInitial) { - if (msg.content.getConstructor() == TdApi.MessageLocation.CONSTRUCTOR) { + if (Td.isLocation(msg.content)) { ((TdApi.MessageLocation) msg.content).expiresIn = expiresInSeconds; } checkAlive(true); @@ -132,7 +135,7 @@ private void setLivePeriod (int livePeriod, int expiresInSeconds, boolean isInit } private void updatePreviewUser (long userId, TdApi.User user) { - this.previewAvatarColorId = tdlib.cache().userAvatarColorId(user); + this.previewAccentColor = tdlib.cache().userAccentColor(userId); if (user != null) { this.previewFile = TD.getAvatar(tdlib, user); this.previewLetters = TD.getLetters(user); @@ -145,7 +148,7 @@ private void updatePreviewUser (long userId, TdApi.User user) { private void updatePreviewChat (long chatId, TdApi.Chat chat) { this.previewFile = tdlib.chatAvatar(chatId); - this.previewAvatarColorId = tdlib.chatAvatarColorId(chatId); + this.previewAccentColor = tdlib.chatAccentColor(chatId); this.previewLetters = tdlib.chatLetters(chatId); this.previewLettersWidth = Paints.measureLetters(previewLetters, 18f); } @@ -718,7 +721,7 @@ protected void drawContent (MessageView view, Canvas c, final int startX, int st int mapCenterX = startX + previewWidth / 2; int mapCenterY = startY + previewHeight / 2; - c.save(); + final int restoreToCount = Views.save(c); c.scale(.85f, .85f, mapCenterX, mapCenterY); Bitmap pinBgIcon = Icons.getLivePin(); @@ -757,13 +760,13 @@ protected void drawContent (MessageView view, Canvas c, final int startX, int st scheduleUpdate(SCHEDULE_FLAG_PULSE, false, pulseUpdateDelay); } - c.drawBitmap(pinBgIcon, mapCenterX - pinBgIcon.getWidth() / 2, pinTop, Paints.getBitmapPaint()); + c.drawBitmap(pinBgIcon, mapCenterX - pinBgIcon.getWidth() / 2f, pinTop, Paints.getBitmapPaint()); int iconSize; if (livePeriod > 0) { iconSize = pinRadius; if (previewFile == null && previewLetters != null) { - c.drawCircle(mapCenterX, pinCenterY, pinRadius, Paints.fillingPaint(Theme.getColor(previewAvatarColorId))); + c.drawCircle(mapCenterX, pinCenterY, pinRadius, Paints.fillingPaint(previewAccentColor.getPrimaryColor())); Paints.drawLetters(c, previewLetters, mapCenterX - previewLettersWidth / 2, pinCenterY + Screen.dp(7f), 18f); } } else { @@ -772,12 +775,12 @@ protected void drawContent (MessageView view, Canvas c, final int startX, int st if (iconReceiver.needPlaceholder()) { float iconAlpha = 1f - ((ImageReceiver) iconReceiver).getDisplayAlpha(); - Paint paint = Paints.getPorterDuffPaint(0xffffffff); + Paint paint = Paints.whitePorterDuffPaint(); if (iconAlpha != 1f) { paint.setAlpha((int) (255f * iconAlpha)); } - Drawable pinIcon = view.getSparseDrawable(R.drawable.baseline_location_on_24, 0); - Drawables.draw(c, pinIcon, mapCenterX - pinIcon.getMinimumWidth() / 2, pinCenterY - pinIcon.getMinimumHeight() / 2, paint); + Drawable pinIcon = view.getSparseDrawable(R.drawable.baseline_location_on_24, ColorId.NONE); + Drawables.draw(c, pinIcon, mapCenterX - pinIcon.getMinimumWidth() / 2f, pinCenterY - pinIcon.getMinimumHeight() / 2f, paint); if (iconAlpha != 1f) { paint.setAlpha(255); } @@ -791,7 +794,7 @@ protected void drawContent (MessageView view, Canvas c, final int startX, int st c.drawCircle(mapCenterX, pinCenterY, pinRadius, Paints.fillingPaint(ColorUtils.alphaColor(.75f, 0xffffffff))); } - c.restore(); + Views.restore(c, restoreToCount); if (!useBubbles && !useFullWidth) { diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageMedia.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageMedia.java index 918af70b7b..234862521e 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageMedia.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageMedia.java @@ -178,7 +178,7 @@ public MediaViewThumbLocation getMediaThumbLocation (long messageId, View view, @Override protected boolean preferFullWidth () { - return UI.isPortrait() && !UI.isTablet() && isChannel() && !isEventLog() && msg.content.getConstructor() != TdApi.MessageAnimation.CONSTRUCTOR && (!mosaicWrapper.isSingular() || mosaicWrapper.getAspectRatio() >= (mosaicWrapper.getSingularItem().isGif() ? MIN_RATIO_GIF : MIN_RATIO)); + return UI.isPortrait() && !UI.isTablet() && isChannel() && !isEventLog() && !Td.isAnimation(msg.content) && (!mosaicWrapper.isSingular() || mosaicWrapper.getAspectRatio() >= (mosaicWrapper.getSingularItem().isGif() ? MIN_RATIO_GIF : MIN_RATIO)); } @Override @@ -210,7 +210,7 @@ private boolean checkCommonCaption (boolean force) { synchronized (this) { ArrayList combinedMessages = getCombinedMessagesUnsafely(); if (combinedMessages != null && !combinedMessages.isEmpty()) { - TdApi.Message captionMessage = TD.getAlbumCaptionMessage(tdlib, combinedMessages); + TdApi.Message captionMessage = ContentPreview.getAlbumCaptionMessage(tdlib, combinedMessages); if (captionMessage != null) { caption = tdlib.getPendingFormattedText(captionMessage.chatId, captionMessage.id); if (caption != null) { @@ -278,7 +278,7 @@ private boolean setCaption (TdApi.FormattedText caption, long messageId, boolean this.wrapper.performDestroy(); } if (!Td.isEmpty(caption)) { - TdApi.FormattedText fText = translatedText != null ? translatedText: caption; + TdApi.FormattedText fText = translatedText != null ? translatedText : caption; this.wrapper = new TextWrapper(fText.text, getTextStyleProvider(), getTextColorSet()) .setEntities(TextEntity.valueOf(tdlib, fText, openParameters()), (wrapper, text, specificMedia) -> { if (this.wrapper == wrapper) { @@ -344,7 +344,7 @@ protected boolean isSupportedMessageContent (TdApi.Message message, TdApi.Messag @Override protected boolean onMessageContentChanged (TdApi.Message message, TdApi.MessageContent oldContent, TdApi.MessageContent newContent, boolean isBottomMessage) { - if (message.viaBotUserId != 0 && oldContent.getConstructor() == TdApi.MessagePhoto.CONSTRUCTOR) { + if (message.viaBotUserId != 0 && Td.isPhoto(oldContent)) { updateMessageContent(message, newContent, isBottomMessage); return true; } @@ -625,7 +625,7 @@ protected void onHotTimerStarted (boolean byEvent) { @Override protected boolean needHotTimer () { - return true; + return !isViewOnce(); } @Override @@ -795,7 +795,7 @@ public boolean onTouchEvent (MessageView view, MotionEvent e) { return true; } - if (isHot() && mosaicWrapper.getSingularItem().getFileProgress().isLoaded()) { + if (isHot() && !isViewOnce() && mosaicWrapper.getSingularItem().getFileProgress().isLoaded()) { switch (e.getAction()) { case MotionEvent.ACTION_DOWN: { cancelScheduledHotOpening(view, false); @@ -850,7 +850,7 @@ public boolean allowLongPress (float x, float y) { int cellRight = cellLeft + mosaicWrapper.getWidth(); int cellBottom = cellTop + mosaicWrapper.getHeight(); - return !isHot() || x < cellLeft || x > cellRight || y < cellTop || y > cellBottom; + return !(isHot() && !isViewOnce()) || x < cellLeft || x > cellRight || y < cellTop || y > cellBottom; } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessagePoll.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessagePoll.java index 19cae075d2..3ecf986e78 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessagePoll.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessagePoll.java @@ -88,9 +88,9 @@ private static class PollState { private final float resultsVisibility, timerVisibility, hintVisibility; private final boolean resultsVisible, timerVisible, hintVisible; - public PollState (Tdlib tdlib, TdApi.Poll poll) { + public PollState (Tdlib tdlib, TdApi.Poll poll, boolean resultsVisible) { this.poll = poll; - this.resultsVisible = TD.needShowResults(poll); + this.resultsVisible = resultsVisible; this.resultsVisibility = resultsVisible ? 1f : 0f; this.timerVisible = !poll.isClosed && poll.openPeriod != 0; this.timerVisibility = timerVisible ? 1f : 0f; @@ -128,7 +128,7 @@ public PollState (Tdlib tdlib, PollState fromState, PollState toState, float fac fromTo(fromState.options[i].progress, toState.options[i].progress, factor) ); } - this.poll = new TdApi.Poll(toState.poll.id, toState.poll.question, options, toState.poll.totalVoterCount, toState.poll.recentVoterUserIds, toState.poll.isAnonymous, toState.poll.type, toState.poll.openPeriod, toState.poll.closeDate, toState.poll.isClosed); + this.poll = new TdApi.Poll(toState.poll.id, toState.poll.question, options, toState.poll.totalVoterCount, toState.poll.recentVoterIds, toState.poll.isAnonymous, toState.poll.type, toState.poll.openPeriod, toState.poll.closeDate, toState.poll.isClosed); } public int size () { @@ -251,21 +251,22 @@ protected void onMessageContainerDestroyed() { private static final float VOTER_OUTLINE = 1f; private static final float VOTER_SPACING = 4f; - private static class UserEntry { - private final long userId; + private static class SenderEntry { + private final TdApi.MessageSender senderId; - public UserEntry (Tdlib tdlib, long userId) { - this.userId = userId; + public SenderEntry (Tdlib tdlib, TdApi.MessageSender senderId) { + this.senderId = senderId; } @Override public boolean equals (@Nullable Object obj) { - return obj instanceof UserEntry && ((UserEntry) obj).userId == this.userId; + return obj instanceof SenderEntry && Td.equalsTo(((SenderEntry) obj).senderId, this.senderId); } @Override public int hashCode() { - return (int) (userId ^ (userId >>> 32)); + long senderId = Td.getSenderId(this.senderId); + return (int) (senderId ^ (senderId >>> 32)); } public void draw (Canvas c, TGMessage context, ComplexReceiver complexReceiver, float cx, float cy, final float alpha) { @@ -275,7 +276,7 @@ public void draw (Canvas c, TGMessage context, ComplexReceiver complexReceiver, int replaceColor = context.getContentReplaceColor(); int radius = Screen.dp(VOTER_RADIUS); - AvatarReceiver receiver = complexReceiver.getAvatarReceiver(userId); + AvatarReceiver receiver = complexReceiver.getAvatarReceiver(Td.getSenderId(senderId)); if (alpha != 1f) receiver.setPaintAlpha(receiver.getPaintAlpha() * alpha); receiver.setBounds((int) (cx - radius), (int) (cy - radius), (int) (cx + radius), (int) (cy + radius)); @@ -304,7 +305,7 @@ public void draw (Canvas c, TGMessage context, ComplexReceiver complexReceiver, } } - private ListAnimator recentVoters; + private ListAnimator recentVoters; // Impl @@ -344,7 +345,7 @@ public TGMessagePoll (MessagesManager manager, TdApi.Message msg, TdApi.Poll pol }*/ this.clickHelper = new ClickHelper(this); - this.state = new PollState(tdlib, poll); + this.state = new PollState(tdlib, poll, needShowResults(poll)); if (!poll.isAnonymous || isMultiChoicePoll()) { this.isButtonActive = new BoolAnimator(ANIMATOR_BUTTON, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 120l); this.button = new ReplaceAnimator<>(animator -> this.invalidate()); @@ -355,7 +356,7 @@ public TGMessagePoll (MessagesManager manager, TdApi.Message msg, TdApi.Poll pol } private void setQuestion (String question) { - String questionToSet = (translatedTexts != null ? StringUtils.trim(translatedTexts[0]): question); + String questionToSet = (translatedTexts != null ? StringUtils.trim(translatedTexts[0]) : question); if (this.questionText == null || !StringUtils.equalsOrBothEmpty(this.questionText.getText(), questionToSet)) { this.questionText = new TextWrapper(questionToSet, getBiggerTextStyleProvider(), getTextColorSet()) .setEntities(new TextEntity[] {TextEntity.valueOf(tdlib, questionToSet, new TdApi.TextEntity(0, questionToSet.length(), new TdApi.TextEntityTypeBold()), null)}, null) @@ -367,7 +368,7 @@ private void setOptions (TdApi.PollOption[] options) { prepareOptions(options); int optionId = 0; for (TdApi.PollOption option : options) { - String optionToSet = (translatedTexts != null ? StringUtils.trim(translatedTexts[optionId + 1]): option.text); + String optionToSet = (translatedTexts != null ? StringUtils.trim(translatedTexts[optionId + 1]) : option.text); if (this.options[optionId].text == null || !StringUtils.equalsOrBothEmpty(this.options[optionId].text.getText(), optionToSet)) { this.options[optionId].text = new TextWrapper(optionToSet, getTextStyleProvider(), getTextColorSet()) .setViewProvider(currentViews); @@ -401,7 +402,7 @@ private void prepareProgress (TdApi.PollOption[] options) { @Override protected void buildContent (int maxWidth) { if (questionText == null) { - setRecentVoters(state.poll.recentVoterUserIds, false); + setRecentVoters(state.poll.recentVoterIds, false); setQuestion(state.poll.question); setOptions(state.poll.options); prepareProgress(state.poll.options); @@ -437,8 +438,8 @@ protected int getContentHeight () { @Override public boolean filterKey (int receiverType, Receiver receiver, long key) { if (recentVoters != null) { - for (ListAnimator.Entry recentVoter : recentVoters) { - if (recentVoter.item.userId == key) { + for (ListAnimator.Entry recentVoter : recentVoters) { + if (Td.getSenderId(recentVoter.item.senderId) == key) { return true; } } @@ -454,9 +455,10 @@ public boolean needComplexReceiver () { @Override public void requestMediaContent (ComplexReceiver complexReceiver, boolean invalidate, int invalidateArg) { if (recentVoters != null) { - for (ListAnimator.Entry entry : recentVoters) { - AvatarReceiver receiver = complexReceiver.getAvatarReceiver(entry.item.userId); - receiver.requestUser(tdlib, entry.item.userId, AvatarReceiver.Options.NONE); + for (ListAnimator.Entry entry : recentVoters) { + long senderId = Td.getSenderId(entry.item.senderId); + AvatarReceiver receiver = complexReceiver.getAvatarReceiver(senderId); + receiver.requestMessageSender(tdlib, entry.item.senderId, AvatarReceiver.Options.NONE); } } complexReceiver.clearReceivers(this); @@ -477,7 +479,7 @@ protected void drawContent (MessageView view, Canvas c, int startX, int startY, int cx = startX + pollStatusText.getWidth() + Screen.dp(VOTER_RADIUS) + Screen.dp(6f); int spacing = Screen.dp(VOTER_RADIUS) * 2 - Screen.dp(VOTER_SPACING); for (int index = recentVoters.size() - 1; index >= 0; index--) { - ListAnimator.Entry item = recentVoters.getEntry(index); + ListAnimator.Entry item = recentVoters.getEntry(index); int x = cx + item.getIndex() * spacing; if (x + Screen.dp(VOTER_RADIUS) + Screen.dp(2f) <= startX + maxWidth) { item.item.draw(c, this, receiver, cx + item.getPosition() * spacing, startY, item.getVisibility()); @@ -713,13 +715,17 @@ protected void drawContent (MessageView view, Canvas c, final int startX, int st cy = (int) (cy + (progressCy - cy) * moveFactor); scale = scale + (1f - scale) * moveFactor; } - if (scale != 1f) { - c.save(); + final boolean needScale = scale != 1f; + final int restoreToCount; + if (needScale) { + restoreToCount = Views.save(c); c.scale(scale, scale, cx, lineY); + } else { + restoreToCount = -1; } SimplestCheckBox.draw(c, cx, cy, selectionFactor, null, option.checkBox, lineColor, contentColor, isQuiz && optionId != correctOptionId, squareFactor); - if (scale != 1f) { - c.restore(); + if (needScale) { + Views.restore(c, restoreToCount); } } @@ -729,10 +735,10 @@ protected void drawContent (MessageView view, Canvas c, final int startX, int st if (highlightOptionId == HIGHLIGHT_BUTTON) { if (useBubble() && !useForward()) { - c.save(); + final int restoreToCount = Views.save(c); c.clipRect(getActualLeftContentEdge(), startY, getActualRightContentEdge(), getBottomContentEdge()); c.drawPath(getBubblePath(), Paints.fillingPaint(Theme.getColor(getPressColorId()))); - c.restore(); + Views.restore(c, restoreToCount); } else { int rightX = startX + maxWidth + (useBubbles() ? getBubblePaddingRight() : 0); c.drawRect(startX - (useBubbles() ? getBubbleContentPadding() : 0), startY, rightX, startY + Screen.dp(46f), Paints.fillingPaint(Theme.getColor(getPressColorId()))); @@ -881,7 +887,27 @@ private ProgressComponent getResultProgressView (int optionId) { } private boolean canVote (boolean checkSelected) { - return TD.canVote(getPoll()) && (!checkSelected || (!isMultiChoicePoll() || !(!hasAnswer() && TD.hasSelectedOption(getPoll())))); + return canVote(getPoll()) && (!checkSelected || (!isMultiChoicePoll() || !(!hasAnswer() && TD.hasSelectedOption(getPoll())))); + } + + private boolean canVote (TdApi.Poll poll) { + return !needShowResults(poll); + } + + private boolean needShowResults (TdApi.Poll poll) { + if (poll.isClosed) + return true; + boolean haveVoters = false; + for (TdApi.PollOption option : poll.options) { + if (option.isChosen) + return true; + if (option.voterCount > 0) { + haveVoters = true; + } + } + // show results for anonymous admin + // FIXME TDLib/server: poll information never returned to anonymous admin + return haveVoters && tdlib.isAnonymousAdminNonCreator(msg.chatId); } @Override @@ -892,11 +918,11 @@ protected boolean onLocaleChange () { return true; } - private void setRecentVoters (long[] recentVoterUserIds, boolean animated) { - if (recentVoterUserIds != null && recentVoterUserIds.length > 0) { - List entries = new ArrayList<>(recentVoterUserIds.length); - for (long userId : recentVoterUserIds) { - entries.add(new UserEntry(tdlib, userId)); + private void setRecentVoters (TdApi.MessageSender[] recentVoterIds, boolean animated) { + if (recentVoterIds != null && recentVoterIds.length > 0) { + List entries = new ArrayList<>(recentVoterIds.length); + for (TdApi.MessageSender senderId : recentVoterIds) { + entries.add(new SenderEntry(tdlib, senderId)); } if (this.recentVoters == null) this.recentVoters = new ListAnimator<>(currentViews); @@ -924,8 +950,8 @@ private void applyPoll (TdApi.Poll updatedPoll, boolean force) { boolean animated = !changed && needAnimateChanges(); if (animated) { resetPollAnimation(true); - futureState = new PollState(tdlib, updatedPoll); - setRecentVoters(updatedPoll.recentVoterUserIds, true); + futureState = new PollState(tdlib, updatedPoll, needShowResults(updatedPoll)); + setRecentVoters(updatedPoll.recentVoterIds, true); setButton(true); if (recentVoters != null) { invalidateContentReceiver(); @@ -990,8 +1016,8 @@ private void applyPoll (TdApi.Poll updatedPoll, boolean force) { animator.animateTo(1f); } else { resetPollAnimation(false); - this.state = new PollState(tdlib, updatedPoll); - setRecentVoters(updatedPoll.recentVoterUserIds, false); + this.state = new PollState(tdlib, updatedPoll, needShowResults(updatedPoll)); + setRecentVoters(updatedPoll.recentVoterIds, false); if (recentVoters != null) { invalidateContentReceiver(); } @@ -1010,7 +1036,7 @@ private void applyPoll (TdApi.Poll updatedPoll, boolean force) { @Override protected boolean onMessageContentChanged (TdApi.Message message, TdApi.MessageContent oldContent, TdApi.MessageContent newContent, boolean isBottomMessage) { - if (newContent.getConstructor() == TdApi.MessagePoll.CONSTRUCTOR) { + if (Td.isPoll(newContent)) { TdApi.Poll updatedPoll = ((TdApi.MessagePoll) newContent).poll; applyPoll(updatedPoll, false); return true; @@ -1095,7 +1121,7 @@ private void setTexts () { if (futureState == null) { setTotalVoterCount(state.poll); setPollStatus(state.poll.isClosed ? POLL_STATUS_CLOSED : POLL_STATUS_ANONYMOUS); - setPercentages(TD.needShowResults(state.poll), state.poll.options); + setPercentages(needShowResults(state.poll), state.poll.options); int correctOptionId = state.poll.type.getConstructor() == TdApi.PollTypeQuiz.CONSTRUCTOR ? ((TdApi.PollTypeQuiz) state.poll.type).correctOptionId : -1; for (int optionId = 0; optionId < state.poll.options.length; optionId++) { options[optionId].selectionFactor = optionId == correctOptionId || state.poll.options[optionId].isChosen ? 1f : 0f; @@ -1407,7 +1433,7 @@ private void showExplanation (View view) { } explanationPopup = buildContentHint(view, (targetView, outRect) -> { outRect.set(0, 0, questionText.getWidth(), questionText.getHeight()); - }).icon(R.drawable.baseline_info_24).needBlink(true).chatTextSize(-2f).interceptTouchEvents(true).handleBackPress(true).show(tdlib, formattedText).addListener(this); + }, true).icon(R.drawable.baseline_info_24).needBlink(true).chatTextSize(-2f).interceptTouchEvents(true).handleBackPress(true).show(tdlib, formattedText).addListener(this); } } @@ -1444,11 +1470,18 @@ public void onClickAt (View view, float x, float y) { } int[] selectedOptionIds = selectedOptions.get(); int[] currentOptionIds = currentOptions.get(); - if (isAnonymous() || messagesController().callNonAnonymousProtection(msg.id + R.id.btn_vote, this, makeVoteButtonLocationProvider())) { + if (!Config.PROTECT_ANONYMOUS_VOTING || isAnonymous() || messagesController().callNonAnonymousProtection(msg.id + R.id.btn_vote, this, makeVoteButtonLocationProvider(true))) { + Tdlib.ResultHandler handler = (ok, error) -> { + if (error != null) { + runOnUiThreadOptional(() -> { + showContentHint(view, makeVoteButtonLocationProvider(false), TD.toFormattedText(TD.toErrorString(error), false)); + }); + } + }; if (Arrays.equals(selectedOptionIds, currentOptionIds)) { - tdlib.client().send(new TdApi.SetPollAnswer(msg.chatId, msg.id, null), tdlib.okHandler()); + tdlib.send(new TdApi.SetPollAnswer(msg.chatId, msg.id, null), handler); } else { - tdlib.client().send(new TdApi.SetPollAnswer(msg.chatId, msg.id, selectedOptionIds), tdlib.okHandler()); + tdlib.send(new TdApi.SetPollAnswer(msg.chatId, msg.id, selectedOptionIds), handler); } } } else if (itemId == R.id.btn_viewResults) { @@ -1499,7 +1532,7 @@ public void onClickAt (View view, float x, float y) { } else if (isMultiChoicePoll()) { selectUnselect(clickOptionId); } else { - chooseOption(clickOptionId); + chooseOption(view, clickOptionId); } clickOptionId = HIGHLIGHT_NONE; } @@ -1509,12 +1542,19 @@ private int getOptionHeight (TextWrapper text) { return Math.max(Screen.dp(46f), Math.max(Screen.dp(8f), (Screen.dp(46f) / 2 - text.getLineHeight() / 2)) + text.getHeight() + Screen.dp(12f)) + Screen.separatorSize(); } - private void chooseOption (final int optionId) { - if (isAnonymous() || messagesController().callNonAnonymousProtection(msg.id + optionId, this, makeButtonLocationProvider(optionId))) { + private void chooseOption (final View view, final int optionId) { + if (!Config.PROTECT_ANONYMOUS_VOTING || isAnonymous() || messagesController().callNonAnonymousProtection(msg.id + optionId, this, makeButtonLocationProvider(optionId, true))) { + Tdlib.ResultHandler handler = (ok, error) -> { + if (error != null) { + runOnUiThreadOptional(() -> { + showContentHint(view, makeButtonLocationProvider(optionId, false), TD.toFormattedText(TD.toErrorString(error), false)); + }); + } + }; if (getPoll().options[optionId].isBeingChosen) { - tdlib.client().send(new TdApi.SetPollAnswer(msg.chatId, msg.id, null), tdlib.okHandler()); + tdlib.send(new TdApi.SetPollAnswer(msg.chatId, msg.id, null), handler); } else { - tdlib.client().send(new TdApi.SetPollAnswer(msg.chatId, msg.id, new int[] {optionId}), tdlib.okHandler()); + tdlib.send(new TdApi.SetPollAnswer(msg.chatId, msg.id, new int[] {optionId}), handler); } } } @@ -1539,7 +1579,7 @@ private void setHighlightOption (int optionId, View view, float x, float y) { } } - private TooltipOverlayView.LocationProvider makeVoteButtonLocationProvider () { + private TooltipOverlayView.LocationProvider makeVoteButtonLocationProvider (boolean needOffset) { return (targetView, outRect) -> { int startY = questionText.getHeight() + Screen.dp(28f); for (OptionEntry option : options) { @@ -1547,11 +1587,13 @@ private TooltipOverlayView.LocationProvider makeVoteButtonLocationProvider () { startY += optionHeight; } outRect.set(0, startY, getContentMaxWidth(), startY + Screen.dp(50)); - outRect.offset(getContentX(), getContentY()); + if (needOffset) { + outRect.offset(getContentX(), getContentY()); + } }; } - private TooltipOverlayView.LocationProvider makeButtonLocationProvider (int selectedOptionId) { + private TooltipOverlayView.LocationProvider makeButtonLocationProvider (int selectedOptionId, boolean needOffset) { return (targetView, outRect) -> { int startY = questionText.getHeight() + Screen.dp(5f); int optionId = 0; @@ -1560,7 +1602,9 @@ private TooltipOverlayView.LocationProvider makeButtonLocationProvider (int sele if (selectedOptionId == optionId) { startY += Screen.dp(15f + 12f); outRect.set(Screen.dp(0f), startY, Screen.dp(24f), startY + option.text.getLineHeight()); - outRect.offset(getContentX(), getContentY()); + if (needOffset) { + outRect.offset(getContentX(), getContentY()); + } return; } startY += optionHeight; @@ -1575,6 +1619,9 @@ private TooltipOverlayView.LocationProvider makeButtonLocationProvider (int sele @Nullable @Override public TdApi.FormattedText getTextToTranslateImpl () { + if (state == null || state.poll == null) { + return null; + } StringBuilder pollText = new StringBuilder(state.poll.question.replaceAll("•", " ")); for (TdApi.PollOption option : state.poll.options) { pollText.append("\n\n• ").append(option.text.replaceAll("•", " ")); diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageService.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageService.java index a021594e2e..675c9355a7 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageService.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageService.java @@ -23,6 +23,7 @@ import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.chat.MessagesManager; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibSender; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; @@ -89,6 +90,46 @@ public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.Messa // TODO design for giftedPremium.sticker } + public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.MessagePremiumGiftCode premiumGiftCode) { + super(context, msg); + setTextCreator(() -> { + if (msg.isOutgoing) { + return getPlural( + R.string.YouGiftedPremiumCode, + premiumGiftCode.monthCount + ); + } else { + return getPlural( + R.string.GiftedPremiumCode, + premiumGiftCode.monthCount, + new SenderArgument(new TdlibSender(tdlib, msg.chatId, premiumGiftCode.creatorId), isUserChat()) + ); + } + }); + // TODO design for premiumGiftCode.sticker + // TODO show details of the gift code + } + + public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.MessagePremiumGiveawayCreated giveawayCreated) { + super(context, msg); + setTextCreator(() -> { + return getText( + R.string.BoostingGiveawayJustStarted, + new SenderArgument(sender) + ); + }); + } + + public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.MessagePremiumGiveawayCompleted giveawayCompleted) { + super(context, msg); + setTextCreator(() -> { + return getPlural( + R.string.BoostingGiveawayServiceWinnersSelected, + giveawayCompleted.winnerCount + ); + }); + } + public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.MessageChatSetTheme setTheme) { super(context, msg); setTextCreator(() -> { @@ -263,6 +304,9 @@ public FormattedText createText () { case TdApi.MessageContact.CONSTRUCTOR: staticResId = R.string.ActionPinnedContact; break; + case TdApi.MessageStory.CONSTRUCTOR: + staticResId = R.string.ActionPinnedStory; + break; case TdApi.MessageDice.CONSTRUCTOR: // TODO? // unreachable case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: @@ -278,12 +322,18 @@ public FormattedText createText () { case TdApi.MessageChatJoinByLink.CONSTRUCTOR: case TdApi.MessageChatJoinByRequest.CONSTRUCTOR: case TdApi.MessageChatSetTheme.CONSTRUCTOR: + case TdApi.MessageChatSetBackground.CONSTRUCTOR: case TdApi.MessageChatSetMessageAutoDeleteTime.CONSTRUCTOR: case TdApi.MessageChatUpgradeFrom.CONSTRUCTOR: case TdApi.MessageChatUpgradeTo.CONSTRUCTOR: case TdApi.MessageContactRegistered.CONSTRUCTOR: case TdApi.MessageGameScore.CONSTRUCTOR: case TdApi.MessageGiftedPremium.CONSTRUCTOR: + case TdApi.MessagePremiumGiftCode.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayCreated.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayCompleted.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayWinners.CONSTRUCTOR: + case TdApi.MessagePremiumGiveaway.CONSTRUCTOR: case TdApi.MessageInviteVideoChatParticipants.CONSTRUCTOR: case TdApi.MessagePassportDataReceived.CONSTRUCTOR: case TdApi.MessagePassportDataSent.CONSTRUCTOR: @@ -299,11 +349,19 @@ public FormattedText createText () { case TdApi.MessageVideoChatStarted.CONSTRUCTOR: case TdApi.MessageWebAppDataReceived.CONSTRUCTOR: case TdApi.MessageWebAppDataSent.CONSTRUCTOR: - case TdApi.MessageWebsiteConnected.CONSTRUCTOR: + case TdApi.MessageForumTopicCreated.CONSTRUCTOR: + case TdApi.MessageForumTopicEdited.CONSTRUCTOR: + case TdApi.MessageForumTopicIsClosedToggled.CONSTRUCTOR: + case TdApi.MessageForumTopicIsHiddenToggled.CONSTRUCTOR: + case TdApi.MessageSuggestProfilePhoto.CONSTRUCTOR: + case TdApi.MessageUsersShared.CONSTRUCTOR: + case TdApi.MessageChatShared.CONSTRUCTOR: + case TdApi.MessageBotWriteAccessAllowed.CONSTRUCTOR: staticResId = R.string.ActionPinnedNoText; break; default: - throw new UnsupportedOperationException(message.content.toString()); + Td.assertMessageContent_d40af239(); + throw Td.unsupported(message.content); } String format = Lang.getString(staticResId); int startIndex = format.indexOf("**"); @@ -598,14 +656,46 @@ public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.Messa }); } - public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.MessageWebsiteConnected websiteConnected) { + public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.MessageBotWriteAccessAllowed botWriteAccessAllowed) { super(context, msg); - setTextCreator(() -> - getText( - R.string.BotWebsiteAllowed, - new BoldArgument(websiteConnected.domainName) - ) - ); + switch (botWriteAccessAllowed.reason.getConstructor()) { + case TdApi.BotWriteAccessAllowReasonConnectedWebsite.CONSTRUCTOR: { + TdApi.BotWriteAccessAllowReasonConnectedWebsite connectedWebsite = (TdApi.BotWriteAccessAllowReasonConnectedWebsite) botWriteAccessAllowed.reason; + setTextCreator(() -> + getText( + R.string.BotWebsiteAllowed, + new BoldArgument(connectedWebsite.domainName) + ) + ); + break; + } + case TdApi.BotWriteAccessAllowReasonAddedToAttachmentMenu.CONSTRUCTOR: { + setTextCreator(() -> + getText(R.string.BotAttachAllowed) + ); + break; + } + case TdApi.BotWriteAccessAllowReasonLaunchedWebApp.CONSTRUCTOR: { + TdApi.BotWriteAccessAllowReasonLaunchedWebApp launchedWebApp = (TdApi.BotWriteAccessAllowReasonLaunchedWebApp) botWriteAccessAllowed.reason; + setTextCreator(() -> + getText( + R.string.BotAppAllowed, + new BoldArgument(launchedWebApp.webApp.title) + ) + ); + break; + } + case TdApi.BotWriteAccessAllowReasonAcceptedRequest.CONSTRUCTOR: { + setTextCreator(() -> + getText(R.string.BotWebappAllowed) + ); + break; + } + default: { + Td.assertBotWriteAccessAllowReason_d7597302(); + throw Td.unsupported(botWriteAccessAllowed.reason); + } + } } public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.MessageChatSetMessageAutoDeleteTime setMessageAutoDeleteTime) { @@ -617,6 +707,8 @@ public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.Messa return getText( isUserChat ? R.string.YouDisabledTimer : + msg.isChannelPost ? + R.string.YouDisabledAutoDeletePosts : R.string.YouDisabledAutoDelete ); } else { @@ -690,7 +782,7 @@ public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.Messa }); if (gameScore.gameMessageId != 0) { setDisplayMessage(msg.chatId, gameScore.gameMessageId, (message) -> { - if (message.content.getConstructor() != TdApi.MessageGame.CONSTRUCTOR) { + if (!Td.isGame(message.content)) { return false; } setTextCreator(() -> { @@ -731,7 +823,7 @@ public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.Messa paymentSuccessful.invoiceChatId, paymentSuccessful.invoiceMessageId, message -> { - if (message.content.getConstructor() != TdApi.MessageInvoice.CONSTRUCTOR) { + if (!Td.isInvoice(message.content)) { return false; } setTextCreator(() -> @@ -919,8 +1011,8 @@ public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.ChatE public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.ChatEventMessageEdited messageEdited) { super(context, msg); setTextCreator(() -> { - if (messageEdited.newMessage.content.getConstructor() == TdApi.MessageText.CONSTRUCTOR || - messageEdited.newMessage.content.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { + if (Td.isText(messageEdited.newMessage.content) || + Td.isAnimatedEmoji(messageEdited.newMessage.content)) { return getText(R.string.EventLogEditedMessages, new SenderArgument(sender)); } else if (Td.isEmpty(Td.textOrCaption(messageEdited.newMessage.content))) { return getText(R.string.EventLogRemovedCaption, new SenderArgument(sender)); @@ -934,7 +1026,7 @@ public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.ChatE super(context, msg); setTextCreator(() -> { final boolean isQuiz = - pollStopped.message.content.getConstructor() == TdApi.MessagePoll.CONSTRUCTOR && + Td.isPoll(pollStopped.message.content) && ((TdApi.MessagePoll) pollStopped.message.content).poll.type.getConstructor() == TdApi.PollTypeQuiz.CONSTRUCTOR; return getText( isQuiz ? @@ -1483,6 +1575,190 @@ public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.ChatE } } + private void setBackgroundTextCreator (int oldAccentColorId, int newAccentColorId, + long oldBackgroundCustomEmojiId, long newBackgroundCustomEmojiId, + boolean isProfile) { + setTextCreator(() -> { + if (oldBackgroundCustomEmojiId == newBackgroundCustomEmojiId) { + // Only accent color changed + if (isProfile && (oldAccentColorId == -1 || newAccentColorId == -1)) { + boolean isUnset = newAccentColorId == -1; + int accentColorId = isUnset ? + oldAccentColorId : + newAccentColorId; + if (msg.isOutgoing) { + return getText( + isUnset ? R.string.EventLogProfileColorUnsetYou : R.string.EventLogProfileColorSetYou, + new AccentColorArgument(tdlib.accentColor(accentColorId)) + ); + } else { + return getText( + isUnset ? R.string.EventLogProfileColorUnset : R.string.EventLogProfileColorSet, + new SenderArgument(sender), + new AccentColorArgument(tdlib.accentColor(accentColorId)) + ); + } + } else { + if (msg.isOutgoing) { + return getText( + isProfile ? R.string.EventLogProfileColorChangedYou : R.string.EventLogAccentColorChangedYou, + new AccentColorArgument(tdlib.accentColor(oldAccentColorId)), + new AccentColorArgument(tdlib.accentColor(newAccentColorId)) + ); + } else { + return getText( + isProfile ? R.string.EventLogProfileColorChanged : R.string.EventLogAccentColorChanged, + new SenderArgument(sender), + new AccentColorArgument(tdlib.accentColor(oldAccentColorId)), + new AccentColorArgument(tdlib.accentColor(newAccentColorId)) + ); + } + } + } else if (oldAccentColorId == newAccentColorId) { + // Only background changed + TdlibAccentColor repaintAccentColor = newAccentColorId != -1 ? tdlib.accentColor(newAccentColorId) : null; + if (newBackgroundCustomEmojiId == 0 || oldBackgroundCustomEmojiId == 0) { + boolean isUnset = newBackgroundCustomEmojiId == 0; + long backgroundCustomEmojiId = isUnset ? + oldBackgroundCustomEmojiId : + newBackgroundCustomEmojiId; + if (msg.isOutgoing) { + return getText( + isProfile ? + (isUnset ? R.string.EventLogProfileEmojiUnsetYou : R.string.EventLogProfileEmojiSetYou) : + (isUnset ? R.string.EventLogEmojiUnsetYou : R.string.EventLogEmojiSetYou), + new CustomEmojiArgument(tdlib, backgroundCustomEmojiId, repaintAccentColor) + ); + } else { + return getText( + isProfile ? + (isUnset ? R.string.EventLogProfileEmojiUnset : R.string.EventLogProfileEmojiSet) : + (isUnset ? R.string.EventLogEmojiUnset : R.string.EventLogEmojiSet), + new SenderArgument(sender), + new CustomEmojiArgument(tdlib, backgroundCustomEmojiId, repaintAccentColor) + ); + } + } else { + if (msg.isOutgoing) { + return getText( + isProfile ? R.string.EventLogProfileEmojiChangedYou : R.string.EventLogEmojiChangedYou, + new CustomEmojiArgument(tdlib, oldBackgroundCustomEmojiId, repaintAccentColor), + new CustomEmojiArgument(tdlib, newBackgroundCustomEmojiId, repaintAccentColor) + ); + } else { + return getText( + isProfile ? R.string.EventLogProfileEmojiChanged : R.string.EventLogEmojiChanged, + new SenderArgument(sender), + new CustomEmojiArgument(tdlib, oldBackgroundCustomEmojiId, repaintAccentColor), + new CustomEmojiArgument(tdlib, newBackgroundCustomEmojiId, repaintAccentColor) + ); + } + } + } else { + // Both color and emoji changed + + boolean hadIconOrColor = oldAccentColorId != -1 || oldBackgroundCustomEmojiId != 0; + boolean hasIconOrColor = newAccentColorId != -1 || newBackgroundCustomEmojiId != 0; + + if (!hadIconOrColor || !hasIconOrColor) { + boolean isUnset = !hasIconOrColor; + int accentColorId = isUnset ? + oldAccentColorId : + newAccentColorId; + long backgroundCustomEmojiId = isUnset ? + oldBackgroundCustomEmojiId : + newBackgroundCustomEmojiId; + if (msg.isOutgoing) { + return getText( + isUnset ? R.string.EventLogProfileColorIconUnsetYou : R.string.EventLogProfileColorIconSetYou, + new AccentColorArgument(accentColorId != -1 ? tdlib.accentColor(accentColorId) : null, backgroundCustomEmojiId) + ); + } else { + return getText( + isUnset ? R.string.EventLogProfileColorIconUnset : R.string.EventLogProfileColorIconSet, + new SenderArgument(sender), + new AccentColorArgument(accentColorId != -1 ? tdlib.accentColor(accentColorId) : null, backgroundCustomEmojiId) + ); + } + } else { + if (msg.isOutgoing) { + return getText( + R.string.EventLogProfileColorIconChangedYou, + new AccentColorArgument(oldAccentColorId != -1 ? tdlib.accentColor(oldAccentColorId) : null, oldBackgroundCustomEmojiId), + new AccentColorArgument(newAccentColorId != -1 ? tdlib.accentColor(newAccentColorId) : null, newBackgroundCustomEmojiId) + ); + } else { + return getText( + R.string.EventLogProfileColorIconChanged, + new SenderArgument(sender), + new AccentColorArgument(oldAccentColorId != -1 ? tdlib.accentColor(oldAccentColorId) : null, oldBackgroundCustomEmojiId), + new AccentColorArgument(newAccentColorId != -1 ? tdlib.accentColor(newAccentColorId) : null, newBackgroundCustomEmojiId) + ); + } + } + } + }); + } + + public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.ChatEventAccentColorChanged accentColorChanged) { + super(context, msg); + setBackgroundTextCreator( + accentColorChanged.oldAccentColorId, accentColorChanged.newAccentColorId, + accentColorChanged.oldBackgroundCustomEmojiId, accentColorChanged.newBackgroundCustomEmojiId, + false + ); + } + + public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.ChatEventProfileAccentColorChanged profileAccentColorChanged) { + super(context, msg); + setBackgroundTextCreator( + profileAccentColorChanged.oldProfileAccentColorId, profileAccentColorChanged.newProfileAccentColorId, + profileAccentColorChanged.oldProfileBackgroundCustomEmojiId, profileAccentColorChanged.newProfileBackgroundCustomEmojiId, + true + ); + } + + public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.ChatEventEmojiStatusChanged emojiStatusChanged) { + super(context, msg); + setTextCreator(() -> { + if (emojiStatusChanged.oldEmojiStatus == null || emojiStatusChanged.newEmojiStatus == null) { + boolean isUnset = emojiStatusChanged.newEmojiStatus == null; + long backgroundCustomEmojiId = isUnset ? + (emojiStatusChanged.oldEmojiStatus != null ? emojiStatusChanged.oldEmojiStatus.customEmojiId : 0) : + emojiStatusChanged.newEmojiStatus.customEmojiId; + if (msg.isOutgoing) { + return getText( + (isUnset ? R.string.EventLogEmojiStatusUnsetYou : R.string.EventLogEmojiStatusSetYou), + new CustomEmojiArgument(tdlib, backgroundCustomEmojiId, null) + ); + } else { + return getText( + (isUnset ? R.string.EventLogEmojiStatusUnset : R.string.EventLogEmojiStatusSet), + new SenderArgument(sender), + new CustomEmojiArgument(tdlib, backgroundCustomEmojiId, null) + ); + } + } else { + long oldBackgroundCustomEmojiId = emojiStatusChanged.oldEmojiStatus.customEmojiId; + long newBackgroundCustomEmojiId = emojiStatusChanged.newEmojiStatus.customEmojiId; + if (msg.isOutgoing) { + return getText( + R.string.EventLogEmojiStatusChangedYou, + new CustomEmojiArgument(tdlib, oldBackgroundCustomEmojiId, null), + new CustomEmojiArgument(tdlib, newBackgroundCustomEmojiId, null) + ); + } else { + return getText( + R.string.EventLogEmojiStatusChanged, + new SenderArgument(sender), + new CustomEmojiArgument(tdlib, oldBackgroundCustomEmojiId, null), + new CustomEmojiArgument(tdlib, newBackgroundCustomEmojiId, null) + ); + } + } + }); + } + public TGMessageService (MessagesManager context, TdApi.Message msg, TdApi.ChatEventSlowModeDelayChanged slowModeDelayChanged) { super(context, msg); setTextCreator(() -> { diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageServiceImpl.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageServiceImpl.java index 496c7900c5..704e3cffb2 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageServiceImpl.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageServiceImpl.java @@ -40,6 +40,9 @@ import org.thunderdog.challegram.mediaview.MediaViewThumbLocation; import org.thunderdog.challegram.mediaview.data.MediaItem; import org.thunderdog.challegram.navigation.TooltipOverlayView; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; +import org.thunderdog.challegram.telegram.TdlibEmojiManager; import org.thunderdog.challegram.telegram.TdlibSender; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; @@ -52,6 +55,7 @@ import org.thunderdog.challegram.util.text.TextColorSetOverride; import org.thunderdog.challegram.util.text.TextEntity; import org.thunderdog.challegram.util.text.TextEntityCustom; +import org.thunderdog.challegram.util.text.TextEntityMessage; import java.util.ArrayList; import java.util.Collections; @@ -105,24 +109,58 @@ protected void setDisplayChatPhoto (@Nullable TdApi.ChatPhoto chatPhoto) { } } + private ServiceMessageCreator originalMessageCreator; + private Filter previewCallback; + private TdApi.Message previewMessage; + + @Override + protected boolean handleMessagePreviewChange (long chatId, long messageId, TdApi.MessageContent newContent) { + if (previewMessage != null && previewMessage.chatId == chatId && previewMessage.id == messageId) { + previewMessage.content = newContent; + if (previewCallback.accept(previewMessage)) { + updateServiceMessage(); + return true; + } + } + return false; + } + + @Override + protected boolean handleMessagePreviewDelete (long chatId, long messageId) { + if (originalMessageCreator != null && previewMessage != null && previewMessage.chatId == chatId && previewMessage.id == messageId) { + previewMessage = null; + previewCallback = null; + setTextCreator(originalMessageCreator); + updateServiceMessage(); + return true; + } + return false; + } + public void setDisplayMessage (long chatId, long messageId, Filter callback) { + originalMessageCreator = textCreator; tdlib.client().send(new TdApi.GetMessage(chatId, messageId), result -> { if (result.getConstructor() == TdApi.Message.CONSTRUCTOR) { TdApi.Message message = (TdApi.Message) result; runOnUiThreadOptional(() -> { if (callback.accept(message)) { - // TODO subscribe to further updates - boolean hadTextMedia = hasTextMedia(); - rebuildAndUpdateContent(); - if (hadTextMedia || hasTextMedia()) { - invalidateTextMediaReceiver(); - } + this.previewMessage = message; + this.previewCallback = callback; + updateServiceMessage(); } }); } }); } + private void updateServiceMessage () { + boolean hadTextMedia = hasTextMedia(); + rebuildAndUpdateContent(); + if (hadTextMedia || hasTextMedia()) { + invalidateTextMediaReceiver(); + } + } + private boolean hasTextMedia () { return this.displayText != null && this.displayText.hasMedia(); } @@ -373,6 +411,77 @@ protected interface FormattedArgument { FormattedText buildArgument (); } + protected final class AccentColorArgument implements FormattedArgument { + private final @Nullable TdlibAccentColor accentColor; + private final long customEmojiId; + + public AccentColorArgument (@NonNull TdlibAccentColor accentColor) { + this(accentColor, 0); + } + + public AccentColorArgument (@Nullable TdlibAccentColor accentColor, long customEmojiId) { + this.accentColor = accentColor; + this.customEmojiId = customEmojiId; + } + + @Override + public FormattedText buildArgument () { + final String text = accentColor != null ? accentColor.getTextRepresentation() : " "; + TextEntity entity; + if (customEmojiId != 0) { + entity = new TextEntityMessage( + tdlib, + text, new TdApi.TextEntity(0, text.length(), new TdApi.TextEntityTypeCustomEmoji(customEmojiId)), + openParameters() + ); + } else { + entity = new TextEntityCustom( + controller(), + tdlib, + text, + 0, text.length(), + 0, + openParameters() + ); + } + if (accentColor != null) { + customizeColor(entity, accentColor); + } + return new FormattedText(text, new TextEntity[] {entity}); + } + } + + protected final class CustomEmojiArgument implements FormattedArgument { + private final Tdlib tdlib; + private final long customEmojiId; + private final String text; + + public CustomEmojiArgument (Tdlib tdlib, long customEmojiId, @Nullable TdlibAccentColor repaintAccentColor) { + this.tdlib = tdlib; + this.customEmojiId = customEmojiId; + TdlibEmojiManager.Entry emoji = tdlib.emoji().find(customEmojiId); + if (emoji != null && !emoji.isNotFound()) { + this.text = emoji.value.emoji; + } else { + this.text = ContentPreview.EMOJI_INFO.textRepresentation; + } + } + + @Override + public FormattedText buildArgument () { + String text = this.text; + TextEntityMessage custom = new TextEntityMessage( + tdlib, + text, + 0, text.length(), + new TdApi.TextEntity(0, text.length(), new TdApi.TextEntityTypeCustomEmoji(customEmojiId)), + null, + openParameters() + ); + return new FormattedText(text, new TextEntity[] {custom}); + } + } + protected final class SenderArgument implements FormattedArgument { private final TdlibSender sender; private final boolean onlyFirstName; @@ -422,68 +531,64 @@ public void onClick (@NonNull View widget) { } } }); - int nameColorId = needColoredNames() ? - sender.getNameColorId() : - ColorId.messageAuthor; - if (useBubbles()) { - custom.setCustomColorSet(new TextColorSetOverride(defaultTextColorSet()) { - @Override - public int defaultTextColor () { - return ColorUtils.fromToArgb( - getBubbleDateTextColor(), - Theme.getColor(nameColorId), - messagesController().wallpaper().getBackgroundTransparency() - ); - } + TdlibAccentColor accentColor = needColoredNames() ? sender.getAccentColor() : tdlib.accentColor(TdlibAccentColor.InternalId.REGULAR); + customizeColor(custom, accentColor); + return new FormattedText(text, new TextEntity[] {custom}); + } + } - @Override - public int clickableTextColor (boolean isPressed) { - return defaultTextColor(); - } + protected void customizeColor (TextEntity entity, TdlibAccentColor accentColor) { + if (useBubbles()) { + entity.setCustomColorSet(new TextColorSetOverride(defaultTextColorSet()) { + @Override + public int defaultTextColor () { + return ColorUtils.fromToArgb( + getBubbleDateTextColor(), + accentColor.getNameColor(), + messagesController().wallpaper().getBackgroundTransparency() + ); + } - @Override - public int backgroundColorId (boolean isPressed) { - float transparency = messagesController().wallpaper().getBackgroundTransparency(); - return isPressed && transparency == 1f ? - nameColorId : - 0; - } + @Override + public int clickableTextColor (boolean isPressed) { + return defaultTextColor(); + } - @Override - public int backgroundColor (boolean isPressed) { - int colorId = backgroundColorId(isPressed); - return colorId != 0 ? - ColorUtils.alphaColor(.2f, Theme.getColor(colorId)) : - 0; - } - }); - } else { - custom.setCustomColorSet(new TextColorSetOverride(defaultTextColorSet()) { - @Override - public int defaultTextColor () { - return Theme.getColor(nameColorId); - } + @Override + public int backgroundColorId (boolean isPressed) { + float transparency = messagesController().wallpaper().getBackgroundTransparency(); + long complexColor = accentColor.getNameComplexColor(); + return isPressed && transparency == 1f ? Theme.extractColorValue(complexColor) : 0; + } - @Override - public int clickableTextColor (boolean isPressed) { - return defaultTextColor(); - } + @Override + public int backgroundColor (boolean isPressed) { + float transparency = messagesController().wallpaper().getBackgroundTransparency(); + return isPressed && transparency == 1f ? ColorUtils.alphaColor(.2f, accentColor.getNameColor()) : 0; + } + }); + } else { + entity.setCustomColorSet(new TextColorSetOverride(defaultTextColorSet()) { + @Override + public int defaultTextColor () { + return accentColor.getNameColor(); + } - @Override - public int backgroundColorId (boolean isPressed) { - return isPressed ? nameColorId : 0; - } + @Override + public int clickableTextColor (boolean isPressed) { + return defaultTextColor(); + } - @Override - public int backgroundColor (boolean isPressed) { - int colorId = backgroundColorId(isPressed); - return colorId != 0 ? - ColorUtils.alphaColor(.2f, Theme.getColor(colorId)) : - 0; - } - }); - } - return new FormattedText(text, new TextEntity[] {custom}); + @Override + public int backgroundColorId (boolean isPressed) { + return isPressed ? Theme.extractColorValue(accentColor.getNameComplexColor()) : 0; + } + + @Override + public int backgroundColor (boolean isPressed) { + return isPressed ? ColorUtils.alphaColor(.2f, accentColor.getNameColor()) : 0; + } + }); } } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageSticker.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageSticker.java index c7ea2b529b..6ff5a27737 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageSticker.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageSticker.java @@ -16,9 +16,11 @@ import android.graphics.Canvas; import android.graphics.Path; +import android.graphics.Rect; import android.view.MotionEvent; import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.drinkless.tdlib.Client; @@ -29,6 +31,7 @@ import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.emoji.Emoji; +import org.thunderdog.challegram.emoji.EmojiInfo; import org.thunderdog.challegram.loader.ComplexReceiver; import org.thunderdog.challegram.loader.DoubleImageReceiver; import org.thunderdog.challegram.loader.ImageFile; @@ -37,28 +40,45 @@ import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.telegram.AnimatedEmojiListener; +import org.thunderdog.challegram.telegram.TdlibEmojiManager; +import org.thunderdog.challegram.telegram.TdlibThread; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.DrawAlgorithms; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.unsorted.Settings; +import org.thunderdog.challegram.util.NonBubbleEmojiLayout; +import org.thunderdog.challegram.util.text.TextMedia; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import me.vkryl.core.MathUtils; import me.vkryl.core.collection.IntSet; -import me.vkryl.core.lambda.FutureBool; +import me.vkryl.core.collection.LongSet; import me.vkryl.td.Td; import me.vkryl.td.TdConstants; -public class TGMessageSticker extends TGMessage implements AnimatedEmojiListener { - private TdApi.DiceStickers sticker; - private Path outline; +public class TGMessageSticker extends TGMessage implements AnimatedEmojiListener, TdlibEmojiManager.Watcher { + private @Nullable TdApi.DiceStickers sticker; + private @Nullable TdApi.FormattedText formattedText; + private @Nullable NonBubbleEmojiLayout multiEmojiLayout; + private List representation; private class Representation { - public final TdApi.Sticker sticker; + public final long stickerId; + public final String emoji; + public final @Nullable EmojiInfo emojiInfo; + + public @Nullable TdApi.Sticker sticker; + public float xIndex; + public int yIndex; + public int width, height; + public Path outline; @Nullable private ImageFile preview; @@ -68,11 +88,28 @@ private class Representation { @Nullable private GifFile animatedFile; - public boolean needRepainting; + public boolean needThemedColorFilter; + + public Representation (@NonNull TdApi.Sticker sticker, int fitzpatrickType, boolean allowNoLoop, boolean forcePlayOnce) { + this(sticker.id, sticker.emoji, Emoji.instance().getEmojiInfo(sticker.emoji), sticker, fitzpatrickType, allowNoLoop, forcePlayOnce); + } + + public Representation (long stickerId, String emoji, @Nullable EmojiInfo info, @Nullable TdApi.Sticker sticker, int fitzpatrickType, boolean allowNoLoop, boolean forcePlayOnce) { + this.stickerId = stickerId; + this.emoji = emoji; + this.emojiInfo = stickerId == 0 ? info : null; + setSticker(sticker, fitzpatrickType, allowNoLoop, forcePlayOnce); + } - public Representation (TdApi.Sticker sticker, int fitzpatrickType, boolean allowNoLoop, boolean forcePlayOnce) { + public void setSticker (@Nullable TdApi.Sticker sticker, int fitzpatrickType, boolean allowNoLoop, boolean forcePlayOnce) { + if (sticker == null || sticker.id != stickerId) { + return; + } this.sticker = sticker; - this.needRepainting = TD.needRepainting(sticker); + this.needThemedColorFilter = TD.needThemedColorFilter(sticker); + this.animatedFile = null; + this.staticFile = null; + this.preview = null; if (fitzpatrickType == 0 || !Td.isAnimated(sticker.format)) { this.preview = TD.toImageFile(tdlib, sticker.thumbnail); @@ -88,7 +125,7 @@ public Representation (TdApi.Sticker sticker, int fitzpatrickType, boolean allow if (allowNoLoop) { this.animatedFile.setPlayOnce( forcePlayOnce || - (specialType != SPECIAL_TYPE_NONE && !(Config.LOOP_BIG_CUSTOM_EMOJI && specialType == SPECIAL_TYPE_ANIMATED_EMOJI && Td.customEmojiId(sticker) != 0)) || + (specialType != SPECIAL_TYPE_NONE && !(specialType == SPECIAL_TYPE_ANIMATED_EMOJI && Td.customEmojiId(sticker) != 0)) || Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_STICKERS_LOOP) ); if (specialType == SPECIAL_TYPE_DICE) { @@ -115,25 +152,15 @@ public Representation (TdApi.Sticker sticker, int fitzpatrickType, boolean allow this.staticFile = new ImageFile(tdlib, sticker.sticker); this.staticFile.setScaleType(ImageFile.FIT_CENTER); this.staticFile.setWebp(); - /*if (baseSticker.width == 0 || baseSticker.height == 0) { - String path = TD.getFilePath(baseSticker.sticker); - if (path != null) { - BitmapFactory.Options opts = ImageReader.getImageWebpSize(path); - if (opts != null) { - baseSticker.width = opts.outWidth; - baseSticker.height = opts.outHeight; - } - } - }*/ } - } - public boolean isAnimated () { - return Td.isAnimated(sticker.format); + if (width > 0 && height > 0) { + setSize(width, height); + } } - public TdApi.ClosedVectorPath[] getOutline () { - return sticker.outline; + public boolean isAnimated () { + return sticker != null && Td.isAnimated(sticker.format); } public boolean hasAnimationEnded () { @@ -141,18 +168,13 @@ public boolean hasAnimationEnded () { } public void requestFiles (int key, ComplexReceiver receiver, boolean invalidate) { - /*@Override - public void requestPreview (DoubleImageReceiver receiver) { - if (stickerPreview == null || hasAnimationEnded()) { - receiver.clear(); - } else if (!isAnimated && TD.isFileLoaded(sticker)) { - receiver.clear(); - stickerPreview = null; - } else { - receiver.requestFile(null, stickerPreview); - } - }*/ - if (!invalidate) { + if (sticker == null) { + receiver.getGifReceiver(key).requestFile(null); + receiver.getImageReceiver(key).requestFile(null); + receiver.getPreviewReceiver(key).clear(); + return; + } + //if (!invalidate) { DoubleImageReceiver previewReceiver = receiver.getPreviewReceiver(key); if (preview == null || hasAnimationEnded()) { previewReceiver.clear(); @@ -162,11 +184,29 @@ public void requestPreview (DoubleImageReceiver receiver) { } else { previewReceiver.requestFile(null, preview); } + //} + GifFile file = receiver.getGifReceiver(key).getCurrentFile(); + if (file != animatedFile) { + receiver.getGifReceiver(key).requestFile(null); // The new file may have the same id as + receiver.getGifReceiver(key).requestFile(animatedFile); // old file, but a different requestedSize } - if (isAnimated()) { - receiver.getGifReceiver(key).requestFile(animatedFile); - } else { - receiver.getImageReceiver(key).requestFile(staticFile); + receiver.getImageReceiver(key).requestFile(staticFile); + } + + public void setSize (int width, int height) { + this.width = width; + this.height = height; + if (outline != null) { + outline.reset(); + } + if (sticker != null) { + outline = Td.buildOutline(sticker, width, height, outline); + if (staticFile != null) { + staticFile.setSize(Math.max(width, height)); + } + if (animatedFile != null) { + animatedFile.setRequestedSize(Math.max(width, height)); + } } } } @@ -176,9 +216,11 @@ public void requestPreview (DoubleImageReceiver receiver) { private static final int SPECIAL_TYPE_DICE = 2; private int stickerWidth, stickerHeight; + private int stickerRowsCount; + private float stickersMaxRowSize; private final int specialType; private TdApi.MessageDice dice; - private TdApi.MessageAnimatedEmoji currentEmoji, animatedEmoji, pendingEmoji; + private TdApi.MessageContent currentEmoji, animatedEmoji, pendingEmoji; public TGMessageSticker (MessagesManager context, TdApi.Message msg, TdApi.MessageDice dice) { super(context, msg); @@ -187,24 +229,32 @@ public TGMessageSticker (MessagesManager context, TdApi.Message msg, TdApi.Messa tdlib.listeners().subscribeToAnimatedEmojiUpdates(this); } - public TGMessageSticker (MessagesManager context, TdApi.Message msg, TdApi.MessageAnimatedEmoji content, TdApi.MessageAnimatedEmoji pendingContent) { + public TGMessageSticker (MessagesManager context, TdApi.Message msg, TdApi.MessageContent content, TdApi.MessageContent pendingContent) { super(context, msg); - this.animatedEmoji = content; - this.pendingEmoji = pendingContent; + this.animatedEmoji = checkContent(content); + this.pendingEmoji = checkContent(pendingContent); this.specialType = SPECIAL_TYPE_ANIMATED_EMOJI; updateAnimatedEmoji(); } private boolean updateAnimatedEmoji () { - TdApi.MessageAnimatedEmoji emoji = pendingEmoji != null ? pendingEmoji : animatedEmoji; - if (this.currentEmoji != emoji && !(this.currentEmoji != null && emoji == null)) { - this.currentEmoji = emoji; - if (emoji.animatedEmoji.sticker != null) { - setSticker(new TdApi.DiceStickersRegular(emoji.animatedEmoji.sticker), emoji.animatedEmoji.fitzpatrickType, false, true); - } else { - // wait for updateMessageContent - setSticker(null, 0, false, true); + TdApi.MessageContent messageContent = pendingEmoji != null ? pendingEmoji : animatedEmoji; + if (this.currentEmoji != messageContent && !(this.currentEmoji != null && messageContent == null)) { + this.currentEmoji = messageContent; + if (messageContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { + TdApi.MessageAnimatedEmoji emoji = (TdApi.MessageAnimatedEmoji) messageContent; + if (emoji.animatedEmoji.sticker != null) { + setSticker(new TdApi.DiceStickersRegular(emoji.animatedEmoji.sticker), emoji.animatedEmoji.fitzpatrickType, false, true); + return true; + } + } else if (messageContent.getConstructor() == TdApi.MessageText.CONSTRUCTOR) { + TdApi.MessageText text = (TdApi.MessageText) messageContent; + setStickers(text.text); + return true; } + + // wait for updateMessageContent + setSticker(null, 0, false, true); return true; } @@ -244,10 +294,21 @@ public void onAnimatedEmojiChanged (int type) { protected int onMessagePendingContentChanged (long chatId, long messageId, int oldHeight) { if (specialType == SPECIAL_TYPE_ANIMATED_EMOJI) { TdApi.MessageContent content = tdlib.getPendingMessageText(chatId, messageId); - if ((content == null && animatedEmoji == null) || (content != null && content.getConstructor() != TdApi.MessageAnimatedEmoji.CONSTRUCTOR)) { + if ((content == null && animatedEmoji == null)) { return MESSAGE_REPLACE_REQUIRED; } - this.pendingEmoji = (TdApi.MessageAnimatedEmoji) content; + if (content != null) { + if (animatedEmoji != null && content.getConstructor() != animatedEmoji.getConstructor()) { + return MESSAGE_REPLACE_REQUIRED; + } + if (content.getConstructor() == TdApi.MessageText.CONSTRUCTOR && !NonBubbleEmojiLayout.isValidEmojiText(((TdApi.MessageText) content).text)) { + return MESSAGE_REPLACE_REQUIRED; + } + if (content.getConstructor() != TdApi.MessageAnimatedEmoji.CONSTRUCTOR && content.getConstructor() != TdApi.MessageText.CONSTRUCTOR) { + return MESSAGE_REPLACE_REQUIRED; + } + } + this.pendingEmoji = checkContent(content); if (updateAnimatedEmoji()) { rebuildContent(); invalidateContentReceiver(); @@ -259,14 +320,14 @@ protected int onMessagePendingContentChanged (long chatId, long messageId, int o @Override protected boolean updateMessageContent (TdApi.Message message, TdApi.MessageContent newContent, boolean isBottomMessage) { - if (specialType == SPECIAL_TYPE_DICE && newContent.getConstructor() == TdApi.MessageDice.CONSTRUCTOR) { + if (specialType == SPECIAL_TYPE_DICE && Td.isDice(newContent)) { List prevRepresentation = this.representation; TdApi.MessageDice newDice = (TdApi.MessageDice) newContent; boolean hadFinalState = this.dice != null && this.dice.finalState != null; boolean hadInitialState = this.dice != null && this.dice.initialState != null; boolean hasFinalState = newDice.finalState != null; setDice(newDice, true); - if (hadInitialState && !hadFinalState && hasFinalState) { + if (hadInitialState && !hadFinalState && hasFinalState && sticker != null) { IntSet fileIds = new IntSet(); switch (sticker.getConstructor()) { case TdApi.DiceStickersRegular.CONSTRUCTOR: { @@ -283,8 +344,10 @@ protected boolean updateMessageContent (TdApi.Message message, TdApi.MessageCont fileIds.add(slotMachine.lever.sticker.id); break; } - default: - throw new UnsupportedOperationException(sticker.toString()); + default: { + Td.assertDiceStickers_bd2aa513(); + throw Td.unsupported(sticker); + } } AtomicInteger finished = new AtomicInteger(fileIds.size()); Client.ResultHandler handler = result -> { @@ -348,9 +411,10 @@ protected boolean updateMessageContent (TdApi.Message message, TdApi.MessageCont } else { invalidateContentReceiver(); } - } else if (specialType == SPECIAL_TYPE_ANIMATED_EMOJI && newContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { - this.animatedEmoji = (TdApi.MessageAnimatedEmoji) newContent; + } else if (specialType == SPECIAL_TYPE_ANIMATED_EMOJI) { + this.animatedEmoji = checkContent(newContent); if (updateAnimatedEmoji()) { + rebuildContent(); invalidateContentReceiver(); } return true; @@ -358,6 +422,16 @@ protected boolean updateMessageContent (TdApi.Message message, TdApi.MessageCont return false; } + protected boolean isSupportedMessageContent (TdApi.Message message, TdApi.MessageContent messageContent) { + if (specialType != SPECIAL_TYPE_NONE) { + final @EmojiMessageContentType int contentType = getEmojiMessageContentType(messageContent); + if (contentType == EmojiMessageContentType.NOT_EMOJI) { + return false; + } + } + return super.isSupportedMessageContent(message, messageContent); + } + public TGMessageSticker (MessagesManager context, TdApi.Message msg, TdApi.Sticker sticker, boolean isAnimatedEmoji, int fitzpatrickType) { super(context, msg); this.specialType = isAnimatedEmoji ? SPECIAL_TYPE_ANIMATED_EMOJI : SPECIAL_TYPE_NONE; @@ -372,8 +446,26 @@ public void markAsBeingAdded (boolean isBeingAdded) { } } + private void setStickers (TdApi.FormattedText text) { + this.formattedText = text; + this.sticker = null; + this.representation = new ArrayList<>(); + this.multiEmojiLayout = NonBubbleEmojiLayout.create(text); + if (multiEmojiLayout != null) { + for (NonBubbleEmojiLayout.Item emojiR : multiEmojiLayout.items) { + if (emojiR.type == NonBubbleEmojiLayout.Item.EMOJI) { + TdlibEmojiManager.Entry entry = emojiR.customEmojiId != 0 ? + tdlib.emoji().findOrPostponeRequest(emojiR.customEmojiId, this) : null; + representation.add(new Representation(emojiR.customEmojiId, emojiR.emoji, emojiR.info, entry != null ? entry.value : null, 0, true, false)); + } + } + tdlib.emoji().performPostponedRequests(); + } + } + private void setSticker (@Nullable TdApi.DiceStickers sticker, int fitzpatrickType, boolean isUpdate, boolean allowNoLoop) { this.sticker = sticker; + this.formattedText = null; final List representation = new ArrayList<>(); if (sticker != null) { switch (sticker.getConstructor()) { @@ -392,16 +484,19 @@ private void setSticker (@Nullable TdApi.DiceStickers sticker, int fitzpatrickTy break; } default: { - throw new UnsupportedOperationException(sticker.toString()); + Td.assertDiceStickers_bd2aa513(); + throw Td.unsupported(sticker); } } } + this.multiEmojiLayout = null; this.representation = representation; } public static final float MAX_STICKER_FORWARD_SIZE = 120f; public static final float MAX_STICKER_SIZE = 190f; + @Nullable private TdApi.Sticker getBaseSticker () { if (sticker == null) return null; @@ -411,7 +506,8 @@ private TdApi.Sticker getBaseSticker () { case TdApi.DiceStickersSlotMachine.CONSTRUCTOR: return ((TdApi.DiceStickersSlotMachine) sticker).background; } - throw new UnsupportedOperationException(sticker.toString()); + Td.assertDiceStickers_bd2aa513(); + throw Td.unsupported(sticker); } private long getStickerSetId () { @@ -423,7 +519,7 @@ private long getStickerSetId () { protected void buildContent (int origMaxWidth) { final TdApi.Sticker sticker = getBaseSticker(); float max = Screen.dp(useForward() ? MAX_STICKER_FORWARD_SIZE : MAX_STICKER_SIZE); - if (specialType != SPECIAL_TYPE_NONE || (sticker.setId == TdConstants.TELEGRAM_ANIMATED_EMOJI_STICKER_SET_ID)) { // TODO check for dice sticker set id + if (specialType != SPECIAL_TYPE_NONE || (sticker != null && sticker.setId == TdConstants.TELEGRAM_ANIMATED_EMOJI_STICKER_SET_ID)) { // TODO check for dice sticker set id max *= tdlib.emojiesAnimatedZoom(); } if (sticker != null) { @@ -438,23 +534,45 @@ protected void buildContent (int origMaxWidth) { stickerWidth = stickerHeight = (int) max; } - if (this.outline != null) { - this.outline.reset(); + if (specialType == SPECIAL_TYPE_ANIMATED_EMOJI && currentEmoji != null && currentEmoji.getConstructor() == TdApi.MessageText.CONSTRUCTOR) { + if (multiEmojiLayout == null) { + throw new IllegalArgumentException(); + } + + final int minEmojiSize = Screen.dp(30); + final int maxRowSize = origMaxWidth / minEmojiSize; + NonBubbleEmojiLayout.LayoutBuildResult layout = multiEmojiLayout.layout(maxRowSize, 0.2f); + + for (int a = 0; a < layout.representations.size(); a++) { + NonBubbleEmojiLayout.Representation emojiR = layout.representations.get(a); + if (representation != null && representation.size() > a) { + Representation repr = representation.get(a); + repr.xIndex = emojiR.x; + repr.yIndex = emojiR.y; + } + } + + stickersMaxRowSize = layout.maxLineSize; + stickerRowsCount = layout.linesCount; + + float realMaxWidth = MathUtils.fromTo(max, origMaxWidth, MathUtils.clamp(stickersMaxRowSize / maxRowSize)); + stickerWidth = stickerHeight = (int) Math.min(realMaxWidth / stickersMaxRowSize, Math.max(max / stickerRowsCount, minEmojiSize)); + if (layout.hasClassicEmoji) { + stickerWidth = stickerHeight = Math.min(stickerWidth, Screen.dp(40)); + } + } else { + stickersMaxRowSize = stickerRowsCount = 1; } if (representation != null) { for (Representation obj : representation) { - // Merging outlines from multiple stickers into a single path - this.outline = Td.buildOutline(obj.sticker, stickerWidth, stickerHeight, outline); - if (obj.staticFile != null) { - obj.staticFile.setSize(Math.max(stickerWidth, stickerHeight)); - } + obj.setSize(stickerWidth, stickerHeight); } } } @Override protected int getContentWidth () { - return stickerWidth; + return (int) (stickerWidth * stickersMaxRowSize); } @Override @@ -464,7 +582,10 @@ protected int getBubbleTimePartOffsetY () { @Override protected int getContentHeight () { - return Math.max(Screen.dp(56f), stickerHeight) + (specialType == SPECIAL_TYPE_DICE && useBubbles() && !useForward() ? getBubbleTimePartOffsetY() + getBubbleTimePartHeight() + Screen.dp(2f) : 0); + final boolean isInMultiEmojiMode = stickersMaxRowSize > 1 || stickerRowsCount > 1; + return Math.max(isInMultiEmojiMode ? 0 : Screen.dp(56f), stickerHeight * stickerRowsCount) + + ((specialType == SPECIAL_TYPE_DICE || isInMultiEmojiMode) && useBubbles() && !useForward() ? + getBubbleTimePartOffsetY() + getBubbleTimePartHeight() + Screen.dp(2f) : 0); } @Override @@ -493,7 +614,7 @@ protected int getAbsolutelyRealRightContentEdge (View view, int timePartWidth) { return super.getAbsolutelyRealRightContentEdge(view, timePartWidth); } else { int left = findStickerLeft(); - int desiredLeft = left + stickerWidth - timePartWidth; + int desiredLeft = (int) (left + stickerWidth * stickersMaxRowSize - timePartWidth); if (!useBubbles() || isOutgoingBubble()) { return desiredLeft; } else { @@ -503,65 +624,93 @@ protected int getAbsolutelyRealRightContentEdge (View view, int timePartWidth) { } private int findStickerLeft () { - return isOutgoingBubble() ? useBubble() ? getContentX() : getActualRightContentEdge() - stickerWidth : getContentX(); + return isOutgoingBubble() ? (int) (useBubble() ? getContentX() : getActualRightContentEdge() - stickerWidth * stickersMaxRowSize) : getContentX(); } + private final static Rect tmpRect = new Rect(); + @Override protected void drawContent (MessageView view, Canvas c, int startX, int startY, int maxWidth, ComplexReceiver receiver) { - int left = findStickerLeft(); - int top = getContentY(); - int right = left + stickerWidth; - int bottom = getContentY() + stickerHeight; + int leftDefault = findStickerLeft(); + int topDefault = getContentY(); + if (representation != null) { int index = 0; - if (this.outline != null) { - boolean needPlaceholder = false; + for (Representation representation : representation) { + int left = (int) (leftDefault + stickerWidth * (representation.xIndex)); + int top = topDefault + stickerHeight * (representation.yIndex); + + } + + boolean needScale = representation.size() > 1 && specialType == SPECIAL_TYPE_ANIMATED_EMOJI; + for (int a = 0; a < 3; a++) { + index = 0; for (Representation representation : representation) { - DoubleImageReceiver preview = receiver.getPreviewReceiver(index); - Receiver target = representation.isAnimated() ? receiver.getGifReceiver(index) : receiver.getImageReceiver(index); - if (target.needPlaceholder() && preview.needPlaceholder()) { - needPlaceholder = true; + final boolean isTgsSticker = representation.sticker != null && representation.sticker.format.getConstructor() == TdApi.StickerFormatTgs.CONSTRUCTOR; + final float scale = needScale && representation.sticker != null ? TextMedia.getScale(representation.sticker, stickerWidth) : 1f; + int left = (int) (leftDefault + stickerWidth * (representation.xIndex)); + int top = topDefault + stickerHeight * (representation.yIndex); + int right = left + stickerWidth; + int bottom = top + stickerHeight; + + final int saveScaleToCount; + boolean needRestore = scale != 1f; + if (needRestore) { + saveScaleToCount = Views.save(c); + c.scale(scale, scale, left + stickerWidth / 2f, top + stickerHeight / 2f); + } else { + saveScaleToCount = -1; } - index++; - } - if (needPlaceholder) { - final int saveCount = Views.save(c); - c.translate(left, top); - c.drawPath(outline, Paints.getPlaceholderPaint()); - Views.restore(c, saveCount); - } - } - index = 0; - for (Representation representation : representation) { - if (representation.needRepainting) { - c.saveLayerAlpha( - left - width / 4f, - top - height / 4f, - right + width / 4f, - bottom + height / 4f, - 255, Canvas.ALL_SAVE_FLAG); - } - DoubleImageReceiver preview = receiver.getPreviewReceiver(index); - Receiver target = representation.isAnimated() ? receiver.getGifReceiver(index) : receiver.getImageReceiver(index); - DrawAlgorithms.drawReceiver(c, preview, target, !representation.isAnimated(), false, left, top, right, bottom); - if (representation.needRepainting) { - c.drawRect( - left - width / 4f, - top - height / 4f, - right + width / 4f, - bottom + height / 4f, - Paints.getSrcInPaint(getTextColorSet().emojiStatusColor())); - c.restore(); - } - index++; - } + if (a == 0 && representation.outline != null) { + DoubleImageReceiver preview = receiver.getPreviewReceiver(index); + Receiver target = representation.isAnimated() ? receiver.getGifReceiver(index) : receiver.getImageReceiver(index); + if (target.needPlaceholder() && preview.needPlaceholder()) { + final int saveCount = Views.save(c); + c.translate(left, top); + c.drawPath(representation.outline, Paints.getPlaceholderPaint()); + Views.restore(c, saveCount); + } + } - if (Config.DEBUG_STICKER_OUTLINES) { - final int saveCount = Views.save(c); - c.translate(left, top); - c.drawPath(outline, Paints.fillingPaint(0x99ff0000)); - Views.restore(c, saveCount); + if (isTgsSticker && a == 2 || !isTgsSticker && a == 1) { + if (representation.sticker == null && representation.emojiInfo != null) { + tmpRect.set(left + Screen.dp(2), top + Screen.dp(2), right - Screen.dp(2), bottom - Screen.dp(2)); + Emoji.instance().draw(c, representation.emojiInfo, tmpRect); + } else { + DoubleImageReceiver preview = receiver.getPreviewReceiver(index); + final Receiver target; + if (representation.isAnimated()) { + target = receiver.getGifReceiver(index); + } else { + target = receiver.getImageReceiver(index); + } + if (representation.needThemedColorFilter) { + long complexColor = getTextColorSet().mediaTextComplexColor(); + Theme.applyComplexColor(preview, complexColor); + Theme.applyComplexColor(target, complexColor); + } else { + preview.disablePorterDuffColorFilter(); + target.disablePorterDuffColorFilter(); + } + DrawAlgorithms.drawReceiver(c, preview, target, !representation.isAnimated(), false, left, top, right, bottom); + } + } + if (a == 2) { + if (Config.DEBUG_STICKER_OUTLINES) { + if (representation.outline != null) { + final int saveCount = Views.save(c); + c.translate(left, top); + c.drawPath(representation.outline, Paints.fillingPaint(0x99ff0000)); + Views.restore(c, saveCount); + } + } + } + if (needRestore) { + Views.restore(c, saveScaleToCount); + } + index++; + } } } } @@ -614,11 +763,15 @@ public boolean performLongPress (View view, float x, float y) { } public boolean needSuggestOpenStickerPack () { - return getStickerSetId() != 0 && Td.isAnimated(getBaseSticker().format) && Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_STICKERS_LOOP) && getStickerSetId() != 0; + return getBaseSticker() != null && getStickerSetId() != 0 && Td.isAnimated(getBaseSticker().format) && Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_STICKERS_LOOP) && getStickerSetId() != 0; } public void openStickerSet () { - tdlib.ui().showStickerSet(controller(), getBaseSticker().setId, null); + TdApi.Sticker sticker = getBaseSticker(); + if (sticker == null) { + return; + } + tdlib.ui().showStickerSet(controller(), sticker.setId, null); } @Override @@ -634,8 +787,8 @@ public boolean onTouchEvent (MessageView view, MotionEvent e) { case MotionEvent.ACTION_DOWN: { int left = findStickerLeft(); int top = getContentY(); - int right = left + stickerWidth; - int bottom = getContentY() + stickerHeight; + int right = (int) (left + stickerWidth * stickersMaxRowSize); + int bottom = getContentY() + stickerHeight * stickerRowsCount; if (isCaught = (sticker != null && (x >= left && x < right && y >= top && y < bottom))) { startX = x; startY = y; @@ -661,18 +814,6 @@ public boolean onTouchEvent (MessageView view, MotionEvent e) { isCaught = false; boolean tapProcessed = false; TdApi.Sticker sticker = getBaseSticker(); - FutureBool fallbackAct = () -> { - GifFile animatedFile = view.getComplexReceiver().getGifReceiver(0).getCurrentFile(); - if (animatedFile != null && Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_STICKERS_LOOP) && animatedFile.setLooped(false)) { - invalidate(); - return true; - } - if (sticker != null && sticker.setId != 0) { - openStickerSet(); - return true; - } - return false; - }; switch (specialType) { case SPECIAL_TYPE_DICE: { tapProcessed = true; @@ -694,29 +835,17 @@ public boolean onTouchEvent (MessageView view, MotionEvent e) { GifReceiver receiver = ((MessageView) targetView).getComplexReceiver().getGifReceiver(0); if (receiver != null) { outRect.set(receiver.getLeft(), receiver.getTop(), receiver.getRight(), receiver.getBottom()); - outRect.top += outRect.height() * (TD.EMOJI_DICE.textRepresentation.equals(dice.emoji) ? .35f : .20f); + outRect.top += outRect.height() * (ContentPreview.EMOJI_DICE.textRepresentation.equals(dice.emoji) ? .35f : .20f); } }) .gif(gifFile, imageFile) .controller(controller()) - .show(tdlib, Lang.getString(TD.EMOJI_DART.textRepresentation.equals(dice.emoji) ? R.string.SendDartHint : TD.EMOJI_DICE.textRepresentation.equals(dice.emoji) ? R.string.SendDiceHint : R.string.SendUnknownDiceHint, dice.emoji)); - break; - } - case SPECIAL_TYPE_ANIMATED_EMOJI: { - GifFile animatedFile = view.getComplexReceiver() != null ? view.getComplexReceiver().getGifReceiver(0).getCurrentFile() : null; - if (Config.LOOP_BIG_CUSTOM_EMOJI && Td.customEmojiId(sticker) != 0) { - tapProcessed = fallbackAct.getBoolValue(); - } else if (animatedFile != null) { - tapProcessed = animatedFile.setVibrationPattern(Emoji.instance().getVibrationPatternType(sticker.emoji)); - if (animatedFile.setLooped(false)) { - tapProcessed = true; - invalidate(); - } - } + .show(tdlib, Lang.getString(ContentPreview.EMOJI_DART.textRepresentation.equals(dice.emoji) ? R.string.SendDartHint : ContentPreview.EMOJI_DICE.textRepresentation.equals(dice.emoji) ? R.string.SendDiceHint : R.string.SendUnknownDiceHint, dice.emoji)); break; } + case SPECIAL_TYPE_ANIMATED_EMOJI: default: { - tapProcessed = fallbackAct.getBoolValue(); + tapProcessed = openOrLoopSticker(view, sticker, Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_STICKERS_LOOP)); break; } } @@ -731,4 +860,88 @@ public boolean onTouchEvent (MessageView view, MotionEvent e) { return isCaught; } + + private boolean openOrLoopSticker (MessageView view, TdApi.Sticker sticker, boolean noLoopSettingEnabled) { + final boolean isAnimatedEmojiStickerSet = sticker != null && sticker.setId == TdConstants.TELEGRAM_ANIMATED_EMOJI_STICKER_SET_ID; + + GifFile animatedFile = view.getComplexReceiver().getGifReceiver(0).getCurrentFile(); + if (animatedFile != null && (noLoopSettingEnabled || isAnimatedEmojiStickerSet) && animatedFile.setLooped(false)) { + invalidate(); + return true; + } + if (sticker != null && sticker.setId != 0 && (!isAnimatedEmojiStickerSet || specialType != SPECIAL_TYPE_ANIMATED_EMOJI)) { + openStickerSet(); + return true; + } + return false; + } + + @Override + public long getFirstEmojiId () { + if (formattedText != null && formattedText.entities != null) { + for (TdApi.TextEntity entity: formattedText.entities) { + if (entity.type.getConstructor() == TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR) { + return ((TdApi.TextEntityTypeCustomEmoji) entity.type).customEmojiId; + } + } + } + if (sticker != null && sticker.getConstructor() == TdApi.DiceStickersRegular.CONSTRUCTOR) { + TdApi.Sticker sticker1 = ((TdApi.DiceStickersRegular) sticker).sticker; + return Td.customEmojiId(sticker1); + } + return 0; + } + + @Override + public long[] getUniqueEmojiPackIdList () { + if (formattedText != null) { + long[] emojiIds = TD.getUniqueEmojiIdList(formattedText); + LongSet emojiSets = new LongSet(); + for (long emojiId : emojiIds) { + TdlibEmojiManager.Entry entry = tdlib().emoji().find(emojiId); + if (entry == null || entry.value == null) continue; + emojiSets.add(entry.value.setId); + } + return emojiSets.toArray(); + } + + if (sticker != null && sticker.getConstructor() == TdApi.DiceStickersRegular.CONSTRUCTOR) { + TdApi.Sticker sticker1 = ((TdApi.DiceStickersRegular) sticker).sticker; + if (Td.customEmojiId(sticker1) != 0) { + return new long[] { + sticker1.setId + }; + } + } + + return new long[0]; + } + + @TdlibThread + @Override + public void onCustomEmojiLoaded (TdlibEmojiManager context, TdlibEmojiManager.Entry entry) { + final TdApi.Sticker sticker = entry.value; + if (sticker == null) return; + + UI.post(() -> { + if (representation != null) { + boolean needInvalidate = false; + for (Representation representation: representation) { + if (sticker.id == representation.stickerId) { + representation.setSticker(entry.value, 0, true, false); + needInvalidate = true; + } + } + if (needInvalidate) { + invalidateContentReceiver(); + } + } + }); + } + + private static TdApi.MessageContent checkContent (TdApi.MessageContent content) { + final boolean allowAnimatedEmoji = !Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_EMOJI); + return !allowAnimatedEmoji && TD.isStickerFromAnimatedEmojiPack(content) ? + new TdApi.MessageText(Td.textOrCaption(content), null, null) : content; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageText.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageText.java index 4c6b338daa..1895dd1152 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageText.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageText.java @@ -23,7 +23,6 @@ import androidx.annotation.Nullable; import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.chat.MessageView; import org.thunderdog.challegram.component.chat.MessagesManager; import org.thunderdog.challegram.config.Config; @@ -34,10 +33,10 @@ import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.mediaview.MediaViewThumbLocation; -import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Strings; +import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.util.text.Highlight; import org.thunderdog.challegram.util.text.Text; @@ -46,16 +45,63 @@ import org.thunderdog.challegram.util.text.TextEntity; import org.thunderdog.challegram.util.text.TextWrapper; +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.BoolAnimator; +import me.vkryl.android.animator.ListAnimator; +import me.vkryl.android.animator.ReplaceAnimator; +import me.vkryl.android.animator.VariableFloat; +import me.vkryl.core.MathUtils; +import me.vkryl.core.StringUtils; import me.vkryl.td.Td; public class TGMessageText extends TGMessage { private TdApi.FormattedText text; - private TextWrapper wrapper; + private final VariableFloat lastLineWidth = new VariableFloat(0f); + private final ReplaceAnimator visibleText = new ReplaceAnimator<>(new ReplaceAnimator.Callback() { + @Override + public void onItemChanged (ReplaceAnimator animator) { + if (rebuildContentDimensions()) { + requestLayout(); + } else { + invalidate(); + } + } + + @Override + public boolean hasChanges (ReplaceAnimator animator) { + int width = effectiveWrapper != null ? effectiveWrapper.getLastLineWidth() : 0; + return lastLineWidth.differs(width); + } + + @Override + public void onForceApplyChanges (ReplaceAnimator animator) { + int width = effectiveWrapper != null ? effectiveWrapper.getLastLineWidth() : 0; + lastLineWidth.set(width); + } + + @Override + public void onPrepareMetadataAnimation (ReplaceAnimator animator) { + int width = effectiveWrapper != null ? effectiveWrapper.getLastLineWidth() : 0; + lastLineWidth.setTo(width); + } + + @Override + public void onFinishMetadataAnimation (ReplaceAnimator animator, boolean applyFuture) { + lastLineWidth.finishAnimation(applyFuture); + } + + @Override + public boolean onApplyMetadataAnimation (ReplaceAnimator animator, float factor) { + return lastLineWidth.applyAnimation(factor); + } + }, AnimatorUtils.DECELERATE_INTERPOLATOR, TEXT_CROSS_FADE_DURATION_MS); private TGWebPage webPage; private TdApi.MessageText currentMessageText, pendingMessageText; - - public TdApi.SponsoredMessage sponsoredMetadata; + private final BoolAnimator webPageOnTop = new BoolAnimator(0, + (id, factor, fraction, callee) -> invalidate(), + AnimatorUtils.DECELERATE_INTERPOLATOR, 180L + ); public TGMessageText (MessagesManager context, TdApi.Message msg, TdApi.MessageText text, @Nullable TdApi.MessageText pendingMessageText) { super(context, msg); @@ -63,22 +109,21 @@ public TGMessageText (MessagesManager context, TdApi.Message msg, TdApi.MessageT this.pendingMessageText = pendingMessageText; if (this.pendingMessageText != null) { setText(this.pendingMessageText.text, false); - setWebPage(this.pendingMessageText.webPage); + setWebPage(this.pendingMessageText.webPage, this.pendingMessageText.linkPreviewOptions); } else { setText(text.text, false); - setWebPage(text.webPage); + setWebPage(text.webPage, text.linkPreviewOptions); } } - public TGMessageText (MessagesManager context, TdApi.Message msg, TdApi.SponsoredMessage text) { - super(context, msg); - this.sponsoredMetadata = text; - this.currentMessageText = (TdApi.MessageText) text.content; + public TGMessageText (MessagesManager context, long inChatId, TdApi.SponsoredMessage sponsoredMessage) { + super(context, inChatId, sponsoredMessage); + this.currentMessageText = (TdApi.MessageText) sponsoredMessage.content; setText(currentMessageText.text, false); } public TGMessageText (MessagesManager context, TdApi.Message msg, TdApi.FormattedText text) { - this(context, msg, new TdApi.MessageText(text, null), null); + this(context, msg, new TdApi.MessageText(text, null, null), null); } public TdApi.File getTargetFile () { @@ -112,6 +157,7 @@ public String findUriFragment (TdApi.WebPage webPage) { Uri uri = null; for (TdApi.TextEntity entity : text.entities) { String url; + //noinspection SwitchIntDef switch (entity.type.getConstructor()) { case TdApi.TextEntityTypeUrl.CONSTRUCTOR: url = Td.substring(text.text, entity); @@ -136,22 +182,33 @@ public String findUriFragment (TdApi.WebPage webPage) { protected int onMessagePendingContentChanged (long chatId, long messageId, int oldHeight) { if (currentMessageText != null) { TdApi.MessageContent messageContent = tdlib.getPendingMessageText(chatId, messageId); - if (messageContent != null && messageContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR && Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_EMOJI)) { - messageContent = new TdApi.MessageText(Td.textOrCaption(messageContent), null); + final @EmojiMessageContentType int contentType = getEmojiMessageContentType(messageContent); + boolean allowEmoji = !Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_EMOJI); + if (contentType != EmojiMessageContentType.NOT_EMOJI) { + return MESSAGE_REPLACE_REQUIRED; + } + if (messageContent != null && Td.isAnimatedEmoji(messageContent) && !allowEmoji) { + messageContent = new TdApi.MessageText(Td.textOrCaption(messageContent), null, null); } if (this.pendingMessageText != messageContent) { - if (messageContent != null && messageContent.getConstructor() != TdApi.MessageText.CONSTRUCTOR) + if (messageContent != null && !Td.isText(messageContent)) return MESSAGE_REPLACE_REQUIRED; TdApi.MessageText messageText = (TdApi.MessageText) messageContent; this.pendingMessageText = messageText; + final boolean textChanged, webPageChanged; if (messageText != null) { - setText(messageText.text, false); - setWebPage(messageText.webPage); + textChanged = setText(messageText.text, false); + webPageChanged = setWebPage(messageText.webPage, messageText.linkPreviewOptions); } else { - setText(currentMessageText.text, false); - setWebPage(currentMessageText.webPage); + textChanged = setText(currentMessageText.text, false); + webPageChanged = setWebPage(currentMessageText.webPage, currentMessageText.linkPreviewOptions); + } + if (!textChanged && !webPageChanged) { + return MESSAGE_NOT_CHANGED; + } + if (webPageChanged) { + rebuildContent(); } - rebuildContent(); return (getHeight() == oldHeight ? MESSAGE_INVALIDATED : MESSAGE_CHANGED); } } @@ -173,9 +230,9 @@ protected boolean hasInstantView (String link) { boolean found = false; for (TdApi.TextEntity entity : text.entities) { String url; - if (entity.type.getConstructor() == TdApi.TextEntityTypeUrl.CONSTRUCTOR) { + if (Td.isUrl(entity.type)) { url = text.text.substring(entity.offset, entity.offset + entity.length); - } else if (entity.type.getConstructor() == TdApi.TextEntityTypeTextUrl.CONSTRUCTOR) { + } else if (Td.isTextUrl(entity.type)) { url = ((TdApi.TextEntityTypeTextUrl) entity.type).url; } else { continue; @@ -196,43 +253,56 @@ protected boolean isBeingEdited () { } private boolean setText (TdApi.FormattedText text, boolean parseEntities) { - return setText(text, parseEntities, false); + return setText(text, parseEntities, false, true); } - private boolean setText (TdApi.FormattedText text, boolean parseEntities, boolean forceUpdate) { + private boolean setText (TdApi.FormattedText text, boolean parseEntities, boolean forceUpdate, boolean animated) { if (this.text == null || !Td.equalsTo(this.text, text) || forceUpdate) { + animated = animated && Config.ENABLE_TEXT_ANIMATIONS && needAnimateChanges(); + this.text = text; TextColorSet colorSet = isErrorMessage() ? TextColorSets.Regular.NEGATIVE : getTextColorSet(); TextWrapper.TextMediaListener textMediaListener = (wrapper, updatedText, specificTextMedia) -> { - if (this.wrapper == wrapper) { + if (effectiveWrapper == wrapper) { invalidateTextMediaReceiver(updatedText, specificTextMedia); } }; + TextWrapper wrapper; if (translatedText != null) { - this.wrapper = new TextWrapper(translatedText.text, getTextStyleProvider(), colorSet) + wrapper = new TextWrapper(translatedText.text, getTextStyleProvider(), colorSet) .setEntities(TextEntity.valueOf(tdlib, translatedText, openParameters()), textMediaListener) .setHighlightText(getHighlightedText(Highlight.Pool.KEY_TEXT, translatedText.text)) .setClickCallback(clickCallback()); } else if (text.entities != null || !parseEntities) { - this.wrapper = new TextWrapper(text.text, getTextStyleProvider(), colorSet) + wrapper = new TextWrapper(text.text, getTextStyleProvider(), colorSet) .setEntities(TextEntity.valueOf(tdlib, text, openParameters()), textMediaListener) .setHighlightText(getHighlightedText(Highlight.Pool.KEY_TEXT, text.text)) .setClickCallback(clickCallback()); } else { - this.wrapper = new TextWrapper(text.text, getTextStyleProvider(), colorSet) + wrapper = new TextWrapper(text.text, getTextStyleProvider(), colorSet) .setEntities(Text.makeEntities(text.text, Text.ENTITY_FLAGS_ALL, null, tdlib, openParameters()), textMediaListener) .setHighlightText(getHighlightedText(Highlight.Pool.KEY_TEXT, text.text)) .setClickCallback(clickCallback()); } - this.wrapper.addTextFlags(Text.FLAG_BIG_EMOJI); + wrapper.addTextFlags(Text.FLAG_BIG_EMOJI); if (useBubbles()) { - this.wrapper.addTextFlags(Text.FLAG_ADJUST_TO_CURRENT_WIDTH); + wrapper.addTextFlags(Text.FLAG_ADJUST_TO_CURRENT_WIDTH); } if (Config.USE_NONSTRICT_TEXT_ALWAYS || !useBubbles()) { - this.wrapper.addTextFlags(Text.FLAG_BOUNDS_NOT_STRICT); + wrapper.addTextFlags(Text.FLAG_BOUNDS_NOT_STRICT); + } + wrapper.setViewProvider(currentViews); + TextWrapper lastWrapper = effectiveWrapper; + boolean hadMedia = lastWrapper != null && lastWrapper.hasMedia(); + if (hadMedia) { + textMediaKeyOffset += lastWrapper.getMaxMediaCount(); } - this.wrapper.setViewProvider(currentViews); - if (hasMedia()) { + wrapper.prepare(getContentMaxWidth()); + this.effectiveWrapper = wrapper; + this.visibleText.replace(wrapper, animated); + this.visibleText.measure(animated); + + if (hadMedia || wrapper.hasMedia()) { invalidateTextMediaReceiver(); } return true; @@ -243,28 +313,32 @@ private boolean setText (TdApi.FormattedText text, boolean parseEntities, boolea @Override protected void onUpdateHighlightedText () { if (this.text != null) { - setText(this.text, false, true); + setText(this.text, false, true, false); rebuildContent(); } } - private boolean hasMedia () { - return wrapper.hasMedia() || (webPage != null && webPage.hasMedia()); - } + private TextWrapper effectiveWrapper; - private static final int WEB_PAGE_RECEIVERS_KEY = Integer.MAX_VALUE / 2; + private static final int MAX_WEB_PAGE_MEDIA_COUNT = Integer.MAX_VALUE / 4; + private long textMediaKeyOffset; @Override public void requestTextMedia (ComplexReceiver textMediaReceiver) { - if (wrapper != null) { - wrapper.requestMedia(textMediaReceiver, 0, WEB_PAGE_RECEIVERS_KEY); - if (webPage != null) { - webPage.requestTextMedia(textMediaReceiver, WEB_PAGE_RECEIVERS_KEY); - } else { - textMediaReceiver.clearReceiversWithHigherKey(WEB_PAGE_RECEIVERS_KEY); - } - } else { + if (effectiveWrapper == null && webPage == null) { textMediaReceiver.clear(); + return; + } + if (webPage != null) { + webPage.requestTextMedia(textMediaReceiver, 0); + } else { + textMediaReceiver.clearReceiversRange(0, MAX_WEB_PAGE_MEDIA_COUNT); + } + if (effectiveWrapper != null) { + long startKey = MAX_WEB_PAGE_MEDIA_COUNT + textMediaKeyOffset; + effectiveWrapper.requestMedia(textMediaReceiver, startKey, Long.MAX_VALUE - startKey); + } else { + textMediaReceiver.clearReceiversWithHigherKey(MAX_WEB_PAGE_MEDIA_COUNT); } } @@ -279,28 +353,40 @@ protected int getBubbleContentPadding () { protected void buildContent (int maxWidth) { maxWidth = Math.max(maxWidth, computeBubbleTimePartWidth(false)); - wrapper.prepare(maxWidth); this.maxWidth = maxWidth; + for (ListAnimator.Entry entry : visibleText) { + entry.item.prepare(maxWidth); + } + visibleText.measure(false); + int webPageMaxWidth = getSmallestMaxContentWidth(); if (pendingMessageText != null) { - if (setWebPage(pendingMessageText.webPage)) + if (setWebPage(pendingMessageText.webPage, pendingMessageText.linkPreviewOptions)) webPage.buildLayout(webPageMaxWidth); - } else if (msg.content.getConstructor() == TdApi.MessageText.CONSTRUCTOR && setWebPage(((TdApi.MessageText) msg.content).webPage)) { + } else if (Td.isText(msg.content) && setWebPage(((TdApi.MessageText) msg.content).webPage, ((TdApi.MessageText) msg.content).linkPreviewOptions)) { webPage.buildLayout(webPageMaxWidth); } else if (webPage != null && webPage.getMaxWidth() != webPageMaxWidth) { webPage.buildLayout(webPageMaxWidth); } } - private boolean setWebPage (TdApi.WebPage page) { + private boolean setWebPage (TdApi.WebPage page, @Nullable TdApi.LinkPreviewOptions linkPreviewOptions) { if (page != null) { - String url = text != null ? Td.findUrl(text, page.url, false) : page.url; - this.webPage = new TGWebPage(this, page, url); + String url = null; + if (text != null) { + url = Td.findUrl(text, page.url, false); + } + if (StringUtils.isEmpty(url)) { + url = page.url; + } + this.webPage = new TGWebPage(this, page, url, linkPreviewOptions); this.webPage.setViewProvider(currentViews); + this.webPageOnTop.setValue(linkPreviewOptions != null && linkPreviewOptions.showAboveText, needAnimateChanges()); return true; } else { this.webPage = null; + this.webPageOnTop.setValue(false, false); } return false; } @@ -320,25 +406,28 @@ protected void onMessageAttachedToView (@NonNull MessageView view, boolean attac } private int getWebY () { - if (Td.isEmpty(text)) { - return getContentY(); - } else { - return getContentY() + wrapper.getHeight() + getTextTopOffset() + Screen.dp(6f); - } + float webPageOnTop = this.webPageOnTop.getFloatValue(); + return getContentY() + getTextTopOffset() + (int) ((visibleText.getMetadata().getTotalHeight() + Screen.dp(6f) * visibleText.getMetadata().getTotalVisibility()) * (1f - webPageOnTop)); } @Override protected boolean isSupportedMessageContent (TdApi.Message message, TdApi.MessageContent messageContent) { - if (messageContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) - return Settings.instance().getNewSetting(Settings.SETTING_FLAG_NO_ANIMATED_EMOJI); + final @EmojiMessageContentType int contentType = getEmojiMessageContentType(messageContent); + if (messageContent.getConstructor() == TdApi.MessageText.CONSTRUCTOR || messageContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { + return contentType == EmojiMessageContentType.NOT_EMOJI; + } return super.isSupportedMessageContent(message, messageContent); } @Override protected boolean onMessageContentChanged (TdApi.Message message, TdApi.MessageContent oldContent, TdApi.MessageContent newContent, boolean isBottomMessage) { + TdApi.MessageText oldMessageText = Td.isText(oldContent) ? (TdApi.MessageText) oldContent : null; + TdApi.MessageText newMessageText = Td.isText(newContent) ? (TdApi.MessageText) newContent : null; if (!Td.equalsTo(Td.textOrCaption(oldContent), Td.textOrCaption(newContent)) || - !Td.equalsTo(oldContent.getConstructor() == TdApi.MessageText.CONSTRUCTOR ? ((TdApi.MessageText) oldContent).webPage : null, - newContent.getConstructor() == TdApi.MessageText.CONSTRUCTOR ? ((TdApi.MessageText) newContent).webPage : null) + !Td.equalsTo(oldMessageText != null ? oldMessageText.webPage : null, + newMessageText != null ? newMessageText.webPage : null) || + !Td.equalsTo(oldMessageText != null ? oldMessageText.linkPreviewOptions : null, + newMessageText != null ? newMessageText.linkPreviewOptions : null) ) { updateMessageContent(msg, newContent, isBottomMessage); return true; @@ -348,18 +437,20 @@ protected boolean onMessageContentChanged (TdApi.Message message, TdApi.MessageC @Override protected boolean updateMessageContent (TdApi.Message message, TdApi.MessageContent newContent, boolean isBottomMessage) { - TdApi.WebPage oldWebPage = this.msg.content.getConstructor() == TdApi.MessageText.CONSTRUCTOR ? ((TdApi.MessageText) this.msg.content).webPage : null; this.msg.content = newContent; - TdApi.MessageText newText = newContent.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR ? new TdApi.MessageText(Td.textOrCaption(newContent), null) : (TdApi.MessageText) newContent; + TdApi.MessageText newText = Td.isText(newContent) ? (TdApi.MessageText) newContent : new TdApi.MessageText(Td.textOrCaption(newContent), null, null); this.currentMessageText = newText; if (!isBeingEdited()) { - setText(newText.text, false); - setWebPage(newText.webPage); - rebuildContent(); - if (!Td.equalsTo(oldWebPage, newText.webPage)) { + boolean textChanged = setText(newText.text, false); + boolean webPageChanged = setWebPage(newText.webPage, newText.linkPreviewOptions); + if (webPageChanged) { + rebuildContent(); invalidateContent(this); invalidatePreviewReceiver(); } + if (webPageChanged || textChanged) { + invalidate(); + } } return true; } @@ -398,7 +489,7 @@ public void autoDownloadContent (TdApi.ChatType type) { @Override public void requestPreview (DoubleImageReceiver receiver) { if (webPage != null) { - webPage.requestPreview(receiver, getContentX(), getWebY()); + webPage.requestPreview(receiver); } else { receiver.clear(); } @@ -415,16 +506,51 @@ public void requestGif (GifReceiver receiver) { // Text without any trash - private int getStartXRtl (int startX, int maxWidth) { + private int getStartXRtl (TextWrapper wrapper, int startX, int maxWidth) { return useBubbles() ? (Config.MOVE_BUBBLE_TIME_RTL_TO_LEFT || wrapper.getLineCount() > 1 ? getActualRightContentEdge() - getBubbleContentPadding() : startX) : startX + maxWidth; } @Override protected void drawContent (MessageView view, Canvas c, int startX, int startY, int maxWidth, Receiver preview, Receiver receiver) { float alpha = getTranslationLoadingAlphaValue(); - wrapper.draw(c, startX, getStartXRtl(startX, maxWidth), Config.MOVE_BUBBLE_TIME_RTL_TO_LEFT ? 0 : getBubbleTimePartWidth(), startY + getTextTopOffset(), null, alpha, view.getTextMediaReceiver()); + final int endXPadding = Config.MOVE_BUBBLE_TIME_RTL_TO_LEFT ? 0 : getBubbleTimePartWidth(); + float webPageOnTop = this.webPageOnTop.getFloatValue(); + ComplexReceiver textMediaReceiver = view.getTextMediaReceiver(); + int webPageY = getWebY(); + final int topTextY = startY + getTextTopOffset(); + final int bottomTextY = webPage == null ? topTextY : webPageY + webPage.getHeight() + Screen.dp(6f) + Screen.dp(2f); + for (ListAnimator.Entry entry : visibleText) { + final int startXRtl = getStartXRtl(entry.item, startX, maxWidth); + boolean needClip = entry.getVisibility() != 1f && useBubbles(); + int saveToCount = -1; + if (needClip) { + saveToCount = Views.save(c); + c.clipRect(bubblePathRect); + } + float textAlpha = alpha * entry.getVisibility(); + if (webPageOnTop == 0f || webPage == null || receiver == null) { + entry.item.draw(c, startX, startXRtl, endXPadding, topTextY, null, textAlpha, textMediaReceiver); + } else if (webPageOnTop == 1f) { + entry.item.draw(c, startX, startXRtl, endXPadding, bottomTextY, null, textAlpha, textMediaReceiver); + } else { + entry.item.beginDrawBatch(textMediaReceiver, 1); + + // top text + int topTextHeight = (int) ((float) (entry.item.getHeight() + Screen.dp(6f)) * MathUtils.clamp(webPageOnTop)); + entry.item.draw(c, startX, startXRtl, endXPadding, topTextY - topTextHeight, null, textAlpha * MathUtils.clamp(1f - webPageOnTop), textMediaReceiver); + + // bottom text + entry.item.draw(c, startX, startXRtl, endXPadding, bottomTextY, null, textAlpha * MathUtils.clamp(webPageOnTop), textMediaReceiver); + + entry.item.finishDrawBatch(textMediaReceiver, 1); + } + if (needClip) { + Views.restore(c, saveToCount); + } + } if (webPage != null && receiver != null) { - webPage.draw(view, c, Lang.rtl() ? startX + maxWidth - webPage.getWidth() : startX, getWebY(), preview, receiver, alpha, view.getTextMediaReceiver()); + int webPageX = Lang.rtl() ? startX + maxWidth - webPage.getWidth() : startX; + webPage.draw(view, c, webPageX, webPageY, preview, receiver, alpha, textMediaReceiver); } } @@ -435,14 +561,11 @@ protected void drawContent (MessageView view, Canvas c, int startX, int startY, @Override protected int getContentHeight () { - int height = 0; - if (!Td.isEmpty(text)) { - height += wrapper.getHeight() + getTextTopOffset(); - } + int height = Math.round(visibleText.getMetadata().getTotalHeight() + getTextTopOffset() * visibleText.getMetadata().getTotalVisibility()); if (webPage != null) { if (height > 0) - height += Screen.dp(8f); - height += webPage.getHeight(); + height += Screen.dp(6f); + height += webPage.getHeight() + Screen.dp(2f); } return height; } @@ -452,47 +575,76 @@ protected int getFooterPaddingTop () { return Td.isEmpty(text) ? -Screen.dp(3f) : Screen.dp(7f); } + private int calculateTextLastLineWidth () { + if (effectiveWrapper != null && Lang.rtl() == effectiveWrapper.getLastLineIsRtl()) { + // TODO: support for rtl <-> non-rtl transition + return Math.round(lastLineWidth.get()); + } + return BOTTOM_LINE_EXPAND_HEIGHT; + } + @Override protected int getBottomLineContentWidth () { - if (webPage != null) { - return webPage.getLastLineWidth(); - } - if (Lang.rtl() == wrapper.getLastLineIsRtl()) { - return wrapper.getLastLineWidth(); + int textLastLineWidth = calculateTextLastLineWidth(); + float webPageOnTop = this.webPageOnTop.getFloatValue(); + if (webPageOnTop == 0f || webPage == null) { + if (webPage != null) { + return webPage.getLastLineWidth(); + } else { + return textLastLineWidth; + } + } else if (webPageOnTop == 1f) { + return textLastLineWidth; } else { - return BOTTOM_LINE_EXPAND_HEIGHT; + // Animated + return BOTTOM_LINE_DEFINE_BY_FACTOR; } } - /*@Override - protected boolean allowBubbleHorizontalExtend () { - return messageReactions.getBubblesCount() < 2 || !useReactionBubbles() || replyData != null; - }*/ + @Override + protected float getIntermediateBubbleExpandFactor () { + int textLastLineWidth = calculateTextLastLineWidth(); + int webPageLastLineWidth = webPage != null ? webPage.getLastLineWidth() : textLastLineWidth; + float fromExpandFactor = webPageLastLineWidth == BOTTOM_LINE_EXPAND_HEIGHT ? 1f : 0f; + float toExpandFactor = textLastLineWidth == BOTTOM_LINE_EXPAND_HEIGHT ? 1f : 0f; + return MathUtils.fromTo(fromExpandFactor, toExpandFactor, webPageOnTop.getFloatValue()); + } + + @Override + protected int getAnimatedBottomLineWidth (int bubbleTimePartWidth) { + int textLastLineWidth = calculateTextLastLineWidth(); + int webPageLastLineWidth = webPage != null ? webPage.getLastLineWidth() : textLastLineWidth; + float factor = webPageOnTop.getFloatValue(); + if (factor == 1f || webPage == null) { + return textLastLineWidth; + } else if (factor == 0f) { + return webPageLastLineWidth; + } + int fromLastLineWidth = webPageLastLineWidth == BOTTOM_LINE_EXPAND_HEIGHT ? webPage.getWidth() - bubbleTimePartWidth : webPageLastLineWidth; + int toLastLineWidth = /*textLastLineWidth == BOTTOM_LINE_KEEP_WIDTH ? wrapper.getWidth() - bubbleTimePartWidth : */textLastLineWidth; + return MathUtils.fromTo(fromLastLineWidth, toLastLineWidth, factor); + } @Override protected int getContentWidth () { + int textWidth = Math.round(visibleText.getMetadata().getTotalWidth()); if (webPage != null) { - return Math.max(wrapper.getWidth(), webPage.getWidth()); - } /* else if (messageReactions.getTotalCount() > 0 && useReactionBubbles()) { - int textWidth = Math.max(wrapper.getWidth(), computeBubbleTimePartWidth(false)); - int reactionsWidth = messageReactions.getWidth(); - return Math.min(maxWidth, Math.max(textWidth, reactionsWidth)); - } */ - - return wrapper.getWidth(); + return Math.max(textWidth, webPage.getWidth()); + } + return textWidth; } private boolean forceExpand = false; @Override protected void buildReactions (boolean animated) { - if (webPage != null || !useBubble() || wrapper == null || !useReactionBubbles() /*|| replyData != null*/) { + if (webPage != null || !useBubble() || visibleText.isEmpty() || !useReactionBubbles() /*|| replyData != null*/) { super.buildReactions(animated); } else { final float maxWidthMultiply = replyData != null ? 1f : 0.7f; - final int textWidth = Math.max(wrapper.getWidth(), computeBubbleTimePartWidth(false)); + final float textWidth = Math.max(visibleText.getMetadata().getTotalWidth(), computeBubbleTimePartWidth(false)); forceExpand = replyData == null && (textWidth < (int)(maxWidth * maxWidthMultiply)) && messageReactions.getBubblesCount() > 1 && messageReactions.getHeight() <= TGReactions.getReactionBubbleHeight(); - messageReactions.measureReactionBubbles(Math.max(textWidth, (int)(maxWidth * maxWidthMultiply)), computeBubbleTimePartWidth(true, true)); + messageReactions.measureReactionBubbles(Math.max(Math.round(textWidth), (int) (maxWidth * maxWidthMultiply)), computeBubbleTimePartWidth(true, true)); messageReactions.resetReactionsAnimator(animated); } } @@ -513,14 +665,13 @@ public TGWebPage getParsedWebPage () { @Override public boolean performLongPress (View view, float x, float y) { boolean res = super.performLongPress(view, x, y); - return wrapper.performLongPress(view) || (webPage != null && webPage.performLongPress(view, this)) || res; + TextWrapper wrapper = effectiveWrapper; + return (wrapper != null && wrapper.performLongPress(view)) || (webPage != null && webPage.performLongPress(view, this)) || res; } @Override protected void onMessageContainerDestroyed () { - if (wrapper != null) { - wrapper.performDestroy(); - } + visibleText.clear(false); if (webPage != null) { webPage.performDestroy(); } @@ -530,104 +681,14 @@ protected void onMessageContainerDestroyed () { @Override public boolean onTouchEvent (MessageView view, MotionEvent e) { - return super.onTouchEvent(view, e) || wrapper.onTouchEvent(view, e) || (webPage != null && webPage.onTouchEvent(view, e, getContentX(), getWebY(), clickCallback())); - } - - // Sponsor-related stuff - // TODO: better be separated in a different object - - @Override - public boolean isSponsored () { - return sponsoredMetadata != null; - } - - public boolean isBotSponsor () { - return isSponsored() && sponsoredMetadata.link != null && sponsoredMetadata.link.getConstructor() == TdApi.InternalLinkTypeBotStart.CONSTRUCTOR; - } - - public int getSponsorButtonName () { - if (!isSponsored() || sponsoredMetadata.link == null) { - return R.string.OpenChannel; - } - - switch (sponsoredMetadata.link.getConstructor()) { - case TdApi.InternalLinkTypeMessage.CONSTRUCTOR: { - return R.string.OpenMessage; - } - - case TdApi.InternalLinkTypeBotStart.CONSTRUCTOR: { - return R.string.OpenBot; - } - - default: { - return R.string.OpenChannel; - } - } - } - - public String getSponsoredButtonUrl () { - if (!isSponsored() || sponsoredMetadata.link == null) { - return tdlib.tMeUrl(tdlib.chatUsername(getSponsorChatId())); - } - - switch (sponsoredMetadata.link.getConstructor()) { - case TdApi.InternalLinkTypeMessage.CONSTRUCTOR: { - TdApi.InternalLinkTypeMessage link = (TdApi.InternalLinkTypeMessage) sponsoredMetadata.link; - return link.url; - } - - case TdApi.InternalLinkTypeBotStart.CONSTRUCTOR: { - TdApi.InternalLinkTypeBotStart link = (TdApi.InternalLinkTypeBotStart) sponsoredMetadata.link; - return tdlib.tMeStartUrl(link.botUsername, link.startParameter, false); - } - - default: { - return tdlib.tMeUrl(tdlib.chatUsername(getSponsorChatId())); - } - } - } - - public void callSponsorButton () { - if (!isSponsored()) { - return; - } - - long sponsorId = getSponsorChatId(); - - if (sponsoredMetadata.link == null) { - tdlib.ui().openChat(this, sponsorId, new TdlibUi.ChatOpenParameters().keepStack()); - return; + if (super.onTouchEvent(view, e)) { + return true; } - - switch (sponsoredMetadata.link.getConstructor()) { - case TdApi.InternalLinkTypeMessage.CONSTRUCTOR: { - TdApi.InternalLinkTypeMessage link = (TdApi.InternalLinkTypeMessage) sponsoredMetadata.link; - tdlib.client().send(new TdApi.GetMessageLinkInfo(link.url), messageLinkResult -> { - if (messageLinkResult.getConstructor() == TdApi.MessageLinkInfo.CONSTRUCTOR) { - TdApi.MessageLinkInfo messageLinkInfo = (TdApi.MessageLinkInfo) messageLinkResult; - tdlib.ui().post(() -> { - tdlib.ui().openMessage(this, messageLinkInfo, null); - }); - } - }); - break; - } - - case TdApi.InternalLinkTypeBotStart.CONSTRUCTOR: { - TdApi.InternalLinkTypeBotStart link = (TdApi.InternalLinkTypeBotStart) sponsoredMetadata.link; - tdlib.ui().openChat(this, sponsorId, new TdlibUi.ChatOpenParameters().shareItem(new TGBotStart(sponsorId, link.startParameter, false)).keepStack()); - break; - } - - default: { - tdlib.ui().openChat(this, sponsorId, new TdlibUi.ChatOpenParameters().keepStack()); - break; - } + TextWrapper wrapper = effectiveWrapper; + if (wrapper != null && wrapper.onTouchEvent(view, e)) { + return true; } - } - - public long getSponsorChatId () { - return isSponsored() ? sponsoredMetadata.sponsorChatId : 0; + return (webPage != null && webPage.onTouchEvent(view, e, getContentX(), getWebY(), clickCallback())); } private TdApi.FormattedText translatedText; @@ -641,7 +702,7 @@ public TdApi.FormattedText getTextToTranslateImpl () { @Override protected void setTranslationResult (@Nullable TdApi.FormattedText text) { translatedText = text; - setText(this.text, false, true); + setText(this.text, false, true, true); rebuildAndUpdateContent(); invalidateTextMediaReceiver(); super.setTranslationResult(text); diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageVideo.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageVideo.java index 512ead076e..6169eccff1 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageVideo.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageVideo.java @@ -403,7 +403,7 @@ protected void drawOverlay (MessageView view, Canvas c, int startX, int startY, restoreToCount = -1; } c.drawCircle(centerX, centerY, radius, Paints.fillingPaint(ColorUtils.alphaColor(alpha, 0x40000000))); - Paint paint = Paints.getPorterDuffPaint(0xffffffff); + Paint paint = Paints.whitePorterDuffPaint(); paint.setAlpha((int) (255f * alpha)); Drawable drawable = view.getSparseDrawable(R.drawable.deproko_baseline_sound_muted_24, 0); Drawables.draw(c, drawable, centerX - drawable.getMinimumWidth() / 2f, centerY - drawable.getMinimumHeight() / 2f, paint); diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGReaction.java b/app/src/main/java/org/thunderdog/challegram/data/TGReaction.java index 723dc9eba6..d2901bfe09 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGReaction.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGReaction.java @@ -74,6 +74,10 @@ private void initialize () { loadAllAnimationsAndCache(); } + public boolean needThemedColorFilter () { + return TD.needThemedColorFilter(customReaction); + } + public boolean isPremium () { return isCustom(); } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGReactions.java b/app/src/main/java/org/thunderdog/challegram/data/TGReactions.java index 5c22a3e6b0..20d77923a1 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGReactions.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGReactions.java @@ -27,6 +27,9 @@ import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Strings; +import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.util.ReactionsListAnimator; import org.thunderdog.challegram.util.text.Counter; import org.thunderdog.challegram.util.text.TextColorSet; @@ -36,12 +39,14 @@ import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import me.vkryl.android.AnimatorUtils; import me.vkryl.android.ViewUtils; import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.core.ArrayUtils; import me.vkryl.core.BitwiseUtils; import me.vkryl.core.ColorUtils; import me.vkryl.core.lambda.Destroyable; @@ -50,7 +55,6 @@ public class TGReactions implements Destroyable, ReactionLoadListener { private final Tdlib tdlib; private TdApi.MessageReaction[] reactions; - private ComplexReceiver complexReceiver; private final TGMessage parent; @@ -85,13 +89,10 @@ public class TGReactions implements Destroyable, ReactionLoadListener { resetReactionsAnimator(false); } - public void setReceiversPool (ComplexReceiver complexReceiver) { - // FIXME: single TGMessage may be displayed in multiple MessageView at once. - // This class wrongly relies that it cannot. - this.complexReceiver = complexReceiver; + public void requestReactionFiles (ComplexReceiver complexReceiver) { for (Map.Entry pair : reactionsMapEntry.entrySet()) { MessageReactionEntry entry = pair.getValue(); - entry.setComplexReceiver(complexReceiver); + entry.requestReactionFiles(complexReceiver); } } @@ -150,12 +151,15 @@ public void setReactions (ArrayList combinedMessages) { for (TdApi.MessageReaction reaction : message.interactionInfo.reactions) { final String reactionKey = TD.makeReactionKey(reaction.type); - TdApi.MessageReaction fakeReaction = reactionsHashMap.get(reactionKey); + TdApi.MessageReaction fakeReaction = reactionsHashMap.get(reactionKey); if (fakeReaction == null) { - fakeReaction = new TdApi.MessageReaction(reaction.type, 0, false, new TdApi.MessageSender[0]); + fakeReaction = new TdApi.MessageReaction(reaction.type, 0, false, null, new TdApi.MessageSender[0]); reactionsHashMap.put(reactionKey, fakeReaction); } fakeReaction.totalCount += reaction.totalCount; + if (reaction.recentSenderIds != null && reaction.recentSenderIds.length > 0) { + fakeReaction.recentSenderIds = reaction.recentSenderIds; // todo conact arrays ? + } fakeReaction.isChosen = reaction.isChosen; totalCount += reaction.totalCount; if (reaction.isChosen) { @@ -197,9 +201,7 @@ private MessageReactionEntry getMessageReactionEntry (TGReaction reactionObj) { .noBackground() .textColor(ColorId.badgeText, ColorId.badgeText, ColorId.badgeText); entry = new MessageReactionEntry(tdlib, delegate, parent, reactionObj, counterBuilder); - if (complexReceiver != null) { - entry.setComplexReceiver(complexReceiver); - } + delegate.onInvalidateReceiversRequested(); reactionsMapEntry.put(reactionObj.key, entry); } else { @@ -218,11 +220,54 @@ public void updateCounterAnimators (boolean animated) { if (reactions == null) { return; } + final int mode = Settings.instance().getReactionAvatarsMode(); + for (TdApi.MessageReaction reaction : reactions) { String reactionKey = TD.makeReactionKey(reaction.type); TGReactions.MessageReactionEntry entry = reactionsMapEntry.get(reactionKey); if (entry != null) { - entry.setCount(reaction.totalCount, reaction.isChosen, animated); + TdApi.MessageSender[] recentSenderIds = getRecentSenderIds(reaction, mode); + recentSenderIds = limitSenders(recentSenderIds, reaction.totalCount > 3 ? 2 : 3); + entry.setCount(recentSenderIds, reaction.totalCount, reaction.isChosen, animated); + } + } + } + + private TdApi.MessageSender[] limitSenders (TdApi.MessageSender[] senders, int maxCount) { + return senders != null && senders.length > maxCount ? Arrays.copyOfRange(senders, 0, maxCount) : senders; + } + + private TdApi.MessageSender[] getRecentSenderIds (TdApi.MessageReaction reaction, int mode) { + if (reaction.recentSenderIds == null || reaction.recentSenderIds.length == 0) + return reaction.recentSenderIds; + if (mode == Settings.REACTION_AVATARS_MODE_NEVER) + return null; + + // Filter out current user/reaction.usedSenderId, unless reaction.isChosen == true + List sendersPreFiltered = ArrayUtils.filter(ArrayUtils.asList(reaction.recentSenderIds), + sender -> !(tdlib.isSelfSender(sender) || Td.equalsTo(reaction.usedSenderId, sender)) || reaction.isChosen + ); + + if (mode == Settings.REACTION_AVATARS_MODE_ALWAYS) { + return sendersPreFiltered.toArray(new TdApi.MessageSender[0]); + } + + final TdApi.FormattedText msgText = parent.getMessageText(); + return ArrayUtils.filter(sendersPreFiltered, (item) -> parent.matchesReactionSenderAvatarFilter(msgText, reaction, item)).toArray(new TdApi.MessageSender[0]); + } + + public void requestAvatarFiles (ComplexReceiver complexReceiver, boolean isUpdate) { + if (reactions == null) { + return; + } + if (!isUpdate) { + complexReceiver.clear(); + } + for (TdApi.MessageReaction reaction : reactions) { + String reactionKey = TD.makeReactionKey(reaction.type); + TGReactions.MessageReactionEntry entry = reactionsMapEntry.get(reactionKey); + if (entry != null) { + entry.requestAvatars(complexReceiver, isUpdate); } } } @@ -290,6 +335,18 @@ public static int getReactionImageSize () { return Screen.dp((TGMessage.reactionsTextStyleProvider().getTextSizeInDp() + 1) * 1.25f + 17); } + public static int getReactionAvatarRadiusDp () { + return (int) ((TGMessage.reactionsTextStyleProvider().getTextSizeInDp() + 1) * 0.625f + 2.5f); + } + + public static int getReactionAvatarOutlineDp () { + return (int) ((TGMessage.reactionsTextStyleProvider().getTextSizeInDp() + 1) / 6f); + } + + public static int getReactionAvatarSpacingDp () { + return (int) -((TGMessage.reactionsTextStyleProvider().getTextSizeInDp() + 1) / 3f); + } + // target values public int getWidth () { @@ -482,7 +539,7 @@ public TdApi.MessageReaction getTdMessageReaction (TdApi.ReactionType reactionTy if (reaction != null) { return reaction; } - return new TdApi.MessageReaction(reactionType, 0, false, new TdApi.MessageSender[0]); + return new TdApi.MessageReaction(reactionType, 0, false, null, new TdApi.MessageSender[0]); } public boolean hasReaction (TdApi.ReactionType reactionType) { @@ -507,12 +564,13 @@ public static class MessageReactionEntry implements TextColorSet, FactorAnimator public static final int TYPE_APPEAR_OPACITY_FLAG = 2; private final Counter counter; + private final TGAvatars avatars; private final TdApi.ReactionType reactionType; private final TGReaction reactionObj; private final TGMessage message; - @Nullable private Receiver staticCenterAnimationReceiver; - @Nullable private GifReceiver centerAnimationReceiver; + @Nullable private Receiver staticCenterAnimationReceiver; // FIXME: single TGMessage may be displayed in multiple MessageView at once. + @Nullable private GifReceiver centerAnimationReceiver; // This class wrongly relies that it cannot. @Nullable private final GifFile animation; private final float animationScale; private final GifFile staticAnimationFile; @@ -537,7 +595,13 @@ public MessageReactionEntry (Tdlib tdlib, MessageReactionsDelegate delegate, TGM this.path = new Path(); this.rect = new RectF(); - this.counter = counter.colorSet(this).build(); + this.counter = counter != null ? counter.colorSet(this).build() : null; + if (message != null) { + this.avatars = new TGAvatars(tdlib, message, message.currentViews); + this.avatars.setDimensions(getReactionAvatarRadiusDp(), getReactionAvatarOutlineDp(), getReactionAvatarSpacingDp()); + } else { + this.avatars = null; + } TGStickerObj stickerObj = reactionObj.newCenterAnimationSicker(); animation = stickerObj.getFullAnimation(); @@ -563,7 +627,7 @@ public MessageReactionEntry (Tdlib tdlib, MessageReactionsDelegate delegate, TGM // Receivers - public void setComplexReceiver (ComplexReceiver complexReceiver) { + public void requestReactionFiles (ComplexReceiver complexReceiver) { if (complexReceiver == null) { this.centerAnimationReceiver = null; this.staticCenterAnimationReceiver = null; @@ -772,7 +836,7 @@ private void forceResetSelection () { } } - private void invalidate () { + public void invalidate () { message.invalidate(); } @@ -798,8 +862,18 @@ public TGReaction getTGReaction () { return reactionObj; } - public void setCount (int count, boolean chosen, boolean animated) { - counter.setCount(count, !chosen, animated); + public void setCount (TdApi.MessageSender[] senders, int count, boolean chosen, boolean animated) { + boolean hasSenders = senders != null && senders.length > 0; + int countToDisplay = count - (hasSenders ? senders.length: 0); + int value = countToDisplay > 0 ? BitwiseUtils.setFlag(countToDisplay, 1 << 30, hasSenders): 0; + String text = hasSenders ? "+" + Strings.buildCounter(countToDisplay): Strings.buildCounter(countToDisplay); + + counter.setCount(value, !chosen, text, animated); + avatars.setSenders(senders, animated); + } + + public void requestAvatars (ComplexReceiver complexReceiver, boolean isUpdate) { + avatars.requestFiles(complexReceiver, isUpdate, true); } // Render @@ -820,6 +894,11 @@ private void drawReceiver (Canvas c, int l, int t, int r, int b, float alpha) { float scale = inAnimation ? animationScale : staticAnimationFile != null ? staticAnimationFileScale : staticImageFileScale; if (receiver != null) { // TODO contour placeholder + if (reactionObj.needThemedColorFilter()) { + receiver.setThemedPorterDuffColorId(ColorId.text); + } else { + receiver.disablePorterDuffColorFilter(); + } receiver.setBounds(l, t, r, b); receiver.setAlpha(alpha); receiver.drawScaled(c, scale); @@ -827,22 +906,23 @@ private void drawReceiver (Canvas c, int l, int t, int r, int b, float alpha) { } public void drawReactionInBubble (MessageView view, Canvas c, float x, float y, float visibility, int appearTypeFlags) { - final boolean hasScaleSaved = visibility != 1f && (BitwiseUtils.hasFlag(appearTypeFlags, TYPE_APPEAR_SCALE_FLAG)); + final boolean hasScale = visibility != 1f && (BitwiseUtils.hasFlag(appearTypeFlags, TYPE_APPEAR_SCALE_FLAG)); final float alpha = BitwiseUtils.hasFlag(appearTypeFlags, TYPE_APPEAR_OPACITY_FLAG) ? visibility : 1f; - c.save(); + final int restoreToCount = Views.save(c); c.translate(x, y); - - if (hasScaleSaved) { - c.save(); + if (hasScale) { c.scale(visibility, visibility, 0, 0); } + int avatarsWidth = (int) avatars.getAnimatedWidth(); + int avatarsOffset = (Screen.dp(2f * avatars.getAvatarsVisibility())); int width = getBubbleWidth(); int height = getBubbleHeight(); int imageSize = getReactionImageSize(); int imgY = (height - imageSize) / 2; - int textX = height + Screen.dp(1); + int avatarsX = height + Screen.dp(1); + int textX = avatarsX + avatarsOffset + avatarsWidth; int radius = height / 2; int backgroundColor = backgroundColor(false); @@ -852,6 +932,7 @@ public void drawReactionInBubble (MessageView view, Canvas c, float x, float y, if (visibility > 0f) { c.drawRoundRect(rect, radius, radius, Paints.fillingPaint( ColorUtils.alphaColor(alpha, backgroundColor))); + avatars.draw(view, c, view.getReactionAvatarsReceiver(), avatarsX, getReactionBubbleHeight() / 2, Gravity.LEFT, alpha); counter.draw(c, textX, getReactionBubbleHeight() / 2f, Gravity.LEFT, alpha, view, ColorId.badgeFailedText); if (!isHidden) { drawReceiver(c, Screen.dp(-1), imgY, Screen.dp(-1) + imageSize, imgY + imageSize, alpha); @@ -885,11 +966,7 @@ public void drawReactionInBubble (MessageView view, Canvas c, float x, float y, //} } - if (hasScaleSaved) { - c.restore(); - } - - c.restore(); + Views.restore(c, restoreToCount); } public void setHidden (boolean isHidden) { @@ -921,13 +998,18 @@ public int getY () { } public int getBubbleWidth () { + float avatarsWidth = avatars.getAnimatedWidth(); + float avatarsOffset = Screen.dp(2f * avatars.getAvatarsVisibility() * counter.getVisibility()); int addW = Screen.dp((TGMessage.reactionsTextStyleProvider().getTextSizeInDp() + 1f) / 3f); - return (int) (counter.getWidth() + getReactionImageSize() + addW); + int subW = Screen.dp(6f - counter.getVisibility() * 6f); + return (int) (counter.getWidth() + getReactionImageSize() + addW - subW + avatarsWidth + avatarsOffset); } public int getBubbleTargetWidth () { + float avatarsWidth = avatars.getTargetWidth(Screen.dp(counter.getVisibilityTarget() ? 2: 0)); int addW = Screen.dp((TGMessage.reactionsTextStyleProvider().getTextSizeInDp() + 1f) / 3f); - return (int) (counter.getTargetWidth() + getReactionImageSize() + addW); + int subW = Screen.dp(counter.getVisibilityTarget() ? 0: 6); + return (int) (counter.getTargetWidth() + getReactionImageSize() + addW - subW + avatarsWidth); } public int getBubbleHeight () { @@ -974,6 +1056,7 @@ public int backgroundColor (boolean isPressed) { public interface MessageReactionsDelegate { default void onClick (View v, MessageReactionEntry entry) {} default void onLongClick (View v, MessageReactionEntry entry) {} + default void onInvalidateReceiversRequested () {} default void onRebuildRequested () {} } @@ -996,4 +1079,4 @@ public void onReactionLoaded (String reactionKey) { delegate.onRebuildRequested(); } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGSource.java b/app/src/main/java/org/thunderdog/challegram/data/TGSource.java index 1c2e29c9e8..e3cac1a344 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGSource.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGSource.java @@ -19,8 +19,8 @@ import androidx.annotation.Nullable; import org.thunderdog.challegram.loader.AvatarReceiver; -import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.Receiver; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.util.text.Text; import org.thunderdog.challegram.util.text.TextPart; @@ -36,7 +36,7 @@ public TGSource (TGMessage msg) { public abstract boolean open (View view, Text text, TextPart part, @Nullable TdlibUi.UrlOpenParameters openParameters, Receiver receiver); public abstract void load (); public abstract String getAuthorName (); - public abstract int getAuthorNameColorId (); + public abstract TdlibAccentColor getAuthorAccentColor (); public abstract void requestAvatar (AvatarReceiver receiver); public abstract void destroy (); diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGSourceChat.java b/app/src/main/java/org/thunderdog/challegram/data/TGSourceChat.java index 7c669ea31b..6592e93359 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGSourceChat.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGSourceChat.java @@ -16,14 +16,13 @@ import android.view.View; -import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.core.Background; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.loader.AvatarReceiver; import org.thunderdog.challegram.loader.Receiver; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.util.text.Text; import org.thunderdog.challegram.util.text.TextPart; @@ -31,7 +30,7 @@ import me.vkryl.core.StringUtils; import me.vkryl.td.MessageId; -public class TGSourceChat extends TGSource implements Client.ResultHandler, Runnable { +public class TGSourceChat extends TGSource implements Runnable { private final long chatId; private final String authorSignature; private final long messageId; @@ -39,14 +38,14 @@ public class TGSourceChat extends TGSource implements Client.ResultHandler, Runn private String title; private TdApi.ChatPhotoInfo photo; - public TGSourceChat (TGMessage msg, TdApi.MessageForwardOriginChannel channel) { + public TGSourceChat (TGMessage msg, TdApi.MessageOriginChannel channel) { super(msg); this.chatId = channel.chatId; this.authorSignature = channel.authorSignature; this.messageId = channel.messageId; } - public TGSourceChat (TGMessage msg, TdApi.MessageForwardOriginChat chat) { + public TGSourceChat (TGMessage msg, TdApi.MessageOriginChat chat) { super(msg); this.chatId = chat.senderChatId; this.authorSignature = chat.authorSignature; @@ -60,13 +59,29 @@ public void load () { if (chat != null) { setChat(chat); } else { - msg.tdlib().client().send(new TdApi.GetChat(chatId), this); + msg.tdlib().send(new TdApi.GetChat(chatId), (remoteChat, error) -> { + if (error != null) { + this.title = Lang.getString(R.string.ChannelPrivate); + this.isReady = true; + this.photo = null; + Background.instance().post(() -> { + msg.rebuildForward(); + msg.postInvalidate(); + }); + } else { + setChat(msg.tdlib().chat(remoteChat.id)); + Background.instance().post(() -> { + msg.rebuildForward(); + msg.postInvalidate(); + }); + } + }); } } } private void setChat (TdApi.Chat chat) { - if (!StringUtils.isEmpty(authorSignature) && !(msg.forceForwardedInfo())) { + if (!StringUtils.isEmpty(authorSignature) && !(msg.forceForwardOrImportInfo())) { this.title = Lang.getString(R.string.format_channelAndSignature, chat.title, authorSignature); } else { this.title = chat.title; @@ -81,34 +96,6 @@ public void run () { msg.postInvalidate(); } - @Override - public void onResult (TdApi.Object object) { - switch (object.getConstructor()) { - case TdApi.Chat.CONSTRUCTOR: { - setChat(msg.tdlib().chat(((TdApi.Chat) object).id)); - Background.instance().post(() -> { - msg.rebuildForward(); - msg.postInvalidate(); - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - this.title = Lang.getString(R.string.ChannelPrivate); - this.isReady = true; - this.photo = null; - Background.instance().post(() -> { - msg.rebuildForward(); - msg.postInvalidate(); - }); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.GetChat.class, TdApi.Chat.class); - break; - } - } - } - @Override public boolean open (View view, Text text, TextPart part, TdlibUi.UrlOpenParameters openParameters, Receiver receiver) { if (chatId != 0) { @@ -133,8 +120,8 @@ public String getAuthorName () { } @Override - public int getAuthorNameColorId () { - return TD.getNameColorId(msg.tdlib.chatAvatarColorId(chatId)); + public TdlibAccentColor getAuthorAccentColor () { + return msg.tdlib().chatAccentColor(chatId); } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGSourceHidden.java b/app/src/main/java/org/thunderdog/challegram/data/TGSourceHidden.java index 5908e80efd..1061f6a6ed 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGSourceHidden.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGSourceHidden.java @@ -20,25 +20,29 @@ import org.thunderdog.challegram.R; import org.thunderdog.challegram.loader.AvatarReceiver; import org.thunderdog.challegram.loader.Receiver; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.util.text.Text; import org.thunderdog.challegram.util.text.TextPart; public class TGSourceHidden extends TGSource { private final String name; + private final TdlibAccentColor accentColor; private final boolean isImported; - public TGSourceHidden (TGMessage msg, TdApi.MessageForwardOriginHiddenUser forward) { - super(msg); - this.name = forward.senderName; - this.isImported = false; - this.isReady = true; + public TGSourceHidden (TGMessage msg, TdApi.MessageOriginHiddenUser forward) { + this(msg, forward.senderName, false); + } + + public TGSourceHidden (TGMessage msg, TdApi.MessageImportInfo messageImport) { + this(msg, messageImport.senderName, true); } - public TGSourceHidden (TGMessage msg, TdApi.MessageForwardOriginMessageImport messageImport) { + private TGSourceHidden (TGMessage msg, String name, boolean isImported) { super(msg); - this.name = messageImport.senderName; - this.isImported = true; + this.name = name; + this.accentColor = msg.tdlib.accentColorForString(name); + this.isImported = isImported; this.isReady = true; } @@ -49,7 +53,7 @@ public boolean open (View view, Text text, TextPart part, TdlibUi.UrlOpenParamet .builder(view, msg.currentViews) .locate(text != null ? (targetView, outRect) -> text.locatePart(outRect, part) : receiver != null ? (targetView, outRect) -> receiver.toRect(outRect) : null) .controller(msg.controller()) - .show(msg.tdlib(), isImported ? R.string.ForwardAuthorImported : R.string.ForwardAuthorHidden) + .show(msg.tdlib(), msg.isImported() ? R.string.ForwardAuthorImported : R.string.ForwardAuthorHidden) .hideDelayed(); return true; } @@ -63,15 +67,19 @@ public String getAuthorName () { } @Override - public int getAuthorNameColorId () { - return TD.getNameColorId(TD.getColorIdForName(name)); + public TdlibAccentColor getAuthorAccentColor () { + if (isImported) { + return accentColor; + } else { + return null; + } } @Override public void requestAvatar (AvatarReceiver receiver) { receiver.requestPlaceholder(msg.tdlib, new AvatarPlaceholder.Metadata( - TD.getColorIdForName(name), + accentColor, isImported ? null : TD.getLetters(name), isImported ? R.drawable.baseline_phone_24 : 0, 0 ), AvatarReceiver.Options.NONE diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGSourceUser.java b/app/src/main/java/org/thunderdog/challegram/data/TGSourceUser.java index 9e910a056b..62314167b8 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGSourceUser.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGSourceUser.java @@ -21,6 +21,7 @@ import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.loader.AvatarReceiver; import org.thunderdog.challegram.loader.Receiver; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibCache; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.util.text.Text; @@ -30,7 +31,7 @@ public class TGSourceUser extends TGSource implements TdlibCache.UserDataChangeL private final long senderUserId; private TdApi.User user; - public TGSourceUser (TGMessage msg, TdApi.MessageForwardOriginUser info) { + public TGSourceUser (TGMessage msg, TdApi.MessageOriginUser info) { super(msg); this.senderUserId = info.senderUserId; } @@ -67,11 +68,9 @@ public void destroy () { @Override public void onUserUpdated (TdApi.User user) { this.user = user; - msg.tdlib().ui().post(() -> { - if (!msg.isDestroyed()) { - msg.rebuildForward(); - msg.postInvalidate(); - } + msg.runOnUiThreadOptional(() -> { + msg.rebuildForward(); + msg.postInvalidate(); }); } @@ -85,8 +84,8 @@ public String getAuthorName () { } @Override - public int getAuthorNameColorId () { - return TD.getNameColorId(msg.tdlib.cache().userAvatarColorId(senderUserId)); + public TdlibAccentColor getAuthorAccentColor () { + return msg.tdlib.cache().userAccentColor(senderUserId); } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGStickerSetInfo.java b/app/src/main/java/org/thunderdog/challegram/data/TGStickerSetInfo.java index b60a2dac65..79724baa0f 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGStickerSetInfo.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGStickerSetInfo.java @@ -15,10 +15,10 @@ package org.thunderdog.challegram.data; import android.graphics.Path; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; @@ -33,7 +33,6 @@ import java.util.ArrayList; import me.vkryl.core.BitwiseUtils; -import me.vkryl.core.StringUtils; import me.vkryl.td.Td; public class TGStickerSetInfo { @@ -42,6 +41,8 @@ public class TGStickerSetInfo { private static final int FLAG_FAVORITE = 1 << 3; private static final int FLAG_TRENDING_EMOJI = 1 << 4; private static final int FLAG_DEFAULT_EMOJI = 1 << 5; + private static final int FLAG_FAKE_CLASSIC_EMOJI = 1 << 6; + private static final int FLAG_COLLAPSABLE_EMOJI = 1 << 7; private final Tdlib tdlib; private final @Nullable TdApi.StickerSetInfo info; @@ -53,6 +54,9 @@ public class TGStickerSetInfo { private int size; private int startIndex; private @Nullable TdApi.StickerSet stickerSet; + private @StringRes int titleRes; + private boolean needThemedColorFilter; + private int fakeClassicEmojiSectionId; private @Nullable ArrayList boundList; private TdApi.Sticker[] allStickers; @@ -78,6 +82,10 @@ public TGStickerSetInfo (Tdlib tdlib, TdApi.Sticker[] stickers, boolean areFavor } public TGStickerSetInfo (Tdlib tdlib, @NonNull TdApi.StickerSetInfo info) { + this(tdlib, info, -1); + } + + public TGStickerSetInfo (Tdlib tdlib, @NonNull TdApi.StickerSetInfo info, int trimToSize) { this.tdlib = tdlib; this.info = info; if (info.thumbnail != null) { @@ -116,6 +124,7 @@ public TGStickerSetInfo (Tdlib tdlib, @NonNull TdApi.StickerSetInfo info) { this.previewOutline = info.covers[0].outline; this.previewWidth = info.covers[0].width; this.previewHeight = info.covers[0].height; + this.needThemedColorFilter = TD.needThemedColorFilter(info.covers[0]); if (Td.isAnimated(info.covers[0].format)) { this.previewImage = null; this.previewAnimation = new GifFile(tdlib, info.covers[0].sticker, info.covers[0].format); @@ -142,12 +151,43 @@ public TGStickerSetInfo (Tdlib tdlib, @NonNull TdApi.StickerSetInfo info) { this.previewAnimation.setOptimizationMode(GifFile.OptimizationMode.STICKER_PREVIEW); this.previewAnimation.setScaleType(ImageFile.FIT_CENTER); } + + if (trimToSize > 0 && info.size > trimToSize) { + this.size = trimToSize; + this.flags |= FLAG_COLLAPSABLE_EMOJI; + } } public TGStickerSetInfo (Tdlib tdlib, TdApi.StickerSet info) { this(tdlib, Td.toStickerSetInfo(info)); } + public static TGStickerSetInfo fromEmojiSection (Tdlib tdlib, int sectionId, int titleRes, int size) { + return new TGStickerSetInfo(tdlib, sectionId, titleRes, size); + } + + private TGStickerSetInfo (Tdlib tdlib, int sectionId, int titleRes, int size) { + this.tdlib = tdlib; + this.info = null; + this.previewAnimation = null; + this.previewImage = null; + this.previewOutline = null; + this.previewWidth = 0; + this.previewHeight = 0; + this.flags = FLAG_FAKE_CLASSIC_EMOJI; + this.titleRes = titleRes; + this.size = size; + this.fakeClassicEmojiSectionId = sectionId; + } + + public int getFakeClassicEmojiSectionId () { + return fakeClassicEmojiSectionId; + } + + public boolean needThemedColorFilter () { + return needThemedColorFilter; + } + public void setBoundList (@Nullable ArrayList list) { this.boundList = list; } @@ -214,6 +254,9 @@ public boolean isDefaultEmoji () { return (flags & FLAG_DEFAULT_EMOJI) != 0; } + public boolean isFakeClassicEmoji () { + return (flags & FLAG_FAKE_CLASSIC_EMOJI) != 0; + } public void setIsTrendingEmoji () { flags |= FLAG_TRENDING_EMOJI; @@ -235,6 +278,10 @@ public boolean isTrending () { return (flags & FLAG_TRENDING) != 0; } + public boolean isCollapsableEmojiSet () { + return BitwiseUtils.hasFlag(flags, FLAG_COLLAPSABLE_EMOJI); + } + @Override public boolean equals (Object obj) { if (obj == null || !(obj instanceof TGStickerSetInfo)) { @@ -262,12 +309,15 @@ public int getEndIndex () { public int getItemCount () { if (isTrending()) { - return 5; + return isEmoji() ? 16 : 5; + } + if (isCollapsableEmojiSet()) { + return size + 1 + (isCollapsed() ? 1 : 0); } if (info != null) { return info.size + 1; } - if (isFavorite()) { + if (isFavorite() || isFakeClassicEmoji()) { return size; } return size + 1; @@ -283,7 +333,7 @@ public TdApi.Sticker[] getAllStickers () { } public void setSize (int size) { - if (info != null) { + if (info != null && !isCollapsableEmojiSet()) { info.size = size; } else { this.size = size; @@ -352,6 +402,10 @@ public boolean isMasks () { return info != null && info.stickerType.getConstructor() == TdApi.StickerTypeMask.CONSTRUCTOR; } + public boolean isEmoji () { + return info != null && info.stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR; + } + public long getId () { return info != null ? info.id : 0; } @@ -377,10 +431,16 @@ public boolean isAnimated () { } public int getSize () { + if (isCollapsableEmojiSet()) { + return size; + } return info != null ? info.size : size; } public int getFullSize () { + if (isCollapsableEmojiSet()) { + return info != null ? info.size : getSize(); + } return allStickers != null ? allStickers.length : getSize(); } @@ -388,11 +448,18 @@ public boolean isCollapsed () { return getFullSize() > getSize(); } + public int getTitleRes () { + return titleRes; + } + public String getName () { return info != null ? info.name : null; } public String getTitle () { - return isDefaultEmoji() ? Lang.getString(R.string.TrendingStatuses): isFavorite() ? "" : isRecent() ? Lang.getString(R.string.RecentStickers) : info != null ? info.title : null; + if (isFakeClassicEmoji()) { + return titleRes != -1 ? Lang.getString(titleRes) : null; + } + return isDefaultEmoji() ? Lang.getString(R.string.TrendingStatuses) : isFavorite() ? "" : isRecent() ? Lang.getString(R.string.RecentStickers) : info != null ? info.title : null; } } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGStorageStats.java b/app/src/main/java/org/thunderdog/challegram/data/TGStorageStats.java index d13c955c33..301e2e6afd 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGStorageStats.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGStorageStats.java @@ -249,7 +249,7 @@ public Entry (Tdlib tdlib, long chatId) { } } else { this.isSecret = false; - this.avatarPlaceholderMetadata = new AvatarPlaceholder.Metadata(); + this.avatarPlaceholderMetadata = new AvatarPlaceholder.Metadata(tdlib.chatAccentColor(chatId)); this.avatarFile = null; } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGUser.java b/app/src/main/java/org/thunderdog/challegram/data/TGUser.java index adbccd13fb..13b95ad361 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGUser.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGUser.java @@ -26,6 +26,7 @@ import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.util.UserProvider; @@ -141,7 +142,7 @@ public boolean needSeparator () { } private void buildContact () { - avatarPlaceholderMetadata = new AvatarPlaceholder.Metadata(TD.getAvatarColorId(rawContactId, tdlib.myUserId()), TD.getLetters(firstName, lastName)); + avatarPlaceholderMetadata = new AvatarPlaceholder.Metadata(TdlibAccentColor.defaultAccentColorForUserId(tdlib, rawContactId), TD.getLetters(firstName, lastName)); updateName(); updateStatus(); } @@ -165,6 +166,7 @@ public void setRole (int role) { public static String getActionDateStatus (Tdlib tdlib, int actionDateSeconds, TdApi.Message viewedMessage) { int stringRes = R.string.viewed; if (viewedMessage != null) { + //noinspection SwitchIntDef switch (viewedMessage.content.getConstructor()) { case TdApi.MessageVoiceNote.CONSTRUCTOR: stringRes = R.string.opened_voice; @@ -209,6 +211,7 @@ public void setCustomStatus (String statusText) { updateStatus(); } else { this.statusText = statusText; + this.statusWidth = U.measureText(statusText, UserView.getStatusPaint()); this.flags |= FLAG_CUSTOM_STATUS_TEXT; this.flags &= ~FLAG_ONLINE; } @@ -260,7 +263,7 @@ public void setUser (@Nullable TdApi.User user, int creatorId) { flags &= ~FLAG_GROUP_CREATOR; } if (user == null || TD.isPhotoEmpty(user.profilePhoto)) { - avatarPlaceholderMetadata = new AvatarPlaceholder.Metadata(TD.getAvatarColorId(user, tdlib.myUserId()), TD.getLetters(user)); + avatarPlaceholderMetadata = new AvatarPlaceholder.Metadata(tdlib.cache().userAccentColor(user), TD.getLetters(user)); } else { imageFile = new ImageFile(tdlib, user.profilePhoto.small); imageFile.setSize(ChatView.getDefaultAvatarCacheSize()); diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGWebPage.java b/app/src/main/java/org/thunderdog/challegram/data/TGWebPage.java index 3ffd3d5022..f736ecceb2 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGWebPage.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGWebPage.java @@ -42,6 +42,7 @@ import org.thunderdog.challegram.mediaview.MediaViewController; import org.thunderdog.challegram.mediaview.data.MediaItem; import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibFilesManager; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.Theme; @@ -128,6 +129,7 @@ private static boolean isTelegramType (int type) { private final TGMessageText parent; private final TdApi.WebPage webPage; private final String url; + private final TdApi.LinkPreviewOptions linkPreviewOptions; private BaseComponent component; private int componentY; @@ -137,7 +139,7 @@ private static boolean isTelegramType (int type) { private @Nullable ArrayList instantItems; private int instantPosition; - public TGWebPage (@NonNull TGMessageText parent, TdApi.WebPage webPage, String url) { + public TGWebPage (@NonNull TGMessageText parent, TdApi.WebPage webPage, String url, @Nullable TdApi.LinkPreviewOptions linkPreviewOptions) { if (paddingLeft == 0) { initSizes(); } @@ -145,6 +147,7 @@ public TGWebPage (@NonNull TGMessageText parent, TdApi.WebPage webPage, String u initPaints(); }*/ this.viewProvider = parent.currentViews; + this.linkPreviewOptions = linkPreviewOptions; this.parent = parent; this.webPage = webPage; this.url = url; @@ -270,71 +273,73 @@ public void buildLayout (int width) { buildHeader(webPage, maxWidth); } - if (needInstantPreview(webPage)) { - if (webPage.animation != null) { - buildGif(webPage, maxWidth); - } else if (webPage.video != null) { - buildVideo(webPage, maxWidth); - } else { - buildPhoto(webPage, maxWidth); + if (webPage.showLargeMedia || mediaWrapper == null) { + if (webPage.duration != 0) { + setDuration(Strings.buildDuration(webPage.duration)); + } else if (webPage.sticker != null && Math.max(webPage.sticker.width, webPage.sticker.height) > STICKER_SIZE_LIMIT) { + setDuration(Strings.buildSize(webPage.sticker.sticker.size)); } - } else { - switch (this.type) { - case TYPE_TELEGRAM_AD: { - break; - } - case TYPE_VIDEO: { - buildVideo(webPage, maxWidth); - break; - } - case TYPE_GIF: { + if (needInstantPreview(webPage)) { + if (webPage.animation != null) { buildGif(webPage, maxWidth); - break; - } - case TYPE_PHOTO: { + } else if (webPage.video != null) { + buildVideo(webPage, maxWidth); + } else { buildPhoto(webPage, maxWidth); - break; } - default: { - if (type == TYPE_TELEGRAM_BACKGROUND) { - String[] partedUrl = url.split("/bg/"); - if (partedUrl.length == 2) { - this.component = new WallpaperComponent(parent, webPage, partedUrl[1]); + } else { + switch (this.type) { + case TYPE_TELEGRAM_AD: { + break; + } + case TYPE_VIDEO: { + buildVideo(webPage, maxWidth); + break; + } + case TYPE_GIF: { + buildGif(webPage, maxWidth); + break; + } + case TYPE_PHOTO: { + buildPhoto(webPage, maxWidth); + break; + } + default: { + if (type == TYPE_TELEGRAM_BACKGROUND) { + String[] partedUrl = url.split("/bg/"); + if (partedUrl.length == 2) { + this.component = new WallpaperComponent(parent, webPage, partedUrl[1]); + } else if (webPage.document != null) { + this.component = new FileComponent(parent, parent.getMessage(), webPage.document); + } else { + this.component = null; + } + } else if (webPage.audio != null) { + this.component = new FileComponent(parent, parent.getMessage(), webPage.audio, null, null); + } else if (webPage.voiceNote != null) { + this.component = new FileComponent(parent, parent.getMessage(), webPage.voiceNote, null, null); } else if (webPage.document != null) { this.component = new FileComponent(parent, parent.getMessage(), webPage.document); } else { this.component = null; } - } else if (webPage.audio != null) { - this.component = new FileComponent(parent, parent.getMessage(), webPage.audio, null, null); - } else if (webPage.voiceNote != null) { - this.component = new FileComponent(parent, parent.getMessage(), webPage.voiceNote, null, null); - } else if (webPage.document != null) { - this.component = new FileComponent(parent, parent.getMessage(), webPage.document); - } else { - this.component = null; - } - if (this.component != null) { - this.component.setViewProvider(viewProvider); - this.component.buildLayout(maxWidth); - if (hasHeader()) { - height += contentPadding; - } - this.componentY = height; - this.height += component.getHeight(); - } else if (isSmallPhotoType(this.type)) { - if (mediaWrapper != null && height < imageY + imageHeight) { - height = imageY + imageHeight; + if (this.component != null) { + this.component.setViewProvider(viewProvider); + this.component.buildLayout(maxWidth); + if (hasHeader()) { + height += contentPadding; + } + this.componentY = height; + this.height += component.getHeight(); + } else if (webPage.video != null) { + buildVideo(webPage, maxWidth); + } else if (webPage.animation != null) { + buildGif(webPage, maxWidth); + } else if (webPage.photo != null || webPage.sticker != null) { + buildPhoto(webPage, maxWidth); } - height += lineAdd; - } else if (webPage.video != null) { - buildVideo(webPage, maxWidth); - } else if (webPage.animation != null) { - buildGif(webPage, maxWidth); - } else if (webPage.photo != null || webPage.sticker != null) { - buildPhoto(webPage, maxWidth); + break; } - break; } } } @@ -499,7 +504,7 @@ public boolean hasMedia () { return description != null && description.hasMedia(); } - public void requestTextMedia (ComplexReceiver receiver, int startKey) { + public void requestTextMedia (ComplexReceiver receiver, long startKey) { if (hasMedia()) { description.requestMedia(receiver, startKey, Integer.MAX_VALUE); } else { @@ -512,18 +517,29 @@ public void requestTextMedia (ComplexReceiver receiver, int startKey) { private Text siteName, title, description; - private void setSmallPhoto (TdApi.Photo photo) { - TdApi.PhotoSize small = Td.findSmallest(photo); - - if (small == null) { - return; + private boolean setSmallMedia () { + if (webPage.sticker != null && (Math.max(webPage.sticker.width, webPage.sticker.height) <= STICKER_SIZE_LIMIT || Td.isAnimated(webPage.sticker.format))) { + // simple animated sticker + return false; // TODO + } else if (webPage.sticker != null) { + setSmallMediaWrapper(new MediaWrapper(parent.context(), parent.tdlib(), TD.convertToPhoto(webPage.sticker), chatId, messageId, parent, false)); + return true; + } else if (webPage.video != null) { + setSmallMediaWrapper(new MediaWrapper(parent.context(), parent.tdlib(), webPage.video, chatId, messageId, parent, false)); + return true; + } else if (webPage.photo != null) { + setSmallMediaWrapper(new MediaWrapper(parent.context(), parent.tdlib(), webPage.photo, chatId, messageId, parent, false, false, EmbeddedService.parse(webPage))); + return true; } + return false; + } + private void setSmallMediaWrapper (MediaWrapper mediaWrapper) { isImageBig = false; - - mediaWrapper = new MediaWrapper(parent.context(), parent.tdlib(), photo, chatId, messageId, parent, false, false, EmbeddedService.parse(webPage)); - mediaWrapper.setViewProvider(viewProvider); - mediaWrapper.setHideLoader(true); + setMediaWrapper(mediaWrapper); + if (!mediaWrapper.isVideo()) { + mediaWrapper.setHideLoader(true); + } mediaWrapper.setOnClickListener(this); mediaWrapper.buildContent(imageSize, imageSize); mediaWrapper.setViewProvider(viewProvider); @@ -569,7 +585,7 @@ public boolean open (View view, boolean allowRipple) { break; } case TYPE_TELEGRAM_AD: { - parent.callSponsorButton(); + parent.openSponsoredMessage(); break; } case TGWebPage.TYPE_PHOTO: @@ -601,6 +617,10 @@ public TdApi.WebPage getWebPage () { return webPage; } + public @Nullable TdApi.LinkPreviewOptions getLinkPreviewOptions () { + return linkPreviewOptions; + } + public boolean isPreviewOf (String url) { return isPreviewOf(webPage.url, url); } @@ -650,18 +670,17 @@ private static boolean isSmallPhotoType (int type) { return false; } - private static float TEXT_PADDING = 4f, TEXT_PADDING_START = 2f; + private static final float TEXT_PADDING = 4f, TEXT_PADDING_START = 2f; private void buildHeader (TdApi.WebPage webPage, int maxWidth) { final int textMaxWidth; - if (!needInstantPreview(webPage) && isSmallPhotoType(type) && !TD.isPhotoEmpty(webPage.photo)) { + int minHeight = 0; + if (!webPage.showLargeMedia && setSmallMedia()) { textMaxWidth = maxWidth - imageMarginLeft - imageSize; imageX = availWidth - imageSize; imageY = imageOffset; imageWidth = imageHeight = imageSize; - if (mediaWrapper == null) { - setSmallPhoto(webPage.photo); - } + minHeight = imageY + imageHeight + lineAdd; } else { textMaxWidth = maxWidth; } @@ -754,6 +773,7 @@ private void buildHeader (TdApi.WebPage webPage, int maxWidth) { } height += textHeight; + height = Math.max(height, minHeight); if (component != null) { height += component.getHeight(); @@ -856,12 +876,11 @@ private void setBigPhoto (int maxWidth, int topY, int bottomY) { height += contentHeight; } else { if (webPage.sticker != null) { - mediaWrapper = new MediaWrapper(parent.context(), parent.tdlib(), TD.convertToPhoto(webPage.sticker), chatId, messageId, parent, false); - setDuration(Strings.buildSize(webPage.sticker.sticker.size)); + setMediaWrapper(new MediaWrapper(parent.context(), parent.tdlib(), TD.convertToPhoto(webPage.sticker), chatId, messageId, parent, false)); } else if (webPage.video != null) { - mediaWrapper = new MediaWrapper(parent.context(), parent.tdlib(), webPage.video, chatId, messageId, parent, false); + setMediaWrapper(new MediaWrapper(parent.context(), parent.tdlib(), webPage.video, chatId, messageId, parent, false)); } else if (webPage.photo != null) { - mediaWrapper = new MediaWrapper(parent.context(), parent.tdlib(), webPage.photo, chatId, messageId, parent, false, false, EmbeddedService.parse(webPage)); + setMediaWrapper(new MediaWrapper(parent.context(), parent.tdlib(), webPage.photo, chatId, messageId, parent, false, false, EmbeddedService.parse(webPage))); } else { throw new NullPointerException(); } @@ -905,9 +924,6 @@ private boolean hasHeader () { private void buildVideo (final TdApi.WebPage webPage, int maxWidth) { if (webPage.video != null || webPage.photo != null) { - if (webPage.duration != 0) { - setDuration(Strings.buildDuration(webPage.duration)); - } if (hasHeader()) { setBigPhoto(maxWidth, contentPadding, contentPadding + lineAdd); } else { @@ -918,10 +934,18 @@ private void buildVideo (final TdApi.WebPage webPage, int maxWidth) { // GIF + private void setMediaWrapper (MediaWrapper mediaWrapper) { + MediaWrapper oldMediaWrapper = this.mediaWrapper; + this.mediaWrapper = mediaWrapper; + if (oldMediaWrapper != null) { + oldMediaWrapper.destroy(); + } + } + private void buildGif (TdApi.WebPage webPage, int maxWidth) { TdApi.Animation gif = webPage.animation; - mediaWrapper = new MediaWrapper(parent.context(), parent.tdlib(), gif, chatId, messageId, parent, false, false, false, EmbeddedService.parse(webPage)); + setMediaWrapper(new MediaWrapper(parent.context(), parent.tdlib(), gif, chatId, messageId, parent, false, false, false, EmbeddedService.parse(webPage))); mediaWrapper.setOnClickListener(this); mediaWrapper.setViewProvider(viewProvider); int maxHeight = parent.getSmallestMaxContentHeight(); @@ -957,48 +981,48 @@ private void buildPhoto (TdApi.WebPage webPage, int maxWidth) { private TGInlineKeyboard rippleButton; private void buildRippleButton () { - int message = 0; + int stringRes = 0; int icon = 0; if (needInstantView()) { - message = R.string.InstantView; + stringRes = R.string.InstantView; icon = R.drawable.deproko_baseline_instantview_24; } else { switch (type) { case TYPE_TELEGRAM_USER: - message = R.string.OpenProfile; + stringRes = R.string.OpenProfile; break; case TYPE_TELEGRAM_MESSAGE: case TYPE_TELEGRAM_ALBUM: if (parent.tdlib().isTmeUrl(url)) - message = R.string.OpenMessage; + stringRes = R.string.OpenMessage; break; case TYPE_TELEGRAM_CHANNEL: - message = R.string.OpenChannel; + stringRes = R.string.OpenChannel; break; case TYPE_TELEGRAM_MEGAGROUP: - message = R.string.OpenGroup; + stringRes = R.string.OpenGroup; break; case TYPE_TELEGRAM_BOT: - message = R.string.OpenBot; + stringRes = R.string.OpenBot; break; case TYPE_TELEGRAM_AD: - message = parent.getSponsorButtonName(); + stringRes = parent.getSponsoredMessageButtonResId(); break; case TYPE_TELEGRAM_CHAT: - message = R.string.OpenChat; + stringRes = R.string.OpenChat; break; case TYPE_TELEGRAM_BACKGROUND: - message = R.string.ChatBackgroundView; + stringRes = R.string.ChatBackgroundView; break; } } - if (message != 0) { + if (stringRes != 0) { rippleButtonY = height + Screen.dp(6f); height = rippleButtonY + TGInlineKeyboard.getButtonHeight(); rippleButton = new TGInlineKeyboard(parent, false); - rippleButton.setCustom(icon, Lang.getString(message), availWidth - paddingLeft, type != TYPE_TELEGRAM_AD, this); + rippleButton.setCustom(icon, Lang.getString(stringRes), availWidth - paddingLeft, type != TYPE_TELEGRAM_AD, this); } } @@ -1008,7 +1032,7 @@ public void onClick (View view, TGInlineKeyboard keyboard, TGInlineKeyboard.Butt button.makeActive(); button.showProgressDelayed(); String anchor = parent.findUriFragment(webPage); - parent.tdlib().client().send(new TdApi.GetWebPageInstantView(url, false), getInstantViewCallback(view, button, webPage, anchor)); + parent.tdlib().send(new TdApi.GetWebPageInstantView(url, false), getInstantViewCallback(view, button, webPage, anchor)); } else { open(view, false); } @@ -1023,16 +1047,11 @@ public boolean onLongClick (View view, TGInlineKeyboard keyboard, TGInlineKeyboa return false; } - String url; - - if (parent.isSponsored()) { - url = parent.getSponsoredButtonUrl(); - } else { - String username = parent.tdlib.chatUsername(parent.getSponsorChatId()); - url = parent.tdlib.tMeUrl(username); + String url = parent.getSponsoredMessageUrl(); + if (!StringUtils.isEmpty(url)) { + c.showCopyUrlOptions(url, parent.openParameters(), null); + return true; } - - c.showCopyUrlOptions(url, parent.openParameters(), null); } return false; @@ -1048,7 +1067,7 @@ public void autodownloadContent (TdApi.ChatType info) { } } - public void requestPreview (DoubleImageReceiver receiver, int startX, int startY) { + public void requestPreview (DoubleImageReceiver receiver) { if (simpleImageFile != null || simpleGifFile != null) { receiver.requestFile(null, simplePreview); } else if (mediaWrapper != null) { @@ -1290,83 +1309,74 @@ private static void initSizes () { // Instant view - private Client.ResultHandler getInstantViewCallback (final View view, final TGInlineKeyboard.Button button, final TdApi.WebPage instantViewSource, final String anchor) { + private void runOnUiThread (Runnable runnable) { + parent.tdlib().ui().post(runnable); + } + + private Tdlib.ResultHandler getInstantViewCallback (final View view, final TGInlineKeyboard.Button button, final TdApi.WebPage instantViewSource, final String anchor) { final int currentContextId = button.getContextId(); final boolean[] signal = new boolean[1]; - return new Client.ResultHandler() { + return new Tdlib.ResultHandler<>() { @Override - public void onResult (TdApi.Object object) { - switch (object.getConstructor()) { - case TdApi.WebPageInstantView.CONSTRUCTOR: { - final TdApi.WebPageInstantView instantView = (TdApi.WebPageInstantView) object; - - if (!TD.hasInstantView(instantView.version)) { - parent.tdlib().ui().post(() -> { - if (currentContextId == button.getContextId()) { - button.makeInactive(); - button.showTooltip(view, R.string.InstantViewUnsupported); - } - }); - break; - } - - if (instantView.pageBlocks == null || instantView.pageBlocks.length == 0) { - boolean retry = !signal[0] && !instantView.isFull; - if (retry) { - signal[0] = true; - parent.tdlib().client().send(new TdApi.GetWebPageInstantView(instantViewSource.url, false), this); - } else { - parent.tdlib().ui().post(() -> { - if (currentContextId == button.getContextId()) { - button.makeInactive(); - button.showTooltip(view, "TDLib: instantView.pageBlocks returned null " + (signal[0] ? "twice isFull == " + instantView.isFull : "with isFull == " + instantView.isFull)); - } - }); - } - - break; + public void onResult (TdApi.WebPageInstantView instantView, @Nullable TdApi.Error error) { + if (error != null) { + runOnUiThread(() -> { + if (currentContextId != button.getContextId()) { + return; } + button.makeInactive(); + button.showTooltip(view, TD.toErrorString(error)); + }); + return; + } - parent.tdlib().ui().post(() -> { - if (currentContextId != button.getContextId()) { - return; - } - + if (!TD.hasInstantView(instantView.version)) { + parent.tdlib().ui().post(() -> { + if (currentContextId == button.getContextId()) { button.makeInactive(); + button.showTooltip(view, R.string.InstantViewUnsupported); + } + }); + return; + } - InstantViewController controller = new InstantViewController(parent.controller().context(), parent.tdlib()); - controller.setArguments(new InstantViewController.Args(instantViewSource, instantView, anchor)); - try { - controller.show(); - } catch (UnsupportedOperationException e) { - Log.w("Unsupported Instant View block:%s", e, instantViewSource.url); - button.showTooltip(view, R.string.InstantViewUnsupported); - controller.destroy(); - } catch (Throwable t) { - Log.e("Unable to open Instant View, url:%s", t, instantViewSource.url); - button.showTooltip(view, R.string.InstantViewError); - controller.destroy(); + if (instantView.pageBlocks == null || instantView.pageBlocks.length == 0) { + boolean retry = !signal[0] && !instantView.isFull; + if (retry) { + signal[0] = true; + parent.tdlib().send(new TdApi.GetWebPageInstantView(instantViewSource.url, false), this); + } else { + runOnUiThread(() -> { + if (currentContextId == button.getContextId()) { + button.makeInactive(); + button.showTooltip(view, "TDLib: instantView.pageBlocks returned null " + (signal[0] ? "twice isFull == " + instantView.isFull : "with isFull == " + instantView.isFull)); } }); - - break; } - case TdApi.Error.CONSTRUCTOR: { - parent.tdlib().ui().post(() -> { - if (currentContextId != button.getContextId()) { - return; - } - button.makeInactive(); - button.showTooltip(view, TD.toErrorString(object)); - }); - break; + return; + } + + runOnUiThread(() -> { + if (currentContextId != button.getContextId()) { + return; } - default: { - Log.unexpectedTdlibResponse(object, TdApi.GetWebPageInstantView.class, TdApi.WebPageInstantView.class); - parent.tdlib().ui().post(button::makeInactive); - break; + + button.makeInactive(); + + InstantViewController controller = new InstantViewController(parent.controller().context(), parent.tdlib()); + controller.setArguments(new InstantViewController.Args(instantViewSource, instantView, anchor)); + try { + controller.show(); + } catch (UnsupportedOperationException e) { + Log.w("Unsupported Instant View block:%s", e, instantViewSource.url); + button.showTooltip(view, R.string.InstantViewUnsupported); + controller.destroy(); + } catch (Throwable t) { + Log.e("Unable to open Instant View, url:%s", t, instantViewSource.url); + button.showTooltip(view, R.string.InstantViewError); + controller.destroy(); } - } + }); } }; } diff --git a/app/src/main/java/org/thunderdog/challegram/data/ThreadInfo.java b/app/src/main/java/org/thunderdog/challegram/data/ThreadInfo.java index e8287fe5af..5a754b9fd9 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/ThreadInfo.java +++ b/app/src/main/java/org/thunderdog/challegram/data/ThreadInfo.java @@ -24,9 +24,8 @@ import org.thunderdog.challegram.telegram.MessageThreadListener; import org.thunderdog.challegram.telegram.Tdlib; -import java.util.Objects; - import me.vkryl.core.BitwiseUtils; +import me.vkryl.core.ObjectUtils; import me.vkryl.core.reference.ReferenceList; import me.vkryl.td.MessageId; import me.vkryl.td.Td; @@ -76,7 +75,8 @@ private ThreadInfo (@Nullable Tdlib tdlib, @NonNull TdApi.MessageThreadInfo thre } @Override public int hashCode () { - return Objects.hash(areComments(), contextChatId, threadInfo.chatId, threadInfo.messageThreadId); + Object[] objects = new Object[] {areComments(), contextChatId, threadInfo.chatId, threadInfo.messageThreadId}; + return ObjectUtils.hashCode(objects); } public boolean belongsTo (long chatId, long messageThreadId) { @@ -87,8 +87,13 @@ public boolean hasMessages () { return threadInfo.messages != null && threadInfo.messages.length > 0; } - public boolean isRootMessage (long messageId) { - return getMessage(messageId) != null; + public boolean isRootMessage (@Nullable TdApi.MessageReplyTo replyTo) { + if (replyTo != null && replyTo.getConstructor() == TdApi.MessageReplyToMessage.CONSTRUCTOR) { + TdApi.MessageReplyToMessage replyToMessage = (TdApi.MessageReplyToMessage) replyTo; + TdApi.Message message = getMessage(replyToMessage.messageId); + return message != null && message.chatId == replyToMessage.chatId; + } + return false; } public @Nullable TdApi.Message getMessage (long messageId) { @@ -193,11 +198,11 @@ public long getLastMessageId () { } public void setDraft (@Nullable TdApi.DraftMessage draftMessage) { - long replyToMessageId = draftMessage != null ? draftMessage.replyToMessageId : 0; - if (replyToMessageId != 0) { + TdApi.InputMessageReplyToMessage replyToMessage = draftMessage != null && draftMessage.replyTo instanceof TdApi.InputMessageReplyToMessage ? ((TdApi.InputMessageReplyToMessage) draftMessage.replyTo) : null; + if (replyToMessage != null && Td.isEmpty(replyToMessage.quote)) { for (TdApi.Message message : threadInfo.messages) { - if (message.id == replyToMessageId) { - draftMessage.replyToMessageId = 0; + if (message.chatId == replyToMessage.chatId && message.id == replyToMessage.messageId) { + draftMessage.replyTo = null; break; } } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TranslationsManager.java b/app/src/main/java/org/thunderdog/challegram/data/TranslationsManager.java index 0f0cc6abee..dbb8ae7c72 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TranslationsManager.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TranslationsManager.java @@ -10,6 +10,7 @@ import java.util.HashMap; import me.vkryl.core.StringUtils; +import me.vkryl.td.Td; public final class TranslationsManager { @@ -52,11 +53,28 @@ public void stopTranslation () { requestTranslation(null); } + private int lastDispatchedStatus = TranslationCounterDrawable.TRANSLATE_STATUS_DEFAULT; + private TdApi.FormattedText lastDispatchedResult; + + private void dispatchStatus (int status, boolean animated) { + if (lastDispatchedStatus != status) { + this.lastDispatchedStatus = status; + statusDelegate.setTranslatedStatus(status, animated); + } + } + + private void dispatchResult (int status, @Nullable TdApi.FormattedText result, boolean animated) { + dispatchStatus(status, animated); + if (!Td.equalsTo(lastDispatchedResult, result)) { + this.lastDispatchedResult = result; + resultDelegate.setTranslationResult(result); + } + } + public void requestTranslation (String language) { currentTranslatedLanguage = language; if (language == null || StringUtils.equalsOrBothEmpty(language, message.getOriginalMessageLanguage())) { - statusDelegate.setTranslatedStatus(TranslationCounterDrawable.TRANSLATE_STATUS_DEFAULT, true); - resultDelegate.setTranslationResult(null); + dispatchResult(TranslationCounterDrawable.TRANSLATE_STATUS_DEFAULT, null, true); currentTranslatedLanguage = null; return; } @@ -70,23 +88,21 @@ public void requestTranslation (String language) { TdApi.FormattedText cachedText = getCachedTextTranslation(textToTranslate.text, language); if (cachedText != null) { - statusDelegate.setTranslatedStatus(TranslationCounterDrawable.TRANSLATE_STATUS_SUCCESS, true); - resultDelegate.setTranslationResult(cachedText); + dispatchResult(TranslationCounterDrawable.TRANSLATE_STATUS_SUCCESS, cachedText, true); return; } - statusDelegate.setTranslatedStatus(TranslationCounterDrawable.TRANSLATE_STATUS_LOADING, true); + dispatchStatus(TranslationCounterDrawable.TRANSLATE_STATUS_LOADING, true); tdlib.ui().post(() -> requestTranslationImpl(textToTranslate, language, object -> tdlib.ui().post(() -> { if (object instanceof TdApi.FormattedText) { TdApi.FormattedText text = prepareTranslatedText((TdApi.FormattedText) object); saveCachedTextTranslation(textToTranslate.text, language, text); if (StringUtils.equalsOrBothEmpty(currentTranslatedLanguage, language)) { - statusDelegate.setTranslatedStatus(TranslationCounterDrawable.TRANSLATE_STATUS_SUCCESS, true); - resultDelegate.setTranslationResult(text); + dispatchResult(TranslationCounterDrawable.TRANSLATE_STATUS_SUCCESS, text, true); } } else { if (StringUtils.equalsOrBothEmpty(currentTranslatedLanguage, language)) { - statusDelegate.setTranslatedStatus(TranslationCounterDrawable.TRANSLATE_STATUS_ERROR, true); + dispatchStatus(TranslationCounterDrawable.TRANSLATE_STATUS_ERROR, true); if (object instanceof TdApi.Error) { errorDelegate.onError(TD.toErrorString(object)); } @@ -124,7 +140,7 @@ public TranslatedCachedValue (String originalLanguage) { public @Nullable String getCachedTextLanguage (String text) { TranslatedCachedValue cachedValue = mTranslationsCache2.get(text); - return (cachedValue != null ? cachedValue.originalLanguage: null); + return (cachedValue != null ? cachedValue.originalLanguage : null); } public void saveCachedTextLanguage (String text, String language) { @@ -135,7 +151,7 @@ public void saveCachedTextLanguage (String text, String language) { public @Nullable TdApi.FormattedText getCachedTextTranslation (String text, String language) { TranslatedCachedValue cachedValue = mTranslationsCache2.get(text); - return (cachedValue != null ? cachedValue.translationsCache.get(language): null); + return (cachedValue != null ? cachedValue.translationsCache.get(language) : null); } public void saveCachedTextTranslation (String text, String language, TdApi.FormattedText translated) { diff --git a/app/src/main/java/org/thunderdog/challegram/data/UserContext.java b/app/src/main/java/org/thunderdog/challegram/data/UserContext.java index e94249c310..271cff604c 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/UserContext.java +++ b/app/src/main/java/org/thunderdog/challegram/data/UserContext.java @@ -26,8 +26,7 @@ import org.thunderdog.challegram.component.dialogs.ChatView; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.telegram.Tdlib; -import org.thunderdog.challegram.theme.ColorId; -import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.util.text.Letters; @@ -43,10 +42,8 @@ public class UserContext { private @Nullable ImageFile imageFile; - private @ColorId - int avatarColorId; - private @Nullable - Letters letters; + private TdlibAccentColor accentColor; + private @Nullable Letters letters; private int lettersWidth; private int nameWidth; @@ -58,7 +55,7 @@ public UserContext (Tdlib tdlib, long userId) { if (user != null) { set(user); } else { - this.avatarColorId = TD.getAvatarColorId(-1, 0); + this.accentColor = tdlib.accentColor(TdlibAccentColor.InternalId.INACTIVE); this.letters = TD.getLetters(); this.fullName = "User#" + userId; } @@ -73,6 +70,7 @@ public UserContext (Tdlib tdlib, @NonNull TdApi.User user) { public void set (TdApi.User user) { this.user = user; this.fullName = TD.getUserName(user.firstName, user.lastName); + this.accentColor = tdlib.cache().userAccentColor(user); if (user.profilePhoto != null) { if (imageFile == null || imageFile.getId() != user.profilePhoto.small.id) { this.imageFile = new ImageFile(tdlib, user.profilePhoto.small); @@ -81,7 +79,6 @@ public void set (TdApi.User user) { this.imageFile.getFile().local.path = user.profilePhoto.small.local.path; } } else { - this.avatarColorId = TD.getAvatarColorId(user.id, tdlib.myUserId()); this.letters = TD.getLetters(user); } } @@ -120,19 +117,15 @@ public ImageFile getImageFile () { return imageFile; } - /*public int getAvatarColor () { - return avatarColor; - }*/ + public TdlibAccentColor getAccentColor () { + return accentColor; + } @Nullable public Letters getLetters () { return letters; } - public int getLettersWidth () { - return lettersWidth; - } - // Drawing-related shit public void measureTexts (float lettersSizeDp, @Nullable TextPaint namePaint) { @@ -178,9 +171,9 @@ public String getFirstName () { } public void drawPlaceholder (Canvas c, int radius, int left, int top, float lettersSize) { - c.drawCircle(left + radius, top + radius, radius, Paints.fillingPaint(Theme.getColor(avatarColorId))); + c.drawCircle(left + radius, top + radius, radius, Paints.fillingPaint(accentColor.getPrimaryColor())); if (letters != null) { - Paints.drawLetters(c, letters, left + radius - lettersWidth / 2, top + radius + Screen.dp(5f), lettersSize); + Paints.drawLetters(c, letters, left + radius - lettersWidth / 2f, top + radius + Screen.dp(5f), lettersSize); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/data/WallpaperComponent.java b/app/src/main/java/org/thunderdog/challegram/data/WallpaperComponent.java index 4aabfbe70a..1253603d17 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/WallpaperComponent.java +++ b/app/src/main/java/org/thunderdog/challegram/data/WallpaperComponent.java @@ -242,10 +242,10 @@ private void drawBackground (Canvas c, TGBackground wallpaper, int startX, int s } else { c.drawColor(ColorUtils.alphaColor(alpha, wallpaper.getBackgroundColor(defaultColor))); } - receiver.setColorFilter(wallpaper.getPatternColor()); + receiver.setPorterDuffColorFilter(wallpaper.getPatternColor()); receiver.setPaintAlpha(alpha * wallpaper.getPatternIntensity()); } else { - receiver.disableColorFilter(); + receiver.disablePorterDuffColorFilter(); if (alpha != 1f) { receiver.setPaintAlpha(alpha); } diff --git a/app/src/main/java/org/thunderdog/challegram/emoji/CustomEmojiSpanImpl.java b/app/src/main/java/org/thunderdog/challegram/emoji/CustomEmojiSpanImpl.java index c59dc8a8f3..32439f893f 100644 --- a/app/src/main/java/org/thunderdog/challegram/emoji/CustomEmojiSpanImpl.java +++ b/app/src/main/java/org/thunderdog/challegram/emoji/CustomEmojiSpanImpl.java @@ -127,11 +127,13 @@ private void drawCustomEmoji (Canvas c) { } else { restoreToCount = -1; } + ComplexReceiver receiver = surfaceProvider.provideComplexReceiverForSpan(this); + boolean haveDuplicateMedia = surfaceProvider.getDuplicateMediaItemCount(this, mediaItem) > 1; mediaItem.draw(c, drawRect, - surfaceProvider.provideComplexReceiverForSpan(this), + receiver, attachedToMediaKey, - surfaceProvider.getDuplicateMediaItemCount(this, mediaItem) > 1 + haveDuplicateMedia ); if (needScale) { Views.restore(c, restoreToCount); diff --git a/app/src/main/java/org/thunderdog/challegram/emoji/Emoji.java b/app/src/main/java/org/thunderdog/challegram/emoji/Emoji.java index f79025b619..b220621fe1 100644 --- a/app/src/main/java/org/thunderdog/challegram/emoji/Emoji.java +++ b/app/src/main/java/org/thunderdog/challegram/emoji/Emoji.java @@ -37,6 +37,7 @@ import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.tool.EmojiCode; import org.thunderdog.challegram.tool.EmojiData; +import org.thunderdog.challegram.tool.Emojis; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; @@ -59,6 +60,9 @@ import me.vkryl.core.util.LocalVar; public class Emoji { + public static final @Deprecated String CUSTOM_EMOJI_CACHE_OLD = "custom_emoji_id_"; + public static final String CUSTOM_EMOJI_CACHE = "_"; + private static Emoji instance; public static Emoji instance () { @@ -74,17 +78,7 @@ public static Emoji instance () { private final HashMap rects; private final ReferenceList emojiChangeListeners = new ReferenceList<>(); - private final CountLimiter singleLimiter = new org.thunderdog.challegram.emoji.Emoji.CountLimiter() { - @Override - public int getEmojiCount () { - return 0; - } - - @Override - public boolean incrementEmojiCount () { - return false; - } - }; + private final CountLimiter singleLimiter = newSingleLimiter(); public static boolean equals (String a, String b) { int end1 = a.length(); @@ -117,6 +111,7 @@ public void removeEmojiChangeListener (EmojiChangeListener listener) { } public final int emojiOriginalSize; + public final int sampleSize; private Emoji () { this.bitmaps = new EmojiBitmaps(Settings.instance().getEmojiPackIdentifier()); @@ -128,8 +123,8 @@ private Emoji () { this.defaultTone = Settings.instance().getEmojiDefaultTone(); - final int sampleSize = EmojiBitmaps.calculateSampleSize(); - emojiOriginalSize = (int) (30 * EmojiCode.SCALE) / sampleSize; + this.sampleSize = EmojiBitmaps.calculateSampleSize(); + this.emojiOriginalSize = (int) (30 * EmojiCode.SCALE) / sampleSize; int totalCount = EmojiData.getTotalDataCount(); this.rects = new HashMap<>(totalCount); @@ -229,26 +224,49 @@ public void install (Settings.EmojiPack emojiPack, @NonNull RunnableBool callbac public interface EmojiChangeListener { void moveEmoji (int oldIndex, int newIndex); void addEmoji (int newIndex, RecentEmoji emoji); + void removeEmoji (int oldIndex, RecentEmoji emoji); void replaceEmoji (int newIndex, RecentEmoji emoji); void onToneChanged (@Nullable String newDefaultTone); void onCustomToneApplied (String emoji, @Nullable String newTone, @Nullable String[] newOtherTones); } - public void saveRecentEmoji (String emoji) { - int time = (int) (System.currentTimeMillis() / 1000l); - getRecents(); + public void saveRecentCustomEmoji (long id) { + saveRecentEmoji(Emoji.CUSTOM_EMOJI_CACHE + id); + } - // emoji = fixEmoji(emoji); + public boolean removeRecentCustomEmoji (long id) { + return removeRecentEmoji(Emoji.CUSTOM_EMOJI_CACHE + id); + } + + public boolean removeRecentEmoji (String emoji) { + int oldIndex = indexOfRecentEmoji(emoji); + if (oldIndex == -1 || recents.size() == 1) { + return false; + } + RecentEmoji recentEmoji = recents.remove(oldIndex); + for (EmojiChangeListener listener : emojiChangeListeners) { + listener.removeEmoji(oldIndex, recentEmoji); + } + saveRecents(true); + return true; + } - int oldIndex = -1; - int i = 0; - for (RecentEmoji oldEmoji : recents) { - if (oldEmoji.emoji.equals(emoji)) { - oldIndex = i; - break; + private int indexOfRecentEmoji (String emoji) { + getRecents(); + int index = 0; + for (RecentEmoji recentEmoji : recents) { + if (recentEmoji.emoji.equals(emoji)) { + return index; } - i++; + index++; } + return -1; + } + + public void saveRecentEmoji (String emoji) { + int time = (int) (System.currentTimeMillis() / 1000l); + + int oldIndex = indexOfRecentEmoji(emoji); boolean changed; @@ -496,20 +514,57 @@ public String[] otherTonesForEmoji (String emojiCode) { // emoji + public static EmojiSpan findPrecedingEmojiSpan (Spanned spanned, int end) { + int next; + for (int i = Math.max(0, end - Emojis.MAX_EMOJI_LENGTH); i < end; i = next) { + next = spanned.nextSpanTransition(i, end, EmojiSpan.class); + if (next != end) { + continue; + } + EmojiSpan[] emojiSpans = spanned.getSpans(i, next, EmojiSpan.class); + if (emojiSpans != null) { + for (EmojiSpan emojiSpan : emojiSpans) { + int emojiEnd = spanned.getSpanEnd(emojiSpan); + if (emojiEnd == end) { + return emojiSpan; + } + } + } + } + return null; + } + @Nullable public static String extractSingleEmoji (String str) { - CharSequence emoji = Emoji.instance().replaceEmoji(str); + CharSequence emoji = Emoji.instance().replaceEmoji(str, 0, str.length(), newSingleLimiter()); if (emoji instanceof Spanned) { - EmojiSpan[] emojis = ((Spanned) emoji).getSpans(0, emoji.length(), EmojiSpan.class); - if (emojis != null && emojis.length > 0) { - int start = ((Spanned) emoji).getSpanStart(emojis[0]); - int end = ((Spanned) emoji).getSpanEnd(emojis[0]); - return start == 0 && end == emoji.length() ? emoji.toString() : emoji.subSequence(start, end).toString(); + Spanned spanned = (Spanned) emoji; + int end = spanned.length(); + int next; + for (int i = 0; i < end; i = next) { + next = spanned.nextSpanTransition(i, end, EmojiSpan.class); + EmojiSpan[] emojis = spanned.getSpans(i, next, EmojiSpan.class); + if (emojis != null && emojis.length > 0) { + int emojiStart = ((Spanned) emoji).getSpanStart(emojis[0]); + int emojiEnd = ((Spanned) emoji).getSpanEnd(emojis[0]); + return emojiStart == 0 && emojiEnd == emoji.length() ? emoji.toString() : emoji.subSequence(emojiStart, emojiEnd).toString(); + } } } return null; } + @Nullable + public static String extractPrecedingEmoji (Spanned spanned, int beforeIndex, boolean allowCustom) { + EmojiSpan span = Emoji.findPrecedingEmojiSpan(spanned, beforeIndex); + if (span != null && (allowCustom || !span.isCustomEmoji())) { + int start = spanned.getSpanStart(span); + int end = spanned.getSpanEnd(span); + return spanned.subSequence(start, end).toString(); + } + return null; + } + public boolean isSingleEmoji (TdApi.FormattedText text) { boolean hasEntities = false; if (text.entities != null) { @@ -603,6 +658,9 @@ public EmojiInfo getEmojiInfo (CharSequence codeCs, boolean allowRetry) { if (lastChar == '\u200D' || lastChar == '\uFE0F') { return getEmojiInfo(code.subSequence(0, code.length() - 1), true); } + if (code.length() == 3 && code.charAt(1) == '\uFE0F') { + return getEmojiInfo(Character.toString(code.charAt(0)) + code.charAt(2)); + } } /*if (info == null) { CharSequence fixedEmoji = fixEmoji(code); @@ -611,11 +669,7 @@ public EmojiInfo getEmojiInfo (CharSequence codeCs, boolean allowRetry) { } }*/ if (info == null) { - StringBuilder b = new StringBuilder(code.length()); - for (int i = 0; i < code.length(); i++) { - b.append("\\u").append(Integer.toString(code.charAt(i), 16)); - } - Log.i("Warning. No drawable for emoji: %s", b.toString()); + Log.i("Warning. No drawable for emoji: %s", StringUtils.toUtfString(code)); return null; } @@ -642,8 +696,10 @@ public EmojiSpan newCustomSpan (CharSequence code, @Nullable EmojiInfo info, return null; if (info == null) { info = getEmojiInfo(code); - if (info == null) - return null; + if (info == null) { + Log.i("Invalid or unknown server emoji: %s", StringUtils.toUtfString(code)); + // Ignore that we don't know this emoji to preserve custom emoji entity + } } return CustomEmojiSpanImpl.newCustomEmojiSpan(info, customEmojiSurfaceProvider, tdlib, customEmojiId); } @@ -660,6 +716,20 @@ public CountLimiter singleLimiter () { return singleLimiter; } + public static CountLimiter newSingleLimiter () { + return new org.thunderdog.challegram.emoji.Emoji.CountLimiter() { + @Override + public int getEmojiCount () { + return 0; + } + + @Override + public boolean incrementEmojiCount () { + return false; + } + }; + } + public interface CountLimiter { int getEmojiCount (); boolean incrementEmojiCount (); diff --git a/app/src/main/java/org/thunderdog/challegram/emoji/EmojiBitmaps.java b/app/src/main/java/org/thunderdog/challegram/emoji/EmojiBitmaps.java index 56ce1516ac..883d1d4d1f 100644 --- a/app/src/main/java/org/thunderdog/challegram/emoji/EmojiBitmaps.java +++ b/app/src/main/java/org/thunderdog/challegram/emoji/EmojiBitmaps.java @@ -51,8 +51,7 @@ public EmojiBitmaps (String identifier) { } } - private static Bitmap loadAsset (String filePath, boolean isAsset) { - final int sampleSize = calculateSampleSize(); + private static Bitmap loadAsset (String filePath, boolean isAsset, int sampleSize) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && Config.MODERN_IMAGE_DECODER_ENABLED) { try { android.graphics.ImageDecoder.Source source; @@ -126,13 +125,14 @@ public void recycle () { private void loadEmoji (int page1, int page2) { String fileSuffix = String.format(Locale.US, "%d_%d.png", page1, page2); + final int sampleSize = Emoji.instance().sampleSize; Bitmap result = null; if (!BuildConfig.EMOJI_BUILTIN_ID.equals(identifier)) { File file = new File(new File(Emoji.getEmojiPackDirectory(), identifier), fileSuffix); - result = loadAsset(file.getPath(), false); + result = loadAsset(file.getPath(), false, sampleSize); } if (result == null) { - result = loadAsset(String.format(Locale.US, "emoji/v%d_%s", (12 + BuildConfig.EMOJI_VERSION), fileSuffix), true); + result = loadAsset(String.format(Locale.US, "emoji/v%d_%s", (12 + BuildConfig.EMOJI_VERSION), fileSuffix), true, sampleSize); } Bitmap resultFinal = result; UI.post(() -> { diff --git a/app/src/main/java/org/thunderdog/challegram/emoji/EmojiFilter.java b/app/src/main/java/org/thunderdog/challegram/emoji/EmojiFilter.java index 5304e2e94f..6e88136c7f 100644 --- a/app/src/main/java/org/thunderdog/challegram/emoji/EmojiFilter.java +++ b/app/src/main/java/org/thunderdog/challegram/emoji/EmojiFilter.java @@ -16,6 +16,7 @@ package org.thunderdog.challegram.emoji; import android.text.InputFilter; +import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -243,6 +244,21 @@ public CharSequence filter (CharSequence source, int start, int end, Spanned des } } } + if (dest instanceof Spannable && dend - dstart > 0) { + EmojiSpan[] spansToRemove = dest.getSpans(dstart, dend, EmojiSpan.class); + if (spansToRemove != null) { + for (EmojiSpan span : spansToRemove) { + int spanStart = dest.getSpanStart(span); + int spanEnd = dest.getSpanEnd(span); + if (spanEnd < dstart || spanStart >= dend) + continue; + ((Spannable) dest).removeSpan(span); + if (span instanceof Destroyable) { + ((Destroyable) span).performDestroy(); + } + } + } + } return replaceEmojiOrNull(source, start, end); } } diff --git a/app/src/main/java/org/thunderdog/challegram/emoji/EmojiSpan.java b/app/src/main/java/org/thunderdog/challegram/emoji/EmojiSpan.java index 7abeadd5da..559c990d07 100644 --- a/app/src/main/java/org/thunderdog/challegram/emoji/EmojiSpan.java +++ b/app/src/main/java/org/thunderdog/challegram/emoji/EmojiSpan.java @@ -28,6 +28,9 @@ public interface EmojiSpan { default long getCustomEmojiId () { return 0; } + default EmojiInfo getBuiltInEmojiInfo () { + return null; + } default boolean belongsToSurface (CustomEmojiSurfaceProvider customEmojiSurfaceProvider) { return false; } diff --git a/app/src/main/java/org/thunderdog/challegram/emoji/EmojiSpanImpl.java b/app/src/main/java/org/thunderdog/challegram/emoji/EmojiSpanImpl.java index b22409315f..4647fddbe4 100644 --- a/app/src/main/java/org/thunderdog/challegram/emoji/EmojiSpanImpl.java +++ b/app/src/main/java/org/thunderdog/challegram/emoji/EmojiSpanImpl.java @@ -51,6 +51,12 @@ protected EmojiSpanImpl (@Nullable EmojiInfo info) { this.info = info; } + + @Override + public EmojiInfo getBuiltInEmojiInfo () { + return info; + } + @Override public final EmojiSpan toBuiltInEmojiSpan () { return info != null ? newSpan(info) : null; diff --git a/app/src/main/java/org/thunderdog/challegram/emoji/PreserveCustomEmojiFilter.java b/app/src/main/java/org/thunderdog/challegram/emoji/PreserveCustomEmojiFilter.java new file mode 100644 index 0000000000..7bed558bd0 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/emoji/PreserveCustomEmojiFilter.java @@ -0,0 +1,82 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 28/08/2023 + */ +package org.thunderdog.challegram.emoji; + +import android.text.InputFilter; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.view.inputmethod.BaseInputConnection; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.data.TD; + +import me.vkryl.td.Td; + +/** + * Workaround for bug present in Samsung Keyboard version 5.6.10.40 with "Predictive text" toggle on (com.samsung.android.honeyboard/.service.HoneyBoardService) + */ +public class PreserveCustomEmojiFilter implements InputFilter { + @Override + public CharSequence filter (CharSequence sourceRaw, int start, int end, Spanned dest, int dstart, int dend) { + if (!(sourceRaw instanceof Spanned)) { + return null; + } + final Spanned source = (Spanned) sourceRaw; + final int length = source.length(); + // Entire non-empty text was replaced with the text of the same length + if (start == 0 && dstart == 0 && end > start && dend == end && end == length && dest.length() == length) { + int transitionStart = dest.nextSpanTransition(dstart, dend, CustomEmojiSpanImpl.class); + // Custom emoji were not present + if (transitionStart == dend) { + return null; + } + transitionStart = source.nextSpanTransition(start, end, CustomEmojiSpanImpl.class); + // Custom emoji are still present + if (transitionStart != end) { + return null; + } + + SpannableStringBuilder newText = new SpannableStringBuilder(source, start, end); + + // Text changed + if (!dest.toString().equals(newText.toString())) { + return null; + } + + CustomEmojiSpanImpl[] lostCustomEmoji = dest.getSpans(0, dest.length(), CustomEmojiSpanImpl.class); + for (CustomEmojiSpanImpl span : lostCustomEmoji) { + int emojiStart = dest.getSpanStart(span); + int emojiEnd = dest.getSpanEnd(span); + if (emojiStart != -1 && emojiEnd != -1) { + newText.setSpan(span, emojiStart, emojiEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + SpannableStringBuilder oldContent = new SpannableStringBuilder(dest, dstart, dend); + BaseInputConnection.removeComposingSpans(oldContent); + SpannableStringBuilder newContent = new SpannableStringBuilder(newText, start, end); + BaseInputConnection.removeComposingSpans(newContent); + TdApi.TextEntity[] oldEntities = TD.toEntities(oldContent, false); + TdApi.TextEntity[] newEntities = TD.toEntities(newContent, false); + // Entities changed + if (!Td.equalsTo(oldEntities, newEntities, true)) { + return null; + } + + return newText; + } + return null; + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/emoji/RecentEmoji.java b/app/src/main/java/org/thunderdog/challegram/emoji/RecentEmoji.java index 12f3b84e21..6a19e1ccfa 100644 --- a/app/src/main/java/org/thunderdog/challegram/emoji/RecentEmoji.java +++ b/app/src/main/java/org/thunderdog/challegram/emoji/RecentEmoji.java @@ -14,12 +14,28 @@ */ package org.thunderdog.challegram.emoji; +import org.thunderdog.challegram.tool.Strings; + public class RecentEmoji { public final String emoji; public final RecentInfo info; + public final boolean isCustomEmoji; + public final long customEmojiId; public RecentEmoji (String emoji, RecentInfo info) { this.emoji = emoji; this.info = info; + this.isCustomEmoji = emoji.startsWith(Emoji.CUSTOM_EMOJI_CACHE) || emoji.startsWith(Emoji.CUSTOM_EMOJI_CACHE_OLD); + long customEmojiId = 0; + if (isCustomEmoji) { + try { + customEmojiId = Long.parseLong(Strings.getNumber(emoji)); + } catch (Throwable ignored) {} + } + this.customEmojiId = customEmojiId; + } + + public boolean isCustomEmoji () { + return isCustomEmoji; } } diff --git a/app/src/main/java/org/thunderdog/challegram/filegen/PhotoGenerationInfo.java b/app/src/main/java/org/thunderdog/challegram/filegen/PhotoGenerationInfo.java index dc8dd4fea5..94f8d4df2b 100644 --- a/app/src/main/java/org/thunderdog/challegram/filegen/PhotoGenerationInfo.java +++ b/app/src/main/java/org/thunderdog/challegram/filegen/PhotoGenerationInfo.java @@ -109,6 +109,9 @@ public PaintState getPaintState () { } public boolean needSpecialProcessing (boolean needRotate) { + if (cropState != null && cropState.getFlags() != 0) { + return true; + } if (paintState != null && !paintState.isEmpty()) { return true; } @@ -241,8 +244,12 @@ public Bitmap process (Bitmap source, boolean needRotate) { int bitmapBottom = source.getHeight(); int rotation = needRotate ? this.rotation : 0; boolean drawingComplete = false; + boolean needMirrorHorizontal = false; + boolean needMirrorVertical = false; if (cropState != null) { + needMirrorHorizontal = cropState.needMirrorHorizontally(); + needMirrorVertical = cropState.needMirrorVertically(); rotation = MathUtils.modulo(rotation + cropState.getRotateBy(), 360); if (regionDecoderState == REGION_ERROR) { Log.i("Region reader failed, cropping in-memory"); @@ -279,7 +286,16 @@ public Bitmap process (Bitmap source, boolean needRotate) { if (scale != 1f) { c.scale(scale, scale, w / 2, h / 2); } + if (needMirrorHorizontal || needMirrorVertical) { + c.save(); + c.scale(needMirrorHorizontal ? -1 : 1, needMirrorVertical ? -1 : 1, w / 2f, h / 2f); + } c.drawBitmap(source, 0, 0, null); + if (needMirrorHorizontal || needMirrorVertical) { + c.restore(); + needMirrorHorizontal = false; + needMirrorVertical = false; + } if (paintState != null) { drawPaintState(c, source.getWidth(), source.getHeight()); drawingComplete = true; @@ -302,13 +318,34 @@ public Bitmap process (Bitmap source, boolean needRotate) { if (paintState != null && !drawingComplete) { Bitmap altered = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(altered); + if (needMirrorHorizontal || needMirrorVertical) { + c.save(); + c.scale(needMirrorHorizontal ? -1 : 1, needMirrorVertical ? -1 : 1, source.getWidth() / 2f, source.getHeight() / 2f); + } c.drawBitmap(source, 0, 0, null); + if (needMirrorHorizontal || needMirrorVertical) { + c.restore(); + needMirrorHorizontal = false; + needMirrorVertical = false; + } drawPaintState(c, source.getWidth(), source.getHeight()); source.recycle(); source = altered; U.recycle(c); } + if (needMirrorHorizontal || needMirrorVertical) { // fixme: use matrix ?? + Bitmap flipped = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(flipped); + + c.scale(needMirrorHorizontal ? -1 : 1, needMirrorVertical ? -1 : 1, source.getWidth() / 2f, source.getHeight() / 2f); + c.drawBitmap(source, 0, 0, null); + + source.recycle(); + source = flipped; + U.recycle(c); + } + return Bitmap.createBitmap(source, bitmapLeft, bitmapTop, bitmapRight - bitmapLeft, bitmapBottom - bitmapTop, matrix, false); } diff --git a/app/src/main/java/org/thunderdog/challegram/filegen/TdlibFileGenerationManager.java b/app/src/main/java/org/thunderdog/challegram/filegen/TdlibFileGenerationManager.java index f809248421..fb05818ada 100644 --- a/app/src/main/java/org/thunderdog/challegram/filegen/TdlibFileGenerationManager.java +++ b/app/src/main/java/org/thunderdog/challegram/filegen/TdlibFileGenerationManager.java @@ -78,6 +78,7 @@ import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.unit.ByteUnit; +import me.vkryl.td.Td; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; @@ -1497,7 +1498,7 @@ private static TdApi.InputFileGenerated newThumbnailFile (TdApi.InputFile inputF } public T createThumbnail (@NonNull final T content, final boolean isSecretChat, @Nullable final TdApi.File file) { - final boolean isSecret = isSecretChat || TD.isSecret(content); + final boolean isSecret = isSecretChat || Td.isSecret(content); final int resolution = content.getConstructor() == TdApi.InputMessageSticker.CONSTRUCTOR || isSecret ? SMALL_THUMB_RESOLUTION : BIG_THUMB_RESOLUTION; switch (content.getConstructor()) { diff --git a/app/src/main/java/org/thunderdog/challegram/helper/BotHelper.java b/app/src/main/java/org/thunderdog/challegram/helper/BotHelper.java index da64d6139e..d9c74814e8 100644 --- a/app/src/main/java/org/thunderdog/challegram/helper/BotHelper.java +++ b/app/src/main/java/org/thunderdog/challegram/helper/BotHelper.java @@ -350,7 +350,7 @@ private void processReplyMarkup (TdApi.Message message, boolean remember) { TdApi.ReplyMarkupShowKeyboard showKeyboard = (TdApi.ReplyMarkupShowKeyboard) markup; processShowKeyboard(messageId, showKeyboard); if (type == TYPE_GROUP || type == TYPE_SUPERGROUP) { - context.showReply(message, false, false); // FIXME? + context.showReply(message, null, false, false); // FIXME? } context.setCustomBotPlaceholder(showKeyboard.inputFieldPlaceholder); break; @@ -371,9 +371,9 @@ private void processHideKeyboard (long messageId, boolean personal) { private void processForceReply (TdApi.Message message, boolean personal) { if (personal) { context.showKeyboard(); - context.showReply(message, false, false); + context.showReply(message, null, false, false); } else if (type == TYPE_PRIVATE) { - context.showReply(message, false, false); + context.showReply(message, null, false, false); } if (message != null) { context.tdlib().client().send(new TdApi.DeleteChatReplyMarkup(chatId, message.id), this); diff --git a/app/src/main/java/org/thunderdog/challegram/helper/FoundUrls.java b/app/src/main/java/org/thunderdog/challegram/helper/FoundUrls.java new file mode 100644 index 0000000000..44edda2f22 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/helper/FoundUrls.java @@ -0,0 +1,154 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 28/11/2023 + */ +package org.thunderdog.challegram.helper; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.tool.Strings; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import me.vkryl.core.ArrayUtils; +import me.vkryl.core.StringUtils; + +public final class FoundUrls { + public final @NonNull Set set; + public final @NonNull String[] urls; + + private static void findUniqueUrls (Set uniqueUrls, @NonNull TdApi.FormattedText formattedText) { + List foundUrls = TD.findUrls(formattedText); + if (foundUrls != null && !foundUrls.isEmpty()) { + for (String url : foundUrls) { + if (!url.matches("^[^/]+$")) { + uniqueUrls.add(url); + } + } + } + } + + public FoundUrls () { + this.set = Collections.emptySet(); + this.urls = new String[0]; + } + + public boolean hasUrl (@NonNull String url) { + if (!StringUtils.isEmpty(url)) { + if (set.contains(url)) { + return true; + } + String unifiedUrl = unifyUrl(url); + if (set.contains(unifiedUrl)) { + return true; + } + for (String existingUrl : urls) { + if (unifiedUrl.equals(unifyUrl(existingUrl))) { + return true; + } + } + } + return false; + } + + public int indexOfUrl (@NonNull String url) { + int index = ArrayUtils.indexOf(urls, url); + if (index == -1) { + String unifiedUrl = unifyUrl(url); + int foundIndex = 0; + for (String existingUrl : urls) { + String unifiedExistingUrl = unifyUrl(existingUrl); + if (unifiedUrl.equals(unifiedExistingUrl)) { + return foundIndex; + } + foundIndex++; + } + } + return index; + } + + private static FoundUrls emptyResult; + + public static FoundUrls emptyResult () { + if (emptyResult == null) { + emptyResult = new FoundUrls(); + } + return emptyResult; + } + + public FoundUrls (@NonNull TdApi.FormattedText formattedText) { + this.set = new LinkedHashSet<>(); + findUniqueUrls(this.set, formattedText); + this.urls = set.toArray(new String[0]); + } + + public FoundUrls (@NonNull TdApi.MessageText messageText) { + this.set = new LinkedHashSet<>(); + findUniqueUrls(this.set, messageText.text); + String specificUrl = + messageText.linkPreviewOptions != null && !messageText.linkPreviewOptions.isDisabled && !StringUtils.isEmpty(messageText.linkPreviewOptions.url) ? + messageText.linkPreviewOptions.url : + messageText.webPage != null ? messageText.webPage.url : + null; + if (!StringUtils.isEmpty(specificUrl)) { + // Make sure there is existing url + this.set.add(specificUrl); + } + this.urls = set.toArray(new String[0]); + } + + public boolean isEmpty () { + return set.isEmpty(); + } + + public int size () { + return set.size(); + } + + @Override + public boolean equals (@Nullable Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof FoundUrls)) { + return false; + } + FoundUrls other = (FoundUrls) obj; + return Arrays.equals(this.urls, other.urls); + } + + private static String unifyUrl (@NonNull String url) { + Uri uri = Strings.forceProtocol(url, "https"); + if (uri != null) { + String path = uri.getPath(); + if (path != null && path.matches("^/+$")) { + uri = uri.buildUpon().path(null).build(); + } + return uri.toString(); + } + return url; + } + + public static boolean compareUrls (@NonNull String a, @NonNull String b) { + return a.equals(b) || unifyUrl(a).equals(unifyUrl(b)); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/helper/InlineSearchContext.java b/app/src/main/java/org/thunderdog/challegram/helper/InlineSearchContext.java index a817934a2a..6d60c2cff7 100644 --- a/app/src/main/java/org/thunderdog/challegram/helper/InlineSearchContext.java +++ b/app/src/main/java/org/thunderdog/challegram/helper/InlineSearchContext.java @@ -14,14 +14,9 @@ */ package org.thunderdog.challegram.helper; -import android.content.Context; -import android.content.res.Resources; import android.location.Location; -import android.os.Build; -import android.os.LocaleList; import android.os.SystemClock; -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.InputMethodSubtype; +import android.text.Spanned; import androidx.annotation.IntRange; import androidx.annotation.NonNull; @@ -61,23 +56,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Locale; -import me.vkryl.android.LocaleUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.lambda.CancellableRunnable; import me.vkryl.td.ChatId; import me.vkryl.td.Td; public class InlineSearchContext implements LocationHelper.LocationChangeListener, InlineResultsWrap.LoadMoreCallback { - public static final int WARNING_OK = 0; - public static final int WARNING_CONFIRM = 1; - public static final int WARNING_BLOCK = 2; - public interface Callback { long provideInlineSearchChatId (); TdApi.Chat provideInlineSearchChat (); - TdApi.WebPage provideExistingWebPage (TdApi.FormattedText currentText); long provideInlineSearchChatUserId (); boolean isDisplayingItems (); void updateInlineMode (boolean isInInlineMode, boolean isInProgress); @@ -85,15 +73,14 @@ public interface Callback { void showInlineResults (ArrayList> items, boolean isContent); void addInlineResults (ArrayList> items); void hideInlineResults (); - void showInlineStickers (ArrayList stickers, boolean isMore); - boolean needsLinkPreview (); - boolean showLinkPreview (@Nullable String link, @Nullable TdApi.WebPage webPage); + void showInlineStickers (ArrayList stickers, String foundByEmoji, boolean isEmoji, boolean isMore); + + boolean enableLinkPreview (); + void showLinkPreview (@Nullable FoundUrls foundUrls); boolean needsInlineBots (); TdApi.FormattedText getOutputText (boolean applyMarkdown); - - int showLinkPreviewWarning (int contextId, @Nullable String link); } public interface CommandListProvider { @@ -108,9 +95,10 @@ public interface QueryResultsChangeListener { public static final int MODE_MENTION = 1; public static final int MODE_HASHTAGS = 2; public static final int MODE_EMOJI_SUGGESTION = 3; - public static final int MODE_STICKERS = 4; + public static final int MODE_STICKERS_AND_EMOJI = 4; public static final int MODE_COMMAND = 5; public static final int MODE_INLINE_SEARCH = 6; + public static final int MODE_EMOJI = 7; private static final int FLAG_CAPTION = 1; private static final int FLAG_DISALLOW_INLINE_RESULTS = 1 << 1; @@ -131,7 +119,7 @@ public interface QueryResultsChangeListener { private boolean canHandlePositionChange; private int lastHandledPosition; private int lastKnownCursorPosition; - private ViewController boundController; + private final ViewController boundController; public InlineSearchContext (BaseActivity context, Tdlib tdlib, @NonNull Callback callback, ViewController boundController) { this.context = context; @@ -192,7 +180,12 @@ public void setCommandListProvider (@Nullable CommandListProvider commandListPro // Public entry public void forceCheck () { - onQueryResultsChanged(currentText); + if (!currentText.isEmpty()) { + String oCurrentText = currentText; + CharSequence oCurrentCs = currentCs; + currentText = ""; currentCs = ""; + onTextChanged(oCurrentCs, oCurrentText, lastKnownCursorPosition); + } } public void onQueryResultsChanged (String queryText) { @@ -210,6 +203,11 @@ public void onCursorPositionChanged (int newPosition) { } } + public void reset () { + cancelPendingQueries(); + setCurrentMode(MODE_NONE); + } + public void onTextChanged (CharSequence newCs, String newText, @IntRange(from = -1, to = Integer.MAX_VALUE) int cursorPosition) { lastKnownCursorPosition = cursorPosition; if (StringUtils.equalsOrBothEmpty(this.currentText, newText)) { @@ -232,12 +230,20 @@ public void onTextChanged (CharSequence newCs, String newText, @IntRange(from = probablyHasWebPagePreview = false; clearInlineMode(); - // Display stickers in case of a single emoji + final boolean canSearchCustomEmoji = canSearchCustomEmoji(); if (isCaption() || disallowInlineResults()) { - setCurrentMode(MODE_NONE); + if (canSearchCustomEmoji) { + setCurrentMode(MODE_EMOJI); + searchStickers(newText, false, true, null); + } else { + setCurrentMode(MODE_NONE); + } } else { - setCurrentMode(MODE_STICKERS); - searchStickers(newText, false, null); + setCurrentMode(MODE_STICKERS_AND_EMOJI); + searchStickers(newText, false, false, null); + if (canSearchCustomEmoji) { + searchStickers(newText, false, true, null); + } } } else { final String inlineUsername = getInlineUsername(); @@ -250,7 +256,7 @@ public void onTextChanged (CharSequence newCs, String newText, @IntRange(from = } } - processLinkPreview(probablyHasWebPagePreview ? newText : ""); + processLinkPreview(probablyHasWebPagePreview); } // Common UI @@ -384,24 +390,59 @@ private void cancelPendingQueries () { // Stickers private CancellableResultHandler stickerRequest; + private CancellableResultHandler emojiRequest; private void cancelStickerRequest () { if (stickerRequest != null) { stickerRequest.cancel(); stickerRequest = null; } + if (emojiRequest != null) { + emojiRequest.cancel(); + emojiRequest = null; + } } - private void searchStickers (final String emoji, final boolean more, @Nullable final int[] ignoreStickerIds) { - final int stickerMode = Settings.instance().getStickerMode(); + private void searchStickers (final String emoji, final boolean more, final boolean isEmoji, @Nullable final int[] ignoreStickerIds) { + if (isEmoji) { + emojiRequest = searchStickersImpl(emoji, true, more, ignoreStickerIds); + } else { + stickerRequest = searchStickersImpl(emoji, false, more, ignoreStickerIds); + } + } + + private int getSearchStickersMode (boolean isEmoji) { + if (isEmoji) { + return Settings.instance().getEmojiMode(); + } else { + return Settings.instance().getStickerMode(); + } + } + + private @Nullable CancellableResultHandler searchStickersImpl (final String emoji, final boolean isEmoji, final boolean more, @Nullable final int[] ignoreStickerIds) { + final int stickerMode = getSearchStickersMode(isEmoji); if (stickerMode == Settings.STICKER_MODE_NONE) { - return; + return null; } if (stickerMode == Settings.STICKER_MODE_ALL && !more && tdlib.suggestOnlyApiStickers()) { - searchStickers(emoji, true, ignoreStickerIds); - return; + return searchStickersImpl(emoji, isEmoji, true, ignoreStickerIds); } - stickerRequest = new CancellableResultHandler() { + final long chatId = callback.provideInlineSearchChatId(); + final TdApi.StickerType type = isEmoji ? new TdApi.StickerTypeCustomEmoji() : new TdApi.StickerTypeRegular(); + TdApi.Function function; + if (more) { + function = new TdApi.SearchStickers(type, emoji, 1000); + } else { + function = new TdApi.GetStickers(type, emoji, 1000, chatId); + } + CancellableResultHandler handler = stickersHandler(emoji, isEmoji, stickerMode, more, ignoreStickerIds); + tdlib.client().send(function, handler); + + return handler; + } + + private CancellableResultHandler stickersHandler (final String emoji, final boolean isEmoji, final int stickerMode, final boolean more, @Nullable final int[] ignoreStickerIds) { + return new CancellableResultHandler() { @Override public void processResult (TdApi.Object object) { switch (object.getConstructor()) { @@ -434,9 +475,9 @@ public void processResult (TdApi.Object object) { } tdlib.ui().post(() -> { if (!isCancelled()) { - displayStickers(displayingStickers, emoji, more); + displayStickers(displayingStickers, isEmoji, emoji, more); if (!more && stickerMode == Settings.STICKER_MODE_ALL) { - searchStickers(emoji, true, futureIgnoreStickerIds); + searchStickers(emoji, true, isEmoji, futureIgnoreStickerIds); } } }); @@ -445,23 +486,15 @@ public void processResult (TdApi.Object object) { } } }; - final long chatId = callback.provideInlineSearchChatId(); - TdApi.Function function; - if (more) { - function = new TdApi.SearchStickers(new TdApi.StickerTypeRegular(), emoji, 1000); - } else { - function = new TdApi.GetStickers(new TdApi.StickerTypeRegular(), emoji, 1000, chatId); - } - tdlib.client().send(function, stickerRequest); } @UiThread - private void displayStickers (TdApi.Sticker[] stickers, String foundByEmoji, boolean isMore) { + private void displayStickers (TdApi.Sticker[] stickers, boolean isEmoji, String foundByEmoji, boolean isMore) { ArrayList list = new ArrayList<>(stickers.length); for (TdApi.Sticker sticker : stickers) { list.add(new TGStickerObj(tdlib, sticker, foundByEmoji, sticker.fullType)); } - callback.showInlineStickers(list, isMore); + callback.showInlineStickers(list, foundByEmoji, isEmoji, isMore); } // Inline query @@ -653,7 +686,7 @@ public TGPlayerController.PlayList buildPlayList (TdApi.Message fromMessage) { return null; } - int contentType = fromMessage.content.getConstructor(); + @TdApi.MessageContent.Constructors int contentType = fromMessage.content.getConstructor(); ArrayList items = null; int foundIndex = -1; @@ -855,6 +888,21 @@ private static void searchWord (char startChar, String text, int cursorPosition, private boolean searchOther (int cursorPosition) { this.canHandlePositionChange = true; this.lastHandledPosition = cursorPosition; + + if (cursorPosition > 0 && canSearchCustomEmoji() && cursorPosition <= currentCs.length()) { + final String singleEmoji; + if (currentCs instanceof Spanned) { + singleEmoji = Emoji.extractPrecedingEmoji((Spanned) currentCs, cursorPosition, false); + } else { + singleEmoji = null; + } + if (!StringUtils.isEmpty(singleEmoji)) { + setCurrentMode(MODE_EMOJI); + searchStickers(singleEmoji, false, true, null); + return true; + } + } + if (currentText.charAt(0) == '/') { boolean isOk = true; @@ -1184,101 +1232,11 @@ private void cancelEmojiHandler () { } } - private static String toLanguageCode (InputMethodSubtype ims) { - if (ims != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - String languageTag = ims.getLanguageTag(); - if (!StringUtils.isEmpty(languageTag)) { - return languageTag; - } - } - String locale = ims.getLocale(); - if (!StringUtils.isEmpty(locale)) { - Locale l = U.getDisplayLocaleOfSubtypeLocale(locale); - if (l != null) { - return LocaleUtils.toBcp47Language(l); - } - } - } - return null; - } - private void searchEmoji (final int startIndex, final int endIndex, final String currentInput, final String suggestionQuery) { if (lastInlineResultsType != TYPE_EMOJI_SUGGESTIONS) { hideResults(); } - final List inputLanguages = new ArrayList<>(); - InputMethodManager imm = (InputMethodManager) UI.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - String inputLanguageCode = null; - try { - inputLanguageCode = toLanguageCode(imm.getCurrentInputMethodSubtype()); - } catch (Throwable ignored) { } - if (StringUtils.isEmpty(inputLanguageCode)) { - try { - inputLanguageCode = toLanguageCode(imm.getLastInputMethodSubtype()); - } catch (Throwable ignored) { } - } - if (!StringUtils.isEmpty(inputLanguageCode)) { - inputLanguages.add(inputLanguageCode); - } - - /*if (Strings.isEmpty(inputLanguageCode)) { - try { - String id = android.provider.Settings.Secure.getString( - UI.getAppContext().getContentResolver(), - android.provider.Settings.Secure.DEFAULT_INPUT_METHOD - ); - if (!Strings.isEmpty(id)) { - List list = imm.getInputMethodList(); - lookup: - for (InputMethodInfo info : list) { - if (id.equals(info.getId())) { - List subtypes = imm.getEnabledInputMethodSubtypeList(info, true); - for (InputMethodSubtype subtype : subtypes) { - String languageCode = toLanguageCode(subtype); - if (!Strings.isEmpty(languageCode)) { - inputLanguageCode = languageCode; - break lookup; - } - } - } - } - } - } catch (Throwable ignored) { } - } - if (Strings.isEmpty(inputLanguageCode) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - try { - LocaleList localeList = ((InputView) callback).getImeHintLocales(); - if (localeList != null) { - for (int i = 0; i < localeList.size(); i++) { - inputLanguageCode = U.toBcp47Language(localeList.get(i)); - if (!Strings.isEmpty(inputLanguageCode)) - break; - } - } - } catch (Throwable ignored) { } - }*/ - } - if (inputLanguages.isEmpty()) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - LocaleList locales = Resources.getSystem().getConfiguration().getLocales(); - for (int i = 0; i < locales.size(); i++) { - String code = LocaleUtils.toBcp47Language(locales.get(i)); - if (!StringUtils.isEmpty(code) && !inputLanguages.contains(code)) - inputLanguages.add(code); - } - } else { - String code = LocaleUtils.toBcp47Language(Resources.getSystem().getConfiguration().locale); - if (!StringUtils.isEmpty(code)) { - inputLanguages.add(code); - } - } - } catch (Throwable ignored) { } - } - Background.instance().post(new CancellableRunnable() { @Override public void act () { @@ -1312,7 +1270,7 @@ public void act () { } if (!StringUtils.isEmpty(query)) { - tdlib.client().send(new TdApi.SearchEmojis(query, false, inputLanguages.isEmpty() ? null : inputLanguages.toArray(new String[0])), result -> { + tdlib.client().send(new TdApi.SearchEmojis(query, false, U.getInputLanguages()), result -> { if (result.getConstructor() == TdApi.Emojis.CONSTRUCTOR) { TdApi.Emojis emojis = (TdApi.Emojis) result; ArrayList> addedResults = new ArrayList<>(emojis.emojis.length); @@ -1362,105 +1320,57 @@ public void act () { // Links processor - private final ArrayList lastLinks = new ArrayList<>(5); - private final ArrayList currentLinks = new ArrayList<>(5); + private FoundUrls lastFoundUrls; private int linkContextId; - private void processLinkPreview (String input) { - currentLinks.clear(); + private void processLinkPreview (boolean allowLinkPreview) { + allowLinkPreview = allowLinkPreview && callback.enableLinkPreview(); - if (!StringUtils.isEmpty(input)) { - List links = TD.findUrls(callback.getOutputText(true)); - if (links != null && !links.isEmpty()) { - currentLinks.addAll(links); - } + boolean isPrivacyCritical = ChatId.isSecret(callback.provideInlineSearchChatId()); + boolean needPrivacyPrompt = isPrivacyCritical && Settings.instance().needTutorial(Settings.TUTORIAL_SECRET_LINK_PREVIEWS); + if (allowLinkPreview && isPrivacyCritical && !needPrivacyPrompt && !Settings.instance().needSecretLinkPreviews()) { + // As an optimization, do not look up for any URLs at all in secret chats when link previews are forbidden. + allowLinkPreview = false; } - int size = currentLinks.size(); - if (lastLinks.size() == size) { - if (size == 0) { - return; - } - boolean changed = false; - int j = 0; - for (String str : lastLinks) { - if (!currentLinks.get(j++).equals(str)) { - changed = true; - break; - } - } - if (!changed) { - return; - } - } + TdApi.FormattedText formattedText = allowLinkPreview ? callback.getOutputText(true) : null; + FoundUrls foundUrls = !Td.isEmpty(formattedText) ? new FoundUrls(formattedText) : null; - lastLinks.clear(); - lastLinks.addAll(currentLinks); + boolean wasEmpty = (lastFoundUrls == null || lastFoundUrls.isEmpty()); + boolean nowEmpty = foundUrls == null || foundUrls.isEmpty(); - final int contextId = ++linkContextId; - if (size == 0 || !callback.needsLinkPreview()) { - closeLinkPreview(); - } else { - processLinkPreview(contextId, lastLinks.get(0)); + if (wasEmpty == nowEmpty && (foundUrls == null || foundUrls.equals(lastFoundUrls))) { + // Nothing changed + return; } - } - public final void processLinkPreview (final int contextId, final String link) { - int result = WARNING_BLOCK; - if (!StringUtils.isEmpty(link)) { - result = callback.showLinkPreviewWarning(contextId, link); + final int contextId = ++linkContextId; + this.lastFoundUrls = foundUrls; + + if (nowEmpty) { + callback.showLinkPreview(null); + return; } - if (result != WARNING_CONFIRM && callback.showLinkPreview(link, null)) { - switch (result) { - case WARNING_BLOCK: { - dispatchLinkPreview(contextId, null, null); - break; - } - case WARNING_OK: { - TdApi.WebPage webPage = callback.provideExistingWebPage(callback.getOutputText(false)); - if (webPage != null) { - dispatchLinkPreview(contextId, link, webPage); - return; - } - UI.post(() -> { - if (linkContextId == contextId) { - tdlib.client().send(new TdApi.GetWebPagePreview(callback.getOutputText(true)), object -> { - switch (object.getConstructor()) { - case TdApi.WebPage.CONSTRUCTOR: { - dispatchLinkPreview(contextId, link, (TdApi.WebPage) object); - break; - } - case TdApi.Error.CONSTRUCTOR: { - TdApi.Error error = (TdApi.Error) object; - if (error.code != 404) { // 404 is "Web page is empty". Maybe something interesting - Log.w("Cannot load link preview: %s", TD.toErrorString(object)); - } - dispatchLinkPreview(contextId, null, null); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.GetWebPagePreview.class, TdApi.WebPage.class); - break; - } - } - }); - } - }, 400l); - break; + + if (needPrivacyPrompt) { + callback.showLinkPreview(null); + boundController.openSecretLinkPreviewAlert(isAccepted -> { + if (linkContextId == contextId && isAccepted) { + callback.showLinkPreview(foundUrls); } - } + }); + return; } + + callback.showLinkPreview(foundUrls); } - private void dispatchLinkPreview (final int contextId, final @Nullable String link, final @Nullable TdApi.WebPage webPage) { - UI.post(() -> { - if (linkContextId == contextId) { - callback.showLinkPreview(link, webPage); - } - }); + private boolean isInSelfChat () { + long chatId = callback.provideInlineSearchChatId(); + return chatId != 0 && tdlib.isSelfChat(chatId); } - private void closeLinkPreview () { - callback.showLinkPreview(null, null); + private boolean canSearchCustomEmoji () { + return tdlib.account().isPremium() || isInSelfChat(); } } diff --git a/app/src/main/java/org/thunderdog/challegram/helper/LinkPreview.java b/app/src/main/java/org/thunderdog/challegram/helper/LinkPreview.java new file mode 100644 index 0000000000..47a180a63c --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/helper/LinkPreview.java @@ -0,0 +1,228 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 28/11/2023 + */ +package org.thunderdog.challegram.helper; + +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.chat.MediaPreview; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGWebPage; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.tool.Strings; +import org.thunderdog.challegram.util.RateLimiter; + +import java.util.ArrayList; +import java.util.List; + +import me.vkryl.core.StringUtils; +import me.vkryl.core.lambda.Destroyable; +import me.vkryl.core.lambda.RunnableData; +import me.vkryl.core.reference.ReferenceList; +import me.vkryl.td.Td; + +public class LinkPreview implements Destroyable { + public final Tdlib tdlib; + public final String url; + + public TdApi.WebPage webPage; + public TdApi.Error error; + + private final TdApi.Message fakeMessage; + + private final RateLimiter linkPreviewLoader = new RateLimiter(this::loadLinkPreview, 400L, null); + private boolean needLoadWebPagePreview, isDestroyed; + private final List> staticLoadCallbacks = new ArrayList<>(); + + private final ReferenceList> loadCallbacks = new ReferenceList<>(false, true, (list, isFull) -> { + if (isFull) { + if (needLoadWebPagePreview) { + linkPreviewLoader.run(); + } + } else { + linkPreviewLoader.cancelIfScheduled(); + } + }); + + private boolean forceSmallMedia, forceLargeMedia; + + public LinkPreview (Tdlib tdlib, String url, @Nullable TdApi.Message existingMessage) { + this.tdlib = tdlib; + this.url = url; + this.linkPreviewLoader.setDelayFirstExecution(true); + this.needLoadWebPagePreview = true; + + TdApi.MessageText messageText = new TdApi.MessageText(new TdApi.FormattedText(url, null), null, null); + TdApi.MessageSender messageSender = existingMessage != null ? existingMessage.senderId : new TdApi.MessageSenderUser(tdlib.myUserId()); + + if (existingMessage != null && Td.isText(existingMessage.content)) { + TdApi.MessageText existingText = (TdApi.MessageText) existingMessage.content; + if (existingText.webPage != null) { + TdApi.LinkPreviewOptions existingOptions = existingText.linkPreviewOptions; + boolean isCurrentWebPage = + (existingText.linkPreviewOptions != null && !StringUtils.isEmpty(existingText.linkPreviewOptions.url) && FoundUrls.compareUrls(existingText.linkPreviewOptions.url, url)) || + (FoundUrls.compareUrls(existingText.webPage.url, url)); + if (isCurrentWebPage) { + this.webPage = existingText.webPage; + updateMessageText(messageText, this.webPage); + this.needLoadWebPagePreview = false; + this.forceSmallMedia = existingOptions != null && existingOptions.forceSmallMedia; + this.forceLargeMedia = existingOptions != null && existingOptions.forceLargeMedia; + } + } + } + this.fakeMessage = TD.newFakeMessage(0, messageSender, messageText); + } + + public void addLoadCallback (RunnableData loadCallback) { + this.staticLoadCallbacks.add(loadCallback); + } + + public void removeLoadCallback (RunnableData loadCallback) { + this.staticLoadCallbacks.remove(loadCallback); + } + + private static void updateMessageText (TdApi.MessageText messageText, TdApi.WebPage webPage) { + messageText.webPage = webPage; + if (!Td.isEmpty(webPage.description)) { + messageText.text = webPage.description; + } else if (!StringUtils.isEmpty(webPage.siteName) && !StringUtils.isEmpty(webPage.title)) { + messageText.text = new TdApi.FormattedText(webPage.title, null); + } + } + + public void addReference (RunnableData callback) { + loadCallbacks.add(callback); + } + + public void removeReference (RunnableData callback) { + loadCallbacks.remove(callback); + } + + @Override + public void performDestroy () { + isDestroyed = true; + needLoadWebPagePreview = false; + linkPreviewLoader.cancelIfScheduled(); + loadCallbacks.clear(); + } + + private void loadLinkPreview () { + needLoadWebPagePreview = false; + tdlib.send(new TdApi.GetWebPagePreview(new TdApi.FormattedText(url, null), null), (webPage, error) -> { + tdlib.ui().post(() -> { + if (webPage != null) { + this.webPage = webPage; + updateMessageText((TdApi.MessageText) fakeMessage.content, webPage); + } else { + this.error = error; + } + notifyLinkPreviewLoaded(); + }); + }); + } + + public boolean isNotFound () { + return error != null; + } + + public boolean isLoading () { + return error == null && webPage == null; + } + + public boolean hasMedia () { + return webPage != null && MediaPreview.hasMedia(webPage); + } + + public boolean forceSmallMedia () { + return forceSmallMedia; + } + + public boolean forceLargeMedia () { + return forceLargeMedia; + } + + public boolean toggleLargeMedia () { + boolean showLargeMedia = getOutputShowLargeMedia(); + if (hasMedia() && webPage.hasLargeMedia) { + forceLargeMedia = !showLargeMedia; + forceSmallMedia = showLargeMedia; + return getOutputShowLargeMedia() != showLargeMedia; + } + return false; + } + + public boolean getOutputShowLargeMedia () { + if (hasMedia()) { + if (webPage.hasLargeMedia) { + if (forceLargeMedia) { + return true; + } + if (forceSmallMedia) { + return false; + } + } + return webPage.showLargeMedia; + } + return false; + } + + public TdApi.Message getFakeMessage () { + return fakeMessage; + } + + public String getForcedTitle () { + if (isLoading()) { + return Lang.getString(R.string.GettingLinkInfo); + } else if (isNotFound()) { + return Lang.getString(R.string.NoLinkInfo); + } + + String title = Td.isEmpty(webPage.description) ? Strings.any(webPage.siteName, webPage.title) : Strings.any(webPage.title, webPage.siteName); + if (!StringUtils.isEmpty(title)) { + return title; + } + if (webPage.photo != null || (webPage.sticker != null && Math.max(webPage.sticker.width, webPage.sticker.height) > TGWebPage.STICKER_SIZE_LIMIT)) { + return Lang.getString(R.string.Photo); + } else if (webPage.video != null) { + return Lang.getString(R.string.Video); + } else if (webPage.document != null || webPage.voiceNote != null) { + title = webPage.document != null ? webPage.document.fileName : Lang.getString(R.string.Audio); + if (StringUtils.isEmpty(title)) { + title = Lang.getString(R.string.File); + } + return title; + } else if (webPage.audio != null) { + return TD.getTitle(webPage.audio) + " – " + TD.getSubtitle(webPage.audio); + } else if (webPage.sticker != null) { + return Lang.getString(R.string.Sticker); + } + return Lang.getString(R.string.LinkPreview); + } + + private void notifyLinkPreviewLoaded () { + if (isDestroyed) { + return; + } + for (RunnableData callback : loadCallbacks) { + callback.runWithData(this); + } + for (int index = staticLoadCallbacks.size() - 1; index >= 0; index--) { + staticLoadCallbacks.get(index).runWithData(this); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/helper/LiveLocationHelper.java b/app/src/main/java/org/thunderdog/challegram/helper/LiveLocationHelper.java index c079fc4ff7..ab27d235b6 100644 --- a/app/src/main/java/org/thunderdog/challegram/helper/LiveLocationHelper.java +++ b/app/src/main/java/org/thunderdog/challegram/helper/LiveLocationHelper.java @@ -124,7 +124,7 @@ public LiveLocationHelper init () { TdApi.Message[] msgs = ((TdApi.Messages) object).messages; messages.ensureCapacity(msgs.length); for (TdApi.Message message : msgs) { - if (message.content.getConstructor() != TdApi.MessageLocation.CONSTRUCTOR || ((TdApi.MessageLocation) message.content).expiresIn == 0) { + if (!Td.isLocation(message.content) || ((TdApi.MessageLocation) message.content).expiresIn == 0) { continue; } messages.add(message); @@ -819,21 +819,21 @@ private void removeChatMessages (long[] messageIds) { @Override public void onNewMessage (TdApi.Message message) { - if (!message.isOutgoing && message.sendingState == null && message.schedulingState == null && message.content.getConstructor() == TdApi.MessageLocation.CONSTRUCTOR && ((TdApi.MessageLocation) message.content).livePeriod > 0 && ((TdApi.MessageLocation) message.content).expiresIn > 0) { + if (!message.isOutgoing && message.sendingState == null && message.schedulingState == null && Td.isLocation(message.content) && ((TdApi.MessageLocation) message.content).livePeriod > 0 && ((TdApi.MessageLocation) message.content).expiresIn > 0) { UI.post(() -> addChatMessage(message)); } } @Override public void onMessageSendSucceeded (TdApi.Message message, long oldMessageId) { - if (message.content.getConstructor() == TdApi.MessageLocation.CONSTRUCTOR && message.schedulingState == null && ((TdApi.MessageLocation) message.content).livePeriod > 0 && ((TdApi.MessageLocation) message.content).expiresIn > 0) { + if (Td.isLocation(message.content) && message.schedulingState == null && ((TdApi.MessageLocation) message.content).livePeriod > 0 && ((TdApi.MessageLocation) message.content).expiresIn > 0) { UI.post(() -> addChatMessage(message)); } } @Override public void onMessageContentChanged (long chatId, long messageId, TdApi.MessageContent newContent) { - if (newContent.getConstructor() == TdApi.MessageLocation.CONSTRUCTOR && ((TdApi.MessageLocation) newContent).livePeriod > 0) { + if (Td.isLocation(newContent) && ((TdApi.MessageLocation) newContent).livePeriod > 0) { UI.post(() -> editChatMessage(messageId, (TdApi.MessageLocation) newContent)); } } diff --git a/app/src/main/java/org/thunderdog/challegram/loader/AvatarReceiver.java b/app/src/main/java/org/thunderdog/challegram/loader/AvatarReceiver.java index b6e2a3dbdb..0e4595aa7c 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/AvatarReceiver.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/AvatarReceiver.java @@ -17,7 +17,9 @@ import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.telegram.ChatListener; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccount; import org.thunderdog.challegram.telegram.TdlibCache; +import org.thunderdog.challegram.telegram.TdlibManager; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.PropertyId; import org.thunderdog.challegram.theme.Theme; @@ -29,6 +31,7 @@ import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.util.DrawableProvider; +import org.thunderdog.challegram.util.text.Letters; import org.thunderdog.challegram.util.text.Text; import org.thunderdog.challegram.util.text.TextColorSets; @@ -173,7 +176,16 @@ public void setPrimaryPlaceholderRadius (float radius) { // Public API public boolean requestPlaceholder (Tdlib tdlib, AvatarPlaceholder.Metadata specificPlaceholder, @Options int options) { - return requestData(tdlib, DataType.PLACEHOLDER, specificPlaceholder != null ? specificPlaceholder.colorId : 0, specificPlaceholder, null, null, options | Options.NO_UPDATES); + int nonZeroId; + if (specificPlaceholder != null) { + nonZeroId = specificPlaceholder.accentColor.getId(); + if (nonZeroId >= 0) { + nonZeroId = nonZeroId + 1; + } + } else { + nonZeroId = 0; + } + return requestData(tdlib, DataType.PLACEHOLDER, nonZeroId, specificPlaceholder, null, null, null, options | Options.NO_UPDATES); } public boolean isDisplayingPlaceholder (AvatarPlaceholder.Metadata specificPlaceholder) { @@ -183,7 +195,7 @@ public boolean isDisplayingPlaceholder (AvatarPlaceholder.Metadata specificPlace } public boolean requestSpecific (Tdlib tdlib, FullChatPhoto specificPhoto, @Options int options) { - return requestData(tdlib, DataType.SPECIFIC_PHOTO, specificPhoto != null ? specificPhoto.chatPhoto.id : 0, null, specificPhoto, null, options | Options.NO_UPDATES); + return requestData(tdlib, DataType.SPECIFIC_PHOTO, specificPhoto != null ? specificPhoto.chatPhoto.id : 0, null, specificPhoto, null, null, options | Options.NO_UPDATES); } public boolean isDisplayingSpecificPhoto (TdApi.ChatPhoto specificPhoto) { @@ -193,7 +205,11 @@ public boolean isDisplayingSpecificPhoto (TdApi.ChatPhoto specificPhoto) { } public boolean requestSpecific (Tdlib tdlib, ImageFile specificFile, @Options int options) { - return requestData(tdlib, DataType.SPECIFIC_FILE, specificFile != null ? specificFile.getId() : 0, null, null, specificFile, options); + return requestData(tdlib, DataType.SPECIFIC_FILE, specificFile != null ? specificFile.getId() : 0, null, null, specificFile, null, options); + } + + public boolean requestSpecific (Tdlib tdlib, TdApi.ChatPhotoInfo photoInfo, @Options int options) { + return requestData(tdlib, DataType.SPECIFIC_PHOTO_INFO, BitwiseUtils.mergeLong(photoInfo.small.id, photoInfo.big.id), null, null, null, photoInfo, options | Options.NO_UPDATES); } public void requestMessageSender (@Nullable Tdlib tdlib, @Nullable TdApi.MessageSender sender, @Options int options) { @@ -222,7 +238,19 @@ public void requestMessageSender (@Nullable Tdlib tdlib, @Nullable TdApi.Message } public boolean requestUser (Tdlib tdlib, long userId, @Options int options) { - return requestData(tdlib, DataType.USER, userId, null, null, null, options); + return requestData(tdlib, DataType.USER, userId, null, null, null, null, options); + } + + public boolean requestAccount (Tdlib tdlib, int accountId, @Options int options) { + // TODO subscribe for updates + TdlibAccount account = TdlibManager.instanceForAccountId(accountId).account(accountId); + AvatarPlaceholder.Metadata placeholder = account.getAvatarPlaceholderMetadata(); + ImageFile imageFile = account.getAvatarFile(false); + if (imageFile != null) { + return requestSpecific(tdlib, imageFile, options); + } else { + return requestPlaceholder(tdlib, placeholder, options); + } } public boolean isDisplayingUser (long userId) { @@ -230,7 +258,7 @@ public boolean isDisplayingUser (long userId) { } public boolean requestChat (Tdlib tdlib, long chatId, @Options int options) { - return requestData(tdlib, DataType.CHAT, chatId, null, null, null, options); + return requestData(tdlib, DataType.CHAT, chatId, null, null, null, null, options); } public boolean isDisplayingChat (long chatId) { @@ -263,6 +291,7 @@ public boolean isDisplayingBasicGroupChat (long basicGroupId) { DataType.PLACEHOLDER, DataType.SPECIFIC_PHOTO, DataType.SPECIFIC_FILE, + DataType.SPECIFIC_PHOTO_INFO, DataType.USER, DataType.CHAT }) @@ -272,8 +301,9 @@ public boolean isDisplayingBasicGroupChat (long basicGroupId) { PLACEHOLDER = 1, SPECIFIC_PHOTO = 2, SPECIFIC_FILE = 3, - USER = 4, - CHAT = 5; + SPECIFIC_PHOTO_INFO = 4, + USER = 5, + CHAT = 6; } private Tdlib tdlib; @@ -282,6 +312,7 @@ public boolean isDisplayingBasicGroupChat (long basicGroupId) { private AvatarPlaceholder.Metadata specificPlaceholder; private FullChatPhoto specificPhoto; private ImageFile specificFile; + private TdApi.ChatPhotoInfo specificPhotoInfo; private @Options int options; private void subscribeToUpdates () { @@ -289,6 +320,7 @@ private void subscribeToUpdates () { case DataType.NONE: case DataType.PLACEHOLDER: case DataType.SPECIFIC_FILE: + case DataType.SPECIFIC_PHOTO_INFO: break; case DataType.SPECIFIC_PHOTO: if (this.additionalDataId != 0) { @@ -331,6 +363,7 @@ private void unsubscribeFromUpdates () { case DataType.NONE: case DataType.PLACEHOLDER: case DataType.SPECIFIC_FILE: + case DataType.SPECIFIC_PHOTO_INFO: break; case DataType.SPECIFIC_PHOTO: if (this.additionalDataId != 0) { @@ -362,7 +395,7 @@ private void unsubscribeFromUpdates () { } @UiThread - private boolean requestData (@Nullable Tdlib tdlib, @DataType int dataType, long dataId, @Nullable AvatarPlaceholder.Metadata specificPlaceholder, FullChatPhoto specificPhoto, ImageFile specificFile, @Options int options) { + private boolean requestData (@Nullable Tdlib tdlib, @DataType int dataType, long dataId, @Nullable AvatarPlaceholder.Metadata specificPlaceholder, FullChatPhoto specificPhoto, ImageFile specificFile, TdApi.ChatPhotoInfo specificPhotoInfo, @Options int options) { if (!UI.inUiThread()) throw new IllegalStateException(); if (dataType == DataType.NONE || dataId == 0 || (tdlib == null && !(dataType == DataType.SPECIFIC_FILE || dataType == DataType.PLACEHOLDER))) { @@ -372,6 +405,7 @@ private boolean requestData (@Nullable Tdlib tdlib, @DataType int dataType, long specificPhoto = null; specificPlaceholder = null; specificFile = null; + specificPhotoInfo = null; options = Options.NONE; } if (this.tdlib != tdlib || this.dataType != dataType || this.dataId != dataId) { @@ -383,6 +417,7 @@ private boolean requestData (@Nullable Tdlib tdlib, @DataType int dataType, long this.specificPlaceholder = specificPlaceholder; this.specificPhoto = specificPhoto; this.specificFile = specificFile; + this.specificPhotoInfo = specificPhotoInfo; this.options = options; if (dataType == DataType.CHAT) { switch (ChatId.getType(dataId)) { @@ -485,7 +520,7 @@ private void updateForumState (boolean isUpdate) { break; } case DataType.SPECIFIC_PHOTO: { - setIsForum(BitwiseUtils.hasFlag(options, Options.FORCE_FORUM) || (specificPhoto != null && tdlib.chatForum(specificPhoto.chatId)), isUpdate); + setIsForum(BitwiseUtils.hasFlag(options, Options.FORCE_FORUM) || (specificPhoto != null && tdlib.isForum(specificPhoto.chatId)), isUpdate); break; } case DataType.SPECIFIC_FILE: @@ -495,7 +530,7 @@ private void updateForumState (boolean isUpdate) { break; } case DataType.CHAT: { - setIsForum(BitwiseUtils.hasFlag(options, Options.FORCE_FORUM) || tdlib.chatForum(dataId), isUpdate); + setIsForum(BitwiseUtils.hasFlag(options, Options.FORCE_FORUM) || tdlib.isForum(dataId), isUpdate); break; } } @@ -560,6 +595,14 @@ private void requestResources (boolean isUpdate) { } break; } + case DataType.SPECIFIC_PHOTO_INFO: { + if (specificPhotoInfo != null) { + requestPhoto(specificPhotoInfo, null, BitwiseUtils.hasFlag(options, Options.FORCE_ANIMATION), options); + } else { + requestEmpty(); + } + break; + } case DataType.PLACEHOLDER: { requestPlaceholder(specificPlaceholder, options); break; @@ -585,7 +628,7 @@ private void requestResources (boolean isUpdate) { } case DataType.CHAT: { TdApi.Chat chat = tdlib.chat(dataId); - setIsForum(tdlib.chatForum(dataId), isUpdate); + setIsForum(tdlib.isForum(dataId), isUpdate); boolean allowAnimation = BitwiseUtils.hasFlag(options, Options.FORCE_ANIMATION) || tdlib.needAvatarPreviewAnimation(dataId); TdApi.ChatPhotoInfo chatPhotoInfo = chat != null && !tdlib.isSelfChat(dataId) ? chat.photo : null; if (chatPhotoInfo == null) { @@ -904,8 +947,9 @@ public int getTargetHeight () { return primaryReceiver().getTargetHeight(); } + /** @noinspection unchecked*/ @Override - public void setUpdateListener (ReceiverUpdateListener listener) { + public final AvatarReceiver setUpdateListener (ReceiverUpdateListener listener) { if (listener != null) { complexReceiver.setUpdateListener( (receiver, key) -> @@ -914,6 +958,7 @@ public void setUpdateListener (ReceiverUpdateListener listener) { } else { complexReceiver.setUpdateListener(null); } + return this; } @Override @@ -978,7 +1023,7 @@ public void clear () { @Override public void destroy () { - requestData(null, DataType.NONE, 0, null, null, null, Options.NONE); + requestData(null, DataType.NONE, 0, null, null, null, null, Options.NONE); } @Override @@ -1131,12 +1176,11 @@ public void draw (Canvas c) { } } } else if (requestedPlaceholder != null) { - int toColorId = Theme.avatarSmallToBig(requestedPlaceholder.colorId); - int placeholderColor = toColorId != 0 ? ColorUtils.fromToArgb( - Theme.getColor(requestedPlaceholder.colorId), - Theme.getColor(toColorId), + int placeholderColor = ColorUtils.fromToArgb( + requestedPlaceholder.accentColor.getPrimaryColor(), + requestedPlaceholder.accentColor.getPrimaryBigColor(), isFullScreen.getFloatValue() - ) : Theme.getColor(requestedPlaceholder.colorId); + ); drawPlaceholderRounded(c, displayRadius, ColorUtils.alphaColor(alpha, placeholderColor)); int avatarContentColorId = ColorId.avatar_content; float primaryContentAlpha = requestedPlaceholder.extraDrawableRes != 0 ? 1f - isFullScreen.getFloatValue() : 1f; @@ -1167,18 +1211,18 @@ public void draw (Canvas c) { private Text displayingLetters; private float displayingLettersTextSize; - private void drawPlaceholderLetters (Canvas c, String letters, float alpha) { - if (StringUtils.isEmpty(letters)) { + private void drawPlaceholderLetters (Canvas c, Letters letters, float alpha) { + if (letters == null || StringUtils.isEmpty(letters.text)) { return; } - float currentRadiusPx = getWidth() / 2f; + float currentRadiusPx = Math.min(getWidth(), getHeight()) / 2f; float textSizeDp = (int) ((primaryPlaceholderRadius != 0 ? primaryPlaceholderRadius : Screen.px(currentRadiusPx)) * .75f); - if (displayingLetters == null || !displayingLetters.getText().equals(letters) || displayingLettersTextSize != textSizeDp) { + if (displayingLetters == null || !displayingLetters.getText().equals(letters.text) || displayingLettersTextSize != textSizeDp) { displayingLetters = new Text.Builder( - letters, (int) (currentRadiusPx * 3), Paints.robotoStyleProvider(textSizeDp), TextColorSets.Regular.AVATAR_CONTENT) + letters.text, (int) (currentRadiusPx * 3), Paints.robotoStyleProvider(textSizeDp), TextColorSets.Regular.AVATAR_CONTENT) .allBold() .singleLine() .build(); @@ -1186,8 +1230,12 @@ private void drawPlaceholderLetters (Canvas c, String letters, float alpha) { } float radiusPx = primaryPlaceholderRadius != 0f ? Screen.dp(primaryPlaceholderRadius) : currentRadiusPx; - float scale = radiusPx < currentRadiusPx ? radiusPx / (float) currentRadiusPx : 1f; - scale *= Math.min(1f, (radiusPx * 2f) / (float) (Math.max(displayingLetters.getWidth(), displayingLetters.getHeight()))); + float scale = radiusPx < currentRadiusPx ? radiusPx / currentRadiusPx : 1f; + float size = Math.max(displayingLetters.getWidth(), displayingLetters.getHeight()); + float maxSize = (float) Math.sqrt(2.0) * (currentRadiusPx - Screen.dp(1f)); + if (size > maxSize) { + scale *= maxSize / size; + } float centerX = centerX(); float centerY = centerY(); @@ -1207,14 +1255,18 @@ private void drawPlaceholderLetters (Canvas c, String letters, float alpha) { } private void drawPlaceholderDrawable (Canvas c, int resId, int colorId, float alpha) { - float currentRadiusPx = getWidth() / 2f; + float currentRadiusPx = Math.min(getWidth(), getHeight()) / 2f; float radiusPx = primaryPlaceholderRadius != 0f ? Screen.dp(primaryPlaceholderRadius) : currentRadiusPx; View view = getTargetView(); Drawable drawable = view instanceof DrawableProvider ? ((DrawableProvider) view).getSparseDrawable(resId, colorId) : Drawables.get(resId); float scale = radiusPx < currentRadiusPx ? radiusPx / currentRadiusPx : 1f; - scale *= Math.min(1f, (radiusPx * 2f) / (float) Math.max(drawable.getMinimumWidth(), drawable.getMinimumHeight())); + float size = Math.max(drawable.getMinimumWidth(), drawable.getMinimumHeight()) * scale; + float maxSize = (float) Math.sqrt(2.0) * (currentRadiusPx - Screen.dp(1f)); + if (size > maxSize) { + scale *= maxSize / size; + } float centerX = centerX(); float centerY = centerY(); final boolean needRestore = scale != 1f; diff --git a/app/src/main/java/org/thunderdog/challegram/loader/ComplexReceiver.java b/app/src/main/java/org/thunderdog/challegram/loader/ComplexReceiver.java index 5330c16691..6fbb05ea20 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/ComplexReceiver.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/ComplexReceiver.java @@ -16,18 +16,22 @@ import android.view.View; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.collection.LongSparseArray; import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.receiver.RefreshRateLimiter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + import me.vkryl.core.lambda.Destroyable; import me.vkryl.core.lambda.RunnableData; public class ComplexReceiver implements Destroyable { public interface KeyFilter { - boolean filterKey (int receiverType, Receiver receiver, long key); + boolean filterKey (@ReceiverType int receiverType, Receiver receiver, long key); } private final View view; @@ -107,7 +111,7 @@ private static void clearReceiver (LongSparseArray targe } } - private static void clearReceivers (LongSparseArray target, int receiverType, @Nullable KeyFilter filter) { + private static void clearReceivers (LongSparseArray target, @ReceiverType int receiverType, @Nullable KeyFilter filter) { int size = target.size(); for (int i = 0; i < size; i++) { Receiver receiver = target.valueAt(i); @@ -117,16 +121,22 @@ private static void clearReceivers (LongSparseArray targ } } - public static final int RECEIVER_TYPE_PREVIEW = 0; - public static final int RECEIVER_TYPE_IMAGE = 1; - public static final int RECEIVER_TYPE_GIF = 2; - public static final int RECEIVER_TYPE_AVATAR = 3; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ReceiverType.PREVIEW, + ReceiverType.IMAGE, + ReceiverType.GIF, + ReceiverType.AVATAR + }) + public @interface ReceiverType { + int PREVIEW = 0, IMAGE = 1, GIF = 2, AVATAR = 3; + } public void clearReceivers (@Nullable KeyFilter filter) { - clearReceivers(imageReceivers, RECEIVER_TYPE_IMAGE, filter); - clearReceivers(gifReceivers, RECEIVER_TYPE_GIF, filter); - clearReceivers(previews, RECEIVER_TYPE_PREVIEW, filter); - clearReceivers(avatarReceivers, RECEIVER_TYPE_AVATAR, filter); + clearReceivers(imageReceivers, ReceiverType.IMAGE, filter); + clearReceivers(gifReceivers, ReceiverType.GIF, filter); + clearReceivers(previews, ReceiverType.PREVIEW, filter); + clearReceivers(avatarReceivers, ReceiverType.AVATAR, filter); } public void clearReceivers (long key) { diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/DayChangeListener.java b/app/src/main/java/org/thunderdog/challegram/loader/ComplexReceiverProvider.java similarity index 75% rename from app/src/main/java/org/thunderdog/challegram/telegram/DayChangeListener.java rename to app/src/main/java/org/thunderdog/challegram/loader/ComplexReceiverProvider.java index 1053081075..1b951731c7 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/DayChangeListener.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/ComplexReceiverProvider.java @@ -10,10 +10,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * - * File created on 15/02/2018 + * File created on 13/09/2023 */ -package org.thunderdog.challegram.telegram; +package org.thunderdog.challegram.loader; -public interface DayChangeListener { - void onDayChanged (); +public interface ComplexReceiverProvider { + ComplexReceiver getComplexReceiver (); } diff --git a/app/src/main/java/org/thunderdog/challegram/loader/DoubleImageReceiver.java b/app/src/main/java/org/thunderdog/challegram/loader/DoubleImageReceiver.java index c4161b6b50..3d43bc4a91 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/DoubleImageReceiver.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/DoubleImageReceiver.java @@ -38,10 +38,12 @@ public DoubleImageReceiver (View view, int radius, boolean animated) { } } + /** @noinspection unchecked*/ @Override - public void setUpdateListener (ReceiverUpdateListener listener) { + public final DoubleImageReceiver setUpdateListener (ReceiverUpdateListener listener) { preview.setUpdateListener(listener); receiver.setUpdateListener(listener); + return this; } public void setAnimationDisabled (boolean disabled) { @@ -118,15 +120,9 @@ public void setPaintAlpha (float alpha) { } @Override - public void setColorFilter (int colorFilter) { - preview.setColorFilter(colorFilter); - receiver.setColorFilter(colorFilter); - } - - @Override - public void disableColorFilter () { - preview.disableColorFilter(); - receiver.disableColorFilter(); + public void setPorterDuffColorFilter (int colorOrColorId, float alpha, boolean colorIsId) { + preview.setPorterDuffColorFilter(colorOrColorId, alpha, colorIsId); + receiver.setPorterDuffColorFilter(colorOrColorId, alpha, colorIsId); } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/loader/ImageFile.java b/app/src/main/java/org/thunderdog/challegram/loader/ImageFile.java index 61b8502467..751697aa98 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/ImageFile.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/ImageFile.java @@ -17,6 +17,7 @@ import android.os.Build; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.palette.graphics.Palette; import org.drinkless.tdlib.TdApi; @@ -566,14 +567,14 @@ public boolean setPaintState (PaintState state) { // TTL - private int ttl; + private @Nullable TdApi.MessageSelfDestructType selfDestructType; - public int getTTL () { - return ttl; + public @Nullable TdApi.MessageSelfDestructType getSelfDestructType () { + return selfDestructType; } - public void setTTL (int ttl) { - this.ttl = ttl; + public void setSelfDestructType (@Nullable TdApi.MessageSelfDestructType selfDestructType) { + this.selfDestructType = selfDestructType; } public static ImageFile copyOf (ImageFile imageFile) { diff --git a/app/src/main/java/org/thunderdog/challegram/loader/ImageFormatDetector.java b/app/src/main/java/org/thunderdog/challegram/loader/ImageFormatDetector.java new file mode 100644 index 0000000000..f6d02f3f2d --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/loader/ImageFormatDetector.java @@ -0,0 +1,70 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 31/12/2023 + */ +package org.thunderdog.challegram.loader; + +import org.thunderdog.challegram.Log; + +import java.io.FileInputStream; + +public class ImageFormatDetector { + public static String getImageFormat (String filePath) { + byte[] buffer = new byte[8]; + boolean isRead = false; + try (FileInputStream fis = new FileInputStream(filePath)) { + if (fis.read(buffer, 0, 8) == 8) { + isRead = true; + } + } catch (Throwable t) { + Log.i("Unable to detect image format", t); + } + if (isRead) { + if (isJPEG(buffer)) { + return "image/jpeg"; + } else if (isPNG(buffer)) { + return "image/png"; + } else if (isGIF(buffer)) { + return "image/gif"; + } else if (isWebP(buffer)) { + return "image/webp"; + } else if (isBMP(buffer)) { + return "image/bmp"; + } + } + return null; + } + + private static boolean isJPEG (byte[] buffer) { + return buffer[0] == (byte) 0xFF && buffer[1] == (byte) 0xD8; + } + + private static boolean isPNG (byte[] buffer) { + return buffer[0] == (byte) 0x89 && buffer[1] == (byte) 0x50 && buffer[2] == (byte) 0x4E && buffer[3] == (byte) 0x47 + && buffer[4] == (byte) 0x0D && buffer[5] == (byte) 0x0A && buffer[6] == (byte) 0x1A && buffer[7] == (byte) 0x0A; + } + + private static boolean isGIF (byte[] buffer) { + return buffer[0] == (byte) 0x47 && buffer[1] == (byte) 0x49 && buffer[2] == (byte) 0x46 && buffer[3] == (byte) 0x38 + && (buffer[4] == (byte) 0x37 || buffer[4] == (byte) 0x39) && buffer[5] == (byte) 0x61; + } + + private static boolean isWebP (byte[] buffer) { + return buffer[0] == (byte) 0x52 && buffer[1] == (byte) 0x49 && buffer[2] == (byte) 0x46 && buffer[3] == (byte) 0x46 + && buffer[8] == (byte) 0x57 && buffer[9] == (byte) 0x45 && buffer[10] == (byte) 0x42 && buffer[11] == (byte) 0x50; + } + + private static boolean isBMP (byte[] buffer) { + return buffer[0] == (byte) 0x42 && buffer[1] == (byte) 0x4D; + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/loader/ImageGalleryFile.java b/app/src/main/java/org/thunderdog/challegram/loader/ImageGalleryFile.java index 21260d636d..b7c31677c0 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/ImageGalleryFile.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/ImageGalleryFile.java @@ -221,7 +221,7 @@ public long getDateTaken () { } public boolean canSendAsFile () { - if (getTTL() > 0) + if (getSelfDestructType() != null) return false; if (isVideo()) { return VideoGenerationInfo.canSendInOriginalQuality(this); diff --git a/app/src/main/java/org/thunderdog/challegram/loader/ImageLoader.java b/app/src/main/java/org/thunderdog/challegram/loader/ImageLoader.java index 50dd1c065d..fc09bf8a9c 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/ImageLoader.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/ImageLoader.java @@ -127,7 +127,7 @@ record = new ImageWatchers(file, actor, reference); }); } else { if (!Config.DEBUG_DISABLE_DOWNLOAD) { - tdlib.client().send(new TdApi.DownloadFile(fileId, 32, 0, 0, false), tdlib.imageLoadHandler()); + tdlib.send(new TdApi.DownloadFile(fileId, 32, 0, 0, false), tdlib.imageLoadHandler()); } } } else { @@ -171,7 +171,7 @@ void downloadFilePersistent (final ImageFileRemote persistentFile, final TdApi.F onLoad(tdlib, file); } else { if (!Config.DEBUG_DISABLE_DOWNLOAD) { - tdlib.client().send(new TdApi.DownloadFile(file.id, 1, 0, 0, false), tdlib.imageLoadHandler()); + tdlib.send(new TdApi.DownloadFile(file.id, 1, 0, 0, false), tdlib.imageLoadHandler()); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/loader/ImageReader.java b/app/src/main/java/org/thunderdog/challegram/loader/ImageReader.java index dd1a4ae0b0..8640c4dd4f 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/ImageReader.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/ImageReader.java @@ -265,21 +265,35 @@ public void onHeaderDecoded (@NonNull android.graphics.ImageDecoder decoder, @No File cacheFile = new File(path); - Bitmap bitmap; + Bitmap bitmap = null; try { - if (file.isWebp() && Config.useBundledWebp()) { - RandomAccessFile f = new RandomAccessFile(cacheFile, "r"); - ByteBuffer buffer = f.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, cacheFile.length()); + boolean webpDecoderFailed = false; + boolean isWebP = file.isWebp() && Config.useBundledWebp(); + if (isWebP) { + try (RandomAccessFile f = new RandomAccessFile(cacheFile, "r")) { + ByteBuffer buffer = f.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, cacheFile.length()); + + BitmapFactory.Options bmOptions = new BitmapFactory.Options(); + bmOptions.inJustDecodeBounds = true; + N.loadWebpImage(null, buffer, buffer.limit(), bmOptions, true); + bitmap = Bitmap.createBitmap(bmOptions.outWidth, bmOptions.outHeight, Bitmap.Config.ARGB_8888); + N.loadWebpImage(bitmap, buffer, buffer.limit(), null, !opts.inPurgeable); + } catch (Throwable t) { + Log.e(Log.TAG_IMAGE_LOADER, "#%s: Cannot load bitmap, config: %s", t, file.toString(), opts.inPreferredConfig.toString()); + webpDecoderFailed = true; + } + } - BitmapFactory.Options bmOptions = new BitmapFactory.Options(); - bmOptions.inJustDecodeBounds = true; - N.loadWebpImage(null, buffer, buffer.limit(), bmOptions, true); - bitmap = Bitmap.createBitmap(bmOptions.outWidth, bmOptions.outHeight, Bitmap.Config.ARGB_8888); - N.loadWebpImage(bitmap, buffer, buffer.limit(), null, !opts.inPurgeable); + if (webpDecoderFailed) { + String mimeType = ImageFormatDetector.getImageFormat(cacheFile.getPath()); + if (mimeType != null && !"image/webp".equals(mimeType)) { + Log.e(Log.TAG_IMAGE_LOADER, "#%s: Not WebP, retry with system decoder: %s", file.toString(), mimeType); + isWebP = false; + } + } - f.close(); - } else { + if (!isWebP) { if (opts.inPurgeable) { RandomAccessFile f = null; for (int attempt = 0; attempt < 2; attempt++) { // fixme stupid fix for EACCESS on early applaunch requests diff --git a/app/src/main/java/org/thunderdog/challegram/loader/ImageReceiver.java b/app/src/main/java/org/thunderdog/challegram/loader/ImageReceiver.java index 25eb6d383d..e9e8ea3b9b 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/ImageReceiver.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/ImageReceiver.java @@ -21,8 +21,6 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; @@ -37,11 +35,14 @@ import org.thunderdog.challegram.U; import org.thunderdog.challegram.mediaview.crop.CropState; import org.thunderdog.challegram.mediaview.paint.PaintState; +import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.DrawAlgorithms; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.UI; import me.vkryl.android.AnimatorUtils; +import me.vkryl.core.ColorUtils; import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; @@ -65,7 +66,7 @@ public class ImageReceiver implements Watcher, ValueAnimator.AnimatorUpdateListe private final Rect drawRegion, bitmapRect; - private final Paint bitmapPaint; + private final Paint metadataPaint; private Matrix bitmapMatrix; private Paint roundPaint; // rounded corners @@ -87,7 +88,7 @@ public ImageReceiver (View view, int radius) { float density = UI.getResources().getDisplayMetrics().density; ANIMATION_ENABLED = density >= 2.0f; //(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && density >= 2f) || (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && density > 2f); } - this.bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG); + this.metadataPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG); this.view = view; this.reference = new WatcherReference(this); this.drawRegion = new Rect(); @@ -98,9 +99,11 @@ public ImageReceiver (View view, int radius) { } } + /** @noinspection unchecked*/ @Override - public void setUpdateListener (ReceiverUpdateListener listener) { + public final ImageReceiver setUpdateListener (ReceiverUpdateListener listener) { this.updateListener = listener; + return this; } @Override @@ -248,35 +251,15 @@ public int getTargetHeight () { return sourceHeight; } - private boolean hasColorFilter; - private int colorFilter; + private int porterDuffColor = ColorId.NONE; + private float porterDuffAlpha; + private boolean porterDuffColorIsId = true; @Override - public void setColorFilter (int colorFilter) { - if (!this.hasColorFilter || this.colorFilter != colorFilter) { - this.hasColorFilter = true; - this.colorFilter = colorFilter; - - PorterDuffColorFilter duffColorFilter = new PorterDuffColorFilter(colorFilter, PorterDuff.Mode.SRC_IN); - this.bitmapPaint.setColorFilter(duffColorFilter); - if (repeatPaint != null) { - this.repeatPaint.setColorFilter(duffColorFilter); - } - - invalidate(); - } - } - - @Override - public void disableColorFilter () { - if (this.hasColorFilter) { - this.hasColorFilter = false; - this.bitmapPaint.setColorFilter(null); - if (repeatPaint != null) { - this.repeatPaint.setColorFilter(null); - } - invalidate(); - } + public void setPorterDuffColorFilter (int colorOrColorId, float alpha, boolean colorIsId) { + this.porterDuffColor = colorOrColorId; + this.porterDuffAlpha = alpha; + this.porterDuffColorIsId = colorIsId; } public void setAnimationDisabled (boolean animationDisabled) { @@ -297,7 +280,7 @@ public void setAnimationDisabled (boolean animationDisabled) { if (repeatPaint != null) { repeatPaint.setAlpha((int) (255f * alpha)); } - bitmapPaint.setAlpha((int) (255f * alpha)); + metadataPaint.setAlpha((int) (255f * alpha)); } invalidate(); @@ -322,7 +305,7 @@ public void setRadius (float radius) { if (roundPaint == null) { roundPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); - roundPaint.setAlpha(bitmapPaint.getAlpha()); + roundPaint.setAlpha(metadataPaint.getAlpha()); shaderMatrix = new Matrix(); bitmapRectF = new RectF(); roundRect = new RectF(); @@ -434,9 +417,20 @@ private int getCroppedHeight (int sourceHeight) { return sourceHeight; } + private int getVisualRotationWithoutCropState () { + if (file != null) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && file instanceof ImageGalleryFile && ((ImageGalleryFile) file).needThumb() ? 0 : file.getVisualRotation(); + } + return 0; + } + + private boolean needSwapMirrorAxis () { + return file != null && U.isRotated(Math.abs(getVisualRotationWithoutCropState() - file.getVisualRotation())); + } + private int getVisualRotation () { if (file != null) { - int rotation = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && file instanceof ImageGalleryFile && ((ImageGalleryFile) file).needThumb() ? 0 : file.getVisualRotation(); + int rotation = getVisualRotationWithoutCropState(); if (displayCrop != null) { rotation = MathUtils.modulo(rotation + displayCrop.getRotateBy(), 360); } @@ -635,6 +629,7 @@ public int getBottom () { return bottom; } + @Override public void setAlpha (@FloatRange(from = 0.0, to = 1.0) float alpha) { if (ANIMATION_ENABLED && !animationDisabled && this.alpha != alpha) { this.alpha = alpha; @@ -645,7 +640,7 @@ public void setAlpha (@FloatRange(from = 0.0, to = 1.0) float alpha) { if (repeatPaint != null) { repeatPaint.setAlpha((int) (255f * alpha)); } - bitmapPaint.setAlpha((int) (255f * alpha)); + metadataPaint.setAlpha((int) (255f * alpha)); } invalidate(); } @@ -671,7 +666,7 @@ public void forceAlpha (float alpha) { if (repeatPaint != null) { repeatPaint.setAlpha((int) (255f * alpha)); } - bitmapPaint.setAlpha((int) (255f * alpha)); + metadataPaint.setAlpha((int) (255f * alpha)); } } @@ -874,8 +869,7 @@ public boolean setBundle (ImageFile file, Bitmap bitmap, boolean local) { if (bitmapChanged) { if (repeatPaint == null) { repeatPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG); - repeatPaint.setAlpha(bitmapPaint.getAlpha()); - repeatPaint.setColorFilter(bitmapPaint.getColorFilter()); + repeatPaint.setAlpha(metadataPaint.getAlpha()); } bitmapShader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); @@ -1081,18 +1075,19 @@ public void postInvalidate () { @Override public float getPaintAlpha () { - return (float) (repeatPaint != null ? repeatPaint.getAlpha() : roundPaint != null ? roundPaint.getAlpha() : bitmapPaint.getAlpha()) / 255f; + return (float) (repeatPaint != null ? repeatPaint.getAlpha() : roundPaint != null ? roundPaint.getAlpha() : metadataPaint.getAlpha()) / 255f; } @Override public void setPaintAlpha (float factor) { - savedAlpha = Color.rgb(repeatPaint != null ? repeatPaint.getAlpha() : 0, roundPaint != null ? roundPaint.getAlpha() : 0, bitmapPaint.getAlpha()); + int bitmapAlpha = metadataPaint.getAlpha(); + savedAlpha = Color.rgb(repeatPaint != null ? repeatPaint.getAlpha() : bitmapAlpha, roundPaint != null ? roundPaint.getAlpha() : bitmapAlpha, bitmapAlpha); final int alpha = (int) (255f * MathUtils.clamp(factor)); if (roundPaint != null) roundPaint.setAlpha(alpha); if (repeatPaint != null) repeatPaint.setAlpha(alpha); - bitmapPaint.setAlpha(alpha); + metadataPaint.setAlpha(alpha); } @Override @@ -1101,7 +1096,7 @@ public void restorePaintAlpha () { repeatPaint.setAlpha(Color.red(savedAlpha)); if (roundPaint != null) roundPaint.setAlpha(Color.green(savedAlpha)); - bitmapPaint.setAlpha(Color.blue(savedAlpha)); + metadataPaint.setAlpha(Color.blue(savedAlpha)); savedAlpha = 0; } @@ -1152,104 +1147,131 @@ public boolean isInsideContent (float x, float y, int emptyWidth, int emptyHeigh return false; } + public Paint getBitmapPaint () { + float alpha = (float) metadataPaint.getAlpha() / 255f; + if (porterDuffColorIsId && porterDuffColor == ColorId.NONE) { + return Paints.bitmapPaint(alpha); + } else if (porterDuffColorIsId) { + return PorterDuffPaint.get(porterDuffColor, porterDuffAlpha * alpha); + } else { + return Paints.getPorterDuffPaint(ColorUtils.alphaColor(porterDuffAlpha * alpha, porterDuffColor)); + } + } + @Override public void draw (Canvas c) { - if (U.isValidBitmap(bitmap)) { - final int rotation = getVisualRotation(); - if (radius != 0) { - if (rotation != 0) { - c.save(); - c.rotate(rotation, left + (right - left) / 2f, top + (bottom - top) / 2f); + if (!U.isValidBitmap(bitmap)) { + return; + } + + Paint paint = getBitmapPaint(); + if (roundPaint != null) { + roundPaint.setColorFilter(paint.getColorFilter()); + } + if (repeatPaint != null) { + repeatPaint.setColorFilter(paint.getColorFilter()); + } + + final int rotation = getVisualRotation(); + if (radius != 0) { + if (rotation != 0) { + c.save(); + c.rotate(rotation, left + (right - left) / 2f, top + (bottom - top) / 2f); + } + drawRoundRect(c, roundRect, radius, radius, roundPaint); + if (rotation != 0) { + c.restore(); + } + } else if (file.getScaleType() == ImageFile.CENTER_REPEAT) { + c.save(); + c.drawRect(left, top, right, bottom, repeatPaint); + c.restore(); + } else { + PaintState paintState = file.getPaintState(); + float scaleType = file.getScaleType(); + final boolean needSwapMirrorAxis = needSwapMirrorAxis(); + final boolean needMirrorHorizontally = displayCrop != null && (needSwapMirrorAxis ? displayCrop.needMirrorVertically() : displayCrop.needMirrorHorizontally()); + final boolean needMirrorVertically = displayCrop != null && (needSwapMirrorAxis ? displayCrop.needMirrorHorizontally() : displayCrop.needMirrorVertically()); + if (scaleType == ImageFile.CENTER_CROP || scaleType == ImageFile.FIT_CENTER) { + // c.drawRect(left, top, right, bottom, Paints.fillingPaint(0xaa00ff00)); + boolean hasCrop = displayCrop != null; + float degrees = 0f; + if (hasCrop) { + degrees = displayCrop.getDegreesAroundCenter(); + hasCrop = degrees != 0f || !displayCrop.isRegionEmpty(); } - drawRoundRect(c, roundRect, radius, radius, roundPaint); + + c.save(); + c.clipRect(left, top, right, bottom); + if (left != 0 || top != 0) { + c.translate(left, top); + } + /*if (hasCrop && displayCrop.needMirror()) { + c.scale(displayCrop.needMirrorHorizontally() ? -1 : 1, displayCrop.needMirrorVertically() ? -1 : 1, (right - left) / 2f, (bottom - top) / 2f); + }*/ if (rotation != 0) { - c.restore(); + c.rotate(rotation, (right - left) / 2f, (bottom - top) / 2f); } - } else if (file.getScaleType() == ImageFile.CENTER_REPEAT) { - c.save(); - c.drawRect(left, top, right, bottom, repeatPaint); - c.restore(); - } else { - PaintState paintState = file.getPaintState(); - float scaleType = file.getScaleType(); - if (scaleType == ImageFile.CENTER_CROP || scaleType == ImageFile.FIT_CENTER) { - // c.drawRect(left, top, right, bottom, Paints.fillingPaint(0xaa00ff00)); - boolean hasCrop = displayCrop != null; - float degrees = 0f; - if (hasCrop) { - degrees = displayCrop.getDegreesAroundCenter(); - hasCrop = degrees != 0f || !displayCrop.isRegionEmpty(); - } - c.save(); - c.clipRect(left, top, right, bottom); - if (left != 0 || top != 0) { - c.translate(left, top); - } - if (rotation != 0) { - c.rotate(rotation, (right - left) / 2f, (bottom - top) / 2f); + if (hasCrop) { + c.concat(bitmapMatrix); + Rect rect = Paints.getRect(); + if (cropApplyFactor < 1f || degrees != 0f || paintState != null) { + int left = croppedRect.left - bitmapRect.left; + int top = croppedRect.top - bitmapRect.top; + c.clipRect(left, top, left + croppedRect.width(), top + croppedRect.height()); } + rect.set(0, 0, bitmapRect.width(), bitmapRect.height()); + if (degrees != 0f) { + c.translate(-bitmapRect.left, -bitmapRect.top); - if (hasCrop) { - c.concat(bitmapMatrix); - Rect rect = Paints.getRect(); - if (cropApplyFactor < 1f || degrees != 0f || paintState != null) { - int left = croppedRect.left - bitmapRect.left; - int top = croppedRect.top - bitmapRect.top; - c.clipRect(left, top, left + croppedRect.width(), top + croppedRect.height()); - } - rect.set(0, 0, bitmapRect.width(), bitmapRect.height()); - if (degrees != 0f) { - c.translate(-bitmapRect.left, -bitmapRect.top); - - float w = bitmap.getWidth(); - float h = bitmap.getHeight(); - double rad = Math.toRadians(degrees); - float sin = (float) Math.abs(Math.sin(rad)); - float cos = (float) Math.abs(Math.cos(rad)); - - // W = w·|cos φ| + h·|sin φ| - // H = w·|sin φ| + h·|cos φ| - - float W = w * cos + h * sin; - float H = w * sin + h * cos; - - float scale = Math.max(W / w, H / h); - float cx = w / 2; - float cy = h / 2; - c.rotate(degrees, cx, cy); - c.scale(scale, scale, cx, cy); - - drawBitmap(c, bitmap, 0, 0, bitmapPaint); - if (paintState != null) { - paintState.draw(c, 0, 0, bitmap.getWidth(), bitmap.getHeight()); - } - } else { - drawBitmap(c, bitmap, bitmapRect, rect, bitmapPaint); - if (paintState != null) { - c.clipRect(rect); - DrawAlgorithms.drawPainting(c, bitmap, bitmapRect, rect, paintState); - } + float w = bitmap.getWidth(); + float h = bitmap.getHeight(); + double rad = Math.toRadians(degrees); + float sin = (float) Math.abs(Math.sin(rad)); + float cos = (float) Math.abs(Math.cos(rad)); + + // W = w·|cos φ| + h·|sin φ| + // H = w·|sin φ| + h·|cos φ| + + float W = w * cos + h * sin; + float H = w * sin + h * cos; + + float scale = Math.max(W / w, H / h); + float cx = w / 2; + float cy = h / 2; + c.rotate(degrees, cx, cy); + c.scale(scale, scale, cx, cy); + + drawBitmap(c, bitmap, 0, 0, needMirrorHorizontally, needMirrorVertically, paint); + if (paintState != null) { + paintState.draw(c, 0, 0, bitmap.getWidth(), bitmap.getHeight()); } } else { - c.concat(bitmapMatrix); - drawBitmap(c, bitmap, 0, 0, bitmapPaint); + drawBitmap(c, bitmap, bitmapRect, rect, needMirrorHorizontally, needMirrorVertically, paint); if (paintState != null) { - c.clipRect(0, 0, bitmap.getWidth(), bitmap.getHeight()); - paintState.draw(c, 0, 0, bitmap.getWidth(), bitmap.getHeight()); + c.clipRect(rect); + DrawAlgorithms.drawPainting(c, bitmap, bitmapRect, rect, paintState); } } - - c.restore(); } else { - drawBitmap(c, bitmap, bitmapRect, drawRegion, bitmapPaint); + c.concat(bitmapMatrix); + drawBitmap(c, bitmap, 0, 0, needMirrorHorizontally, needMirrorVertically, paint); if (paintState != null) { - c.save(); - c.clipRect(drawRegion); - DrawAlgorithms.drawPainting(c, bitmap, bitmapRect, drawRegion, paintState); - c.restore(); + c.clipRect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + paintState.draw(c, 0, 0, bitmap.getWidth(), bitmap.getHeight()); } } + + c.restore(); + } else { + drawBitmap(c, bitmap, bitmapRect, drawRegion, needMirrorHorizontally, needMirrorVertically, paint); + if (paintState != null) { + c.save(); + c.clipRect(drawRegion); + DrawAlgorithms.drawPainting(c, bitmap, bitmapRect, drawRegion, paintState); + c.restore(); + } } } } @@ -1258,18 +1280,42 @@ public interface OnCompleteListener { void onComplete (ImageReceiver receiver, ImageFile imageFile); } - private static void drawBitmap (Canvas c, Bitmap bitmap, float left, float top, Paint paint) { + private static void drawBitmap (Canvas c, Bitmap bitmap, float left, float top, boolean needMirrorHorizontally, boolean needMirrorVertically, Paint paint) { try { + c.save(); + c.scale(needMirrorHorizontally ? -1 : 1, needMirrorVertically ? -1 : 1, left + bitmap.getWidth() / 2f, top + bitmap.getHeight() / 2f); c.drawBitmap(bitmap, left, top, paint); + c.restore(); } catch (Throwable t) { Log.e(Log.TAG_IMAGE_LOADER, "Unable to draw bitmap", t); Tracer.onOtherError(t); } } - private static void drawBitmap (Canvas c, Bitmap bitmap, Rect rect, Rect drawRegion, Paint paint) { + private static final Rect tmpRect = new Rect(); + + private static void drawBitmap (Canvas c, Bitmap bitmap, Rect rect, Rect drawRegion, boolean needMirrorHorizontally, boolean needMirrorVertically, Paint paint) { try { - c.drawBitmap(bitmap, rect, drawRegion, paint); + c.save(); + c.scale(needMirrorHorizontally ? -1 : 1, needMirrorVertically ? -1 : 1, drawRegion.centerX(), drawRegion.centerY()); + tmpRect.set(rect); + + if (needMirrorHorizontally) { + int width = bitmap.getWidth(); + int left = rect.left; + int right = rect.right; + tmpRect.left = width - right; + tmpRect.right = width - left; + } + if (needMirrorVertically) { + int height = bitmap.getHeight(); + int top = rect.top; + int bottom = rect.bottom; + tmpRect.top = height - bottom; + tmpRect.bottom = height - top; + } + c.drawBitmap(bitmap, tmpRect, drawRegion, paint); + c.restore(); } catch (Throwable t) { Log.e(Log.TAG_IMAGE_LOADER, "Unable to draw bitmap", t); Tracer.onOtherError(t); diff --git a/app/src/main/java/org/thunderdog/challegram/loader/Receiver.java b/app/src/main/java/org/thunderdog/challegram/loader/Receiver.java index 632b22ca02..efb0b9c17a 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/Receiver.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/Receiver.java @@ -21,8 +21,12 @@ import android.graphics.RectF; import android.view.View; +import androidx.annotation.ColorInt; + import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.navigation.TooltipOverlayView; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Views; @@ -34,7 +38,7 @@ public interface Receiver extends TooltipOverlayView.LocationProvider { int getTargetWidth (); int getTargetHeight (); - void setUpdateListener (ReceiverUpdateListener listener); + T setUpdateListener (ReceiverUpdateListener listener); int getLeft (); int getTop (); @@ -70,8 +74,18 @@ default void toRect (Rect rect) { rect.set(getLeft(), getTop(), getRight(), getBottom()); } - default void setColorFilter (int colorFilter) { throw new UnsupportedOperationException(); } - default void disableColorFilter () { throw new UnsupportedOperationException(); } + default void setPorterDuffColorFilter (int colorOrColorId, float alpha, boolean colorIsId) { + throw new UnsupportedOperationException(); + } + default void setThemedPorterDuffColorId (@PorterDuffColorId int colorId) { + setPorterDuffColorFilter(colorId, 1f, true); + } + default void setPorterDuffColorFilter (@ColorInt int color) { + setPorterDuffColorFilter(color, 1f, false); + } + default void disablePorterDuffColorFilter () { + setPorterDuffColorFilter(ColorId.NONE, 0f, true); + } default void drawPlaceholderContour (Canvas c, Path path) { drawPlaceholderContour(c, path, 1f); diff --git a/app/src/main/java/org/thunderdog/challegram/loader/gif/GifActor.java b/app/src/main/java/org/thunderdog/challegram/loader/gif/GifActor.java index b341f80a9b..9d3fb77ae3 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/gif/GifActor.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/gif/GifActor.java @@ -342,6 +342,9 @@ public void startDecoding (String path) { case GifFile.OptimizationMode.STICKER_PREVIEW: resolution = Math.min(Math.max(EmojiMediaListController.getEstimateColumnResolution(), StickersListController.getEstimateColumnResolution()), 160); break; + case GifFile.OptimizationMode.EMOJI_PREVIEW: + resolution = Math.min(Screen.dp(40), 120); + break; case GifFile.OptimizationMode.NONE: resolution = Math.min(Screen.dp(TGMessageSticker.MAX_STICKER_SIZE), 384); break; diff --git a/app/src/main/java/org/thunderdog/challegram/loader/gif/GifBridge.java b/app/src/main/java/org/thunderdog/challegram/loader/gif/GifBridge.java index f350a290f7..7dc4e55d0e 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/gif/GifBridge.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/gif/GifBridge.java @@ -66,7 +66,7 @@ private GifBridge () { for (int i = 0; i < emojiThreads.length; i++) { emojiThreads[i] = new GifThread(i); } - lottieThreads = new GifThread[3]; + lottieThreads = new GifThread[5]; for (int i = 0; i < lottieThreads.length; i++) { lottieThreads[i] = new GifThread(i); } @@ -74,6 +74,9 @@ private GifBridge () { private GifThread obtainFrameThread (GifFile file) { if (file.getGifType() == GifFile.TYPE_TG_LOTTIE) { + if (file.isHighPriorityForDecode()) { + return lottieThreads[lottieThreads.length - 1]; + } return lottieThreads[file.getOptimizationMode()]; } else { // TODO rework to executors diff --git a/app/src/main/java/org/thunderdog/challegram/loader/gif/GifFile.java b/app/src/main/java/org/thunderdog/challegram/loader/gif/GifFile.java index 71d1c3a117..805f6aad4b 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/gif/GifFile.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/gif/GifFile.java @@ -48,15 +48,19 @@ public class GifFile { public static final int FLAG_PLAY_ONCE = 1 << 2; public static final int FLAG_UNIQUE = 1 << 3; public static final int FLAG_DECODE_LAST_FRAME = 1 << 4; + public static final int FLAG_HIGH_PRIORITY_FOR_DECODE = 1 << 5; @Retention(RetentionPolicy.SOURCE) @IntDef({ OptimizationMode.NONE, OptimizationMode.STICKER_PREVIEW, - OptimizationMode.EMOJI + OptimizationMode.EMOJI, + OptimizationMode.EMOJI_PREVIEW }) public @interface OptimizationMode { - int NONE = 0, STICKER_PREVIEW = 1, EMOJI = 2; + int NONE = 0, STICKER_PREVIEW = 1, + EMOJI = 2, // for text media + EMOJI_PREVIEW = 3; // for emoji keyboard } protected final Tdlib tdlib; @@ -142,6 +146,19 @@ public interface FrameChangeListener { public void setTotalFrameCount (long totalFrameCount) { this.totalFrameCount = totalFrameCount; + if (onTotalFrameCountLoadListener != null) { + tdlib.ui().post(() -> { + if (onTotalFrameCountLoadListener != null) { + onTotalFrameCountLoadListener.run(); + } + }); + } + } + + private Runnable onTotalFrameCountLoadListener; + + public void setOnTotalFrameCountLoadListener (Runnable onTotalFrameCountLoadListener) { + this.onTotalFrameCountLoadListener = onTotalFrameCountLoadListener; } public boolean hasFrame (long frameNo) { @@ -192,6 +209,14 @@ public boolean setVibrationPattern (int vibrationPattern) { return false; } + public void setHighPriorityForDecode () { + flags = BitwiseUtils.setFlag(flags, FLAG_HIGH_PRIORITY_FOR_DECODE, true); + } + + public boolean isHighPriorityForDecode () { + return BitwiseUtils.hasFlag(flags, FLAG_HIGH_PRIORITY_FOR_DECODE); + } + public void setOptimizationMode (@OptimizationMode int optimizationMode) { this.optimizationMode = optimizationMode; } @@ -201,7 +226,7 @@ public void setOptimizationMode (@OptimizationMode int optimizationMode) { } public boolean isOneTimeCache () { // Delete cache file as soon as file no longer displayed - return optimizationMode == OptimizationMode.EMOJI || optimizationMode == OptimizationMode.STICKER_PREVIEW; + return optimizationMode == OptimizationMode.EMOJI || optimizationMode == OptimizationMode.STICKER_PREVIEW || optimizationMode == OptimizationMode.EMOJI_PREVIEW; } @Deprecated() diff --git a/app/src/main/java/org/thunderdog/challegram/loader/gif/GifReceiver.java b/app/src/main/java/org/thunderdog/challegram/loader/gif/GifReceiver.java index 2b27cb46ad..3dfc5d304d 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/gif/GifReceiver.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/gif/GifReceiver.java @@ -33,17 +33,21 @@ import org.thunderdog.challegram.N; import org.thunderdog.challegram.U; +import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.loader.ReceiverUpdateListener; +import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; import java.lang.ref.WeakReference; import me.vkryl.core.BitwiseUtils; +import me.vkryl.core.ColorUtils; import me.vkryl.core.MathUtils; public class GifReceiver implements GifWatcher, Runnable, Receiver { @@ -96,9 +100,11 @@ public GifReceiver (@Nullable View view) { this.croppedClipRegion = new RectF(); } + /** @noinspection unchecked*/ @Override - public void setUpdateListener (ReceiverUpdateListener listener) { + public final GifReceiver setUpdateListener (ReceiverUpdateListener listener) { this.updateListener = listener; + return this; } private float radius; @@ -717,26 +723,66 @@ public void drawPlaceholder (Canvas c) { } } - private static final int DRAW_BATCH_STARTED = 1; - private static final int DRAW_BATCH_DRAWN = 1 << 1; + private static final int DRAW_BATCH_DRAWN = 1; private int drawBatchFlags; - public void beginDrawBatch () { + public boolean isInDrawBatch () { + return BitwiseUtils.setFlag(drawBatchFlags, DRAW_BATCH_DRAWN, false) != 0; + } + + public void beginDrawBatch (int batchId) { + if (batchId <= 0) { + throw new IllegalArgumentException(Integer.toString(batchId)); + } // Use when drawing the same GifReceiver multiple times on one canvas - drawBatchFlags = DRAW_BATCH_STARTED; + drawBatchFlags = BitwiseUtils.setFlag(drawBatchFlags, (1 << batchId), true); + } + + public void finishAllDrawBatches () { + finishDrawBatch(0); } - public void finishDrawBatch () { - int flags = drawBatchFlags; - if (gif != null && BitwiseUtils.hasFlag(flags, DRAW_BATCH_STARTED) && BitwiseUtils.hasFlag(flags, DRAW_BATCH_DRAWN)) { + public void finishDrawBatch (int batchId) { + if (batchId < 0) + throw new IllegalArgumentException(Integer.toString(batchId)); + int flags; + if (batchId == 0) { + // Drop all batches + flags = (drawBatchFlags & DRAW_BATCH_DRAWN); + } else { + flags = BitwiseUtils.setFlag(drawBatchFlags, (1 << batchId), false); + } + int remainingFlags = BitwiseUtils.setFlag(flags, DRAW_BATCH_DRAWN, false); + if (gif != null && BitwiseUtils.hasFlag(flags, DRAW_BATCH_DRAWN) && isInDrawBatch() && remainingFlags == 0) { synchronized (gif.getBusyList()) { if (gif.hasBitmap()) { gif.getDrawFrame(true); } } } - this.drawBatchFlags = 0; + this.drawBatchFlags = remainingFlags; + } + + private int porterDuffColor = ColorId.NONE; + private float porterDuffAlpha; + private boolean porterDuffColorIsId = true; + + @Override + public void setPorterDuffColorFilter (int colorOrColorId, float alpha, boolean colorIsId) { + this.porterDuffColor = colorOrColorId; + this.porterDuffAlpha = alpha; + this.porterDuffColorIsId = colorIsId; + } + + private static final int[] debugOptimizationColors; + static { + debugOptimizationColors = Config.DEBUG_GIF_OPTIMIZATION_MODE ? new int[]{ + 0x80FFFFFF, // GifFile.OptimizationMode.NONE + 0x80FF0000, // GifFile.OptimizationMode.STICKER_PREVIEW + 0x8000FF00, // GifFile.OptimizationMode.EMOJI + 0x800000FF // GifFile.OptimizationMode.EMOJI_PREVIEW + } : null; } public void draw (Canvas c) { @@ -748,18 +794,24 @@ public void draw (Canvas c) { boolean isFirstFrame = false; synchronized (gif.getBusyList()) { if (gif.hasBitmap()) { - final boolean inBatch = BitwiseUtils.hasFlag(drawBatchFlags, DRAW_BATCH_STARTED); + if (Config.DEBUG_GIF_OPTIMIZATION_MODE) { + c.drawRect(drawRegion, Paints.fillingPaint(debugOptimizationColors[file.getOptimizationMode()])); + } + final boolean inBatch = isInDrawBatch(); if (!inBatch || !BitwiseUtils.hasFlag(drawBatchFlags, DRAW_BATCH_DRAWN)) { gif.applyNext(); if (inBatch) { drawBatchFlags |= DRAW_BATCH_DRAWN; } } - final int alpha = (int) (255f * MathUtils.clamp(this.alpha)); - Paint bitmapPaint = Paints.getBitmapPaint(); - int restoreAlpha = bitmapPaint.getAlpha(); - if (alpha != restoreAlpha) { - bitmapPaint.setAlpha(alpha); + float alpha = MathUtils.clamp(this.alpha); + Paint paint; + if (porterDuffColorIsId && porterDuffColor == ColorId.NONE) { + paint = Paints.bitmapPaint(alpha); + } else if (porterDuffColorIsId) { + paint = PorterDuffPaint.get(porterDuffColor, porterDuffAlpha * alpha); + } else { + paint = Paints.getPorterDuffPaint(ColorUtils.alphaColor(porterDuffAlpha * alpha, porterDuffColor)); } int scaleType = file.getScaleType(); GifState.Frame frame = gif.getDrawFrame(!inBatch); @@ -772,12 +824,12 @@ public void draw (Canvas c) { } else { restoreToCount = -1; } - c.drawRoundRect(croppedClipRegion, radius, radius, shaderPaint(frame.bitmap, bitmapPaint.getAlpha())); + c.drawRoundRect(croppedClipRegion, radius, radius, shaderPaint(frame.bitmap, paint.getAlpha())); if (clip) { Views.restore(c, restoreToCount); } } else if (scaleType != 0) { - c.save(); + final int restoreToCount = Views.save(c); c.clipRect(drawRegion); if (drawRegion.left != 0 || drawRegion.top != 0) { @@ -796,18 +848,18 @@ public void draw (Canvas c) { } c.concat(bitmapMatrix); - c.drawBitmap(frame.bitmap, 0f, 0f, bitmapPaint); + c.drawBitmap(frame.bitmap, 0f, 0f, paint); - c.restore(); + Views.restore(c, restoreToCount); } else { Rect rect = Paints.getRect(); rect.set((int) bitmapRect.left, (int) bitmapRect.top, (int) bitmapRect.right, (int) bitmapRect.bottom); - c.drawBitmap(frame.bitmap, rect, drawRegion, bitmapPaint); - } - if (alpha != restoreAlpha) { - bitmapPaint.setAlpha(restoreAlpha); + c.drawBitmap(frame.bitmap, rect, drawRegion, paint); } isFirstFrame = frame.no == 0; + if (Config.DEBUG_GIF_OPTIMIZATION_MODE) { + c.drawText("" + file.getRequestedSize(), (int) drawRegion.left, (int) drawRegion.top + Screen.dp(16), Paints.robotoStyleProvider(12f).getFakeBoldPaint()); + } } } if (isFirstFrame) { diff --git a/app/src/main/java/org/thunderdog/challegram/loader/gif/LottieCache.java b/app/src/main/java/org/thunderdog/challegram/loader/gif/LottieCache.java index 0ba803c5b8..00a907e984 100644 --- a/app/src/main/java/org/thunderdog/challegram/loader/gif/LottieCache.java +++ b/app/src/main/java/org/thunderdog/challegram/loader/gif/LottieCache.java @@ -48,7 +48,7 @@ public static LottieCache instance () { } private final BaseThread gcThread = new BaseThread("LottieCacheGcThread"); - private final BaseThread[] threadPool = new BaseThread[3]; + private final BaseThread[] threadPool = new BaseThread[4]; private LottieCache () { } @@ -258,7 +258,7 @@ public static File getCacheFile (GifFile file, boolean optimize, int size, int f keepAliveMs = 0; } String colorKey = fitzpatrickType != 0 ? Integer.toString(fitzpatrickType) : null; - int accountId = file.tdlib() != null ? file.tdlib().id(): TdlibAccount.NO_ID; + int accountId = file.tdlib() != null ? file.tdlib().id() : TdlibAccount.NO_ID; File cacheDir = getCacheDir(accountId, size, optimize, colorKey); if (cacheDir == null) return null; @@ -285,7 +285,7 @@ public void checkFile (GifFile file, File cacheFile, boolean optimize, int size, cacheFile.delete(); } else { String colorKey = fitzpatrickType != 0 ? Integer.toString(fitzpatrickType) : null; - String key = getCacheFileKey(file.tdlib != null ? file.tdlib.accountId(): TdlibAccount.NO_ID, optimize, size, colorKey, new File(file.getFilePath()).getName()); + String key = getCacheFileKey(file.tdlib != null ? file.tdlib.accountId() : TdlibAccount.NO_ID, optimize, size, colorKey, new File(file.getFilePath()).getName()); long time = Settings.instance().getLong(key, 0); if (time == 0 || System.currentTimeMillis() >= time) { cacheFile.delete(); diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/AvatarPickerMode.java b/app/src/main/java/org/thunderdog/challegram/mediaview/AvatarPickerMode.java new file mode 100644 index 0000000000..f60e0913dc --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/AvatarPickerMode.java @@ -0,0 +1,26 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 19/11/2023 at 00:51 + */ +package org.thunderdog.challegram.mediaview; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +@IntDef({AvatarPickerMode.NONE, AvatarPickerMode.PROFILE, AvatarPickerMode.GROUP, AvatarPickerMode.CHANNEL}) +public @interface AvatarPickerMode { + int NONE = 0, PROFILE = 1, GROUP = 2, CHANNEL = 3; +} diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/EditButton.java b/app/src/main/java/org/thunderdog/challegram/mediaview/EditButton.java index 57d3d9ac91..9aae55a95e 100644 --- a/app/src/main/java/org/thunderdog/challegram/mediaview/EditButton.java +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/EditButton.java @@ -18,19 +18,25 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Path; import android.graphics.drawable.Drawable; import android.view.MotionEvent; import android.view.View; +import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import org.thunderdog.challegram.R; +import org.thunderdog.challegram.loader.AvatarReceiver; +import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.widget.SendButton; import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.BoolAnimator; import me.vkryl.android.animator.FactorAnimator; import me.vkryl.core.ColorUtils; import me.vkryl.core.MathUtils; @@ -49,6 +55,7 @@ public class EditButton extends View implements FactorAnimator.Target { public EditButton (Context context) { super(context); + avatarReceiver = new AvatarReceiver(this); setBackgroundResource(R.drawable.bg_btn_header_light); } @@ -80,6 +87,7 @@ public void setUseFastAnimations () { private static final int ACTIVE_ANIMATOR = 0; private static final int CHANGE_ANIMATOR = 1; private static final int EDITED_ANIMATOR = 2; + private static final int SLOW_MODE_VISIBILITY_ANIMATOR = 3; private float editedFactor; private FactorAnimator editedAnimator; @@ -119,6 +127,16 @@ private void setEditedFactor (float factor) { } } + private BoolAnimator slowModeVisibilityAnimator; + + public void setSlowModeVisibility (boolean visibility, boolean animated) { + if (slowModeVisibilityAnimator == null) { + slowModeVisibilityAnimator = new BoolAnimator(SLOW_MODE_VISIBILITY_ANIMATOR, this, AnimatorUtils.DECELERATE_INTERPOLATOR, useFastAnimations ? 180L : 380L, visibility); + } + + slowModeVisibilityAnimator.setValue(visibility, animated); + } + private FactorAnimator iconAnimator; private int pendingIcon; @@ -163,7 +181,7 @@ private void setIconInternal (int iconRes) { private void drawSoundOn () { Canvas c = specialIconCanvas; specialIcon.eraseColor(0); - Drawables.draw(c, icon, 0, 0, Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, icon, 0, 0, Paints.whitePorterDuffPaint()); if (activeFactor != 0f) { int width = Screen.dp(2f); int height = Screen.dp(24f); @@ -265,6 +283,10 @@ public void onFactorChanged (int id, float factor, float fraction, FactorAnimato setEditedFactor(factor); break; } + case SLOW_MODE_VISIBILITY_ANIMATOR: { + invalidate(); + break; + } } } @@ -275,10 +297,18 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca private static final float MIN_SCALE = USE_SCALE ? .78f : 1f; private static final float STEP_FACTOR = USE_SCALE ? .45f : .5f; + + private final Path clipPath = new Path(); + + @ColorInt + private int getCurrentIconColor () { + float activeFactor = iconRes != R.drawable.baseline_volume_up_24 ? this.activeFactor : 0f; + return changer.getColor(activeFactor); + } + @Override protected void onDraw (Canvas c) { - float activeFactor = iconRes != R.drawable.baseline_volume_up_24 ? this.activeFactor : 0f; - Paint paint = Paints.getPorterDuffPaint(changer.getColor(activeFactor)); + Paint paint = Paints.getPorterDuffPaint(getCurrentIconColor()); int centerX = getMeasuredWidth() / 2; int centerY = getMeasuredHeight() / 2; @@ -300,6 +330,16 @@ protected void onDraw (Canvas c) { return; } + final float slowModeCounterFactor = slowModeVisibilityAnimator != null ? slowModeVisibilityAnimator.getFloatValue() : 1f; + final boolean needDrawSlowModeCounter = slowModeCounterController != null && slowModeCounterController.isVisible() && slowModeCounterFactor > 0f; + int clipSaveTo = -1; + if (needDrawSlowModeCounter) { + slowModeCounterController.draw(c, avatarReceiver, centerX, centerY, slowModeCounterFactor); + slowModeCounterController.buildClipPath(this, clipPath); + clipSaveTo = Views.save(c); + c.clipPath(clipPath); + } + if (alpha != 1f) { paint.setAlpha((int) (255f * alpha)); if (USE_SCALE) { @@ -345,5 +385,46 @@ protected void onDraw (Canvas c) { c.drawCircle(centerX, getMeasuredHeight() - Screen.dp(9.5f), Screen.dp(2f), Paints.fillingPaint(color)); } + + if (needDrawSlowModeCounter) { + Views.restore(c, clipSaveTo); + } + } + + private SendButton.SlowModeCounterController slowModeCounterController; + private final AvatarReceiver avatarReceiver; + + public void destroySlowModeCounterController () { + if (slowModeCounterController != null) { + slowModeCounterController.performDestroy(); + slowModeCounterController = null; + } + avatarReceiver.destroy(); + } + + @Override + protected void onAttachedToWindow () { + avatarReceiver.attach(); + super.onAttachedToWindow(); + } + + @Override + protected void onDetachedFromWindow () { + avatarReceiver.detach(); + super.onDetachedFromWindow(); + } + + public SendButton.SlowModeCounterController getSlowModeCounterController (Tdlib tdlib) { + if (slowModeCounterController != null && slowModeCounterController.tdlib() != tdlib) { + destroySlowModeCounterController(); + } + + if (slowModeCounterController == null) { + slowModeCounterController = new SendButton.SlowModeCounterController(tdlib, this, this::getCurrentIconColor, true, false, (a, b, c) -> { + avatarReceiver.requestMessageSender(a, b, c); + invalidate(); + }); + } + return slowModeCounterController; } } diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/MediaViewController.java b/app/src/main/java/org/thunderdog/challegram/mediaview/MediaViewController.java index 8f4c603b2a..95774c7b98 100644 --- a/app/src/main/java/org/thunderdog/challegram/mediaview/MediaViewController.java +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/MediaViewController.java @@ -66,6 +66,7 @@ import org.thunderdog.challegram.component.chat.InlineResultsWrap; import org.thunderdog.challegram.component.chat.InputView; import org.thunderdog.challegram.component.preview.FlingDetector; +import org.thunderdog.challegram.component.sticker.TGStickerObj; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.InlineResult; @@ -183,7 +184,7 @@ public class MediaViewController extends ViewController openCrop(true)); + } + } + private void onAppear () { // Nothing to do? } @@ -1173,10 +1202,8 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca setFullScreen(true); } mediaView.autoplayIfNeeded(false); - if (mode == MODE_SECRET) { - final MediaItem secretItem = stack.getCurrent(); - UI.post(() -> secretItem.viewSecretContent(), 20); - } + final MediaItem openedItem = stack.getCurrent(); + UI.post(() -> openedItem.viewContent(false), 20); if (canSendAsFile() != SEND_MODE_NONE && Settings.instance().needTutorial(Settings.TUTORIAL_SEND_AS_FILE)) { Settings.instance().markTutorialAsShown(Settings.TUTORIAL_SEND_AS_FILE); context().tooltipManager().builder(sendButton).color(context().tooltipManager().overrideColorProvider(getForcedTheme())).locate((targetView, outRect) -> { @@ -1196,7 +1223,9 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca break; } case ANIMATOR_SECTION: { - applySection(); + if (finalFactor == 1f) { + applySection(); + } break; } case ANIMATOR_CAPTION: { @@ -1233,7 +1262,14 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca break; } case ANIMATOR_IMAGE_ROTATE: { - applyImageRotation(); + if (finalFactor == 1f) { + applyImageRotation(); + } + break; + } + case ANIMATOR_IMAGE_FLIP_HORIZONTALLY: + case ANIMATOR_IMAGE_FLIP_VERTICALLY: { + applyImageMirror(); break; } case ANIMATOR_THUMBS: { @@ -1444,6 +1480,10 @@ protected int getHeaderIconColorId () { @Override protected int getMenuId () { + MediaItem current = stack.getCurrent(); + if (current != null && current.isViewOnce()) { + return 0; + } return R.id.menu_photo; } @@ -1567,7 +1607,7 @@ private ThemeDelegate getForcedTheme () { // TODO actually move this to ViewCont @Override public boolean shouldDisallowScreenshots () { - return mode == MODE_SECRET || !stack.getCurrent().canBeSaved(); + return mode == MODE_SECRET || !stack.getCurrent().canBeSaved() || super.shouldDisallowScreenshots(); } @Override @@ -1649,11 +1689,11 @@ public void onMoreItemPressed (int id) { ShareController c; if (item.getMessage() != null) { c = new ShareController(context, tdlib); - if (item.getMessage().content.getConstructor() != TdApi.MessageText.CONSTRUCTOR) { - c.setArguments(new ShareController.Args(item.getMessage())); - } else { + if (Td.isText(item.getMessage().content)) { TdApi.WebPage webPage = ((TdApi.MessageText) item.getMessage().content).webPage; c.setArguments(new ShareController.Args(item, webPage.displayUrl, webPage.displayUrl)); + } else { + c.setArguments(new ShareController.Args(item.getMessage())); } } else if (item.getShareFile() != null) { c = new ShareController(context, tdlib); @@ -1924,7 +1964,7 @@ private CharSequence genSubtitle () { switch (mode) { case MODE_MESSAGES: { TdApi.Message message = item.getMessage(); - if (message != null && message.content.getConstructor() == TdApi.MessageText.CONSTRUCTOR) { + if (message != null && Td.isText(message.content)) { TdApi.MessageText messageText = (TdApi.MessageText) message.content; if (messageText.webPage != null) { if (!StringUtils.isEmpty(messageText.webPage.author)) { @@ -2197,7 +2237,7 @@ private int addItemsImpl (List messages, final int totalCount) { skipCount--; continue; } - if (TD.isSecret(msg) || (!ChatId.isSecret(msg.chatId) && msg.selfDestructTime != 0)) // skip self-destructing images + if (Td.isSecret(msg.content)) // skip self-destructing images continue; MediaItem item = MediaItem.valueOf(context(), tdlib, msg); @@ -2259,9 +2299,13 @@ public void onMessageContentChanged (long chatId, long messageId, TdApi.MessageC MediaItem newItem = MediaItem.valueOf(context(), tdlib, message); if (newItem != null) { replaceMedia(index, oldItem, newItem); - headerCell.setSubtitle(genSubtitle()); + if (headerCell != null) { + headerCell.setSubtitle(genSubtitle()); + } } else if (stack.getCurrentIndex() == index) { - forceClose(); + // if (message.selfDestructType == null || message.selfDestructType.getConstructor() != TdApi.MessageSelfDestructTypeImmediately.CONSTRUCTOR) { + forceClose(); + // } } else { deleteMedia(index, oldItem); } @@ -2907,10 +2951,10 @@ protected void onDraw (Canvas c) { if (isLeft) { c.save(); c.rotate(180, getMeasuredWidth() / 2, getMeasuredHeight() / 2); - Drawables.draw(c, backIcon, 0, y, Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, backIcon, 0, y, Paints.whitePorterDuffPaint()); c.restore(); } else { - Drawables.draw(c, backIcon, 0, y, Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, backIcon, 0, y, Paints.whitePorterDuffPaint()); } } } @@ -2928,9 +2972,9 @@ protected void onDraw (Canvas c) { private LinearLayout editButtons; private EditButton cropOrStickerButton; private EditButton paintOrMuteButton; + private EditButton mirrorButton; private EditButton adjustOrTextButton; private StopwatchHeaderButton stopwatchButton; - private @Nullable MediaLayout.SenderSendIcon senderSendIcon; private FrameLayoutFix bottomWrap; private LinearLayout captionWrapView; @@ -4117,7 +4161,7 @@ public void onClickAt (View view, float x, float y) { ThumbView thumbView = ThumbViewHolder.getThumbView(child); MediaItem item = thumbView.getItem(); ThumbItems items = thumbView.getItems(); - if (items != null && thumbView.preview.isInsideReceiver(x, y)) { + if (items != null && thumbView.getPreviewReceiver().isInsideReceiver(x, y)) { if (controller.fastShowMediaItem(item, items, items.indexOf(item), true)) { ViewUtils.onClick(this); return; @@ -4134,6 +4178,10 @@ private static class ThumbView extends View implements AttachDelegate, MediaItem private ThumbItems items; private MediaItem item; + private Receiver getPreviewReceiver () { + return item != null && item.isAvatar() ? avatarReceiver : this.preview; + } + public ThumbView (Context context, RecyclerView drawTarget) { super(context); preview = new DoubleImageReceiver(drawTarget, 0); @@ -4268,7 +4316,7 @@ public void drawImage (Canvas c, int centerX, int startY, float alpha, float exp float expandFactor = items != null ? items.getExpandFactor(item) * expandAllowance : 0f; int thumbWidth = thumbStartWidth + (int) ((float) (thumbEndWidth - thumbStartWidth) * expandFactor); - Receiver preview = item != null && item.isAvatar() ? avatarReceiver : this.preview; + Receiver preview = getPreviewReceiver(); if (alpha != 1f) { preview.setPaintAlpha(alpha); } @@ -4631,6 +4679,7 @@ protected View onCreateView (Context context) { popupView = new PopupLayout(context); popupView.setOverlayStatusBar(true); + popupView.setShowListener(this); if (mode == MODE_SECRET) { popupView.setIgnoreHorizontal(); } @@ -4645,6 +4694,10 @@ protected View onCreateView (Context context) { @Override public void onActivityPause () { mediaView.onMediaActivityPause(); + MediaItem item = stack.getCurrent(); + if (item != null && item.isViewOnce()) { + item.viewContent(true); + } } @Override @@ -4896,6 +4949,8 @@ public void getOutline (View view, android.graphics.Outline outline) { switch (mode) { case MODE_GALLERY: { + final boolean inProfilePhotoEditMode = inProfilePhotoEditMode(); + TdApi.Chat chat = getArgumentsStrict().receiverChatId != 0 ? tdlib.chat(getArgumentsStrict().receiverChatId) : null; mediaView.setOffsets(0, 0, 0, 0, 0); // Screen.dp(56f) @@ -4912,19 +4967,14 @@ public void getOutline (View view, android.graphics.Outline outline) { sendButton = new EditButton(context); sendButton.setId(R.id.btn_send); - sendButton.setIcon(R.drawable.deproko_baseline_send_24, false, false); + setDefaultSendButtonIcon(false); sendButton.setOnClickListener(this); sendButton.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(56f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.RIGHT)); sendButton.setBackgroundResource(R.drawable.bg_btn_header_light); - editWrap.addView(sendButton); - - if (chat != null && chat.messageSenderId != null) { - senderSendIcon = new MediaLayout.SenderSendIcon(context, tdlib(), chat.id); - senderSendIcon.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(19), Screen.dp(19), Gravity.RIGHT | Gravity.BOTTOM, 0, 0, Screen.dp(11), Screen.dp(8))); - senderSendIcon.setBackgroundColorId(getHeaderColorId()); - senderSendIcon.update(chat.messageSenderId); - editWrap.addView(senderSendIcon); + if (selectDelegate != null) { + sendButton.getSlowModeCounterController(tdlib).setCurrentChat(selectDelegate.getOutputChatId()); } + editWrap.addView(sendButton); if (chat != null) { tdlib.ui().createSimpleHapticMenu(this, chat.id, () -> currentActiveButton == 0, this::canDisableMarkdown, () -> true, hapticItems -> { @@ -4978,8 +5028,8 @@ public boolean onHapticMenuItemClick (View view, View parentView, HapticMenuHelp return true; }).bindTutorialFlag(Settings.TUTORIAL_SEND_AS_FILE)); } - if (senderSendIcon != null) { - hapticItems.add(0, senderSendIcon.createHapticSenderItem(chat).setOnClickListener((view, parentView, item) -> { + if (chat != null && chat.messageSenderId != null) { + hapticItems.add(0, MediaLayout.createHapticSenderItem(tdlib, chat).setOnClickListener((view, parentView, item) -> { openSetSenderPopup(chat); return true; })); @@ -5195,10 +5245,11 @@ public boolean onTouchEvent (MotionEvent event) { captionWrapView.addView(captionView); captionWrapView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM)); - bottomWrap.addView(captionWrapView); - - bottomWrap.addView(captionDoneButton); - bottomWrap.addView(captionEmojiButton); + if (!inProfilePhotoEditMode) { + bottomWrap.addView(captionWrapView); + bottomWrap.addView(captionDoneButton); + bottomWrap.addView(captionEmojiButton); + } videoSliderView = new VideoControlView(context); videoSliderView.setSliderListener(this); @@ -5306,7 +5357,6 @@ public boolean onTouchEvent (MotionEvent event) { checkView.setLayoutParams(fp); checkView.setOnClickListener(this); checkView.forceSetChecked(isCurrentItemSelected()); - contentView.addView(checkView); fp = FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, Screen.dp(30f), Gravity.RIGHT); fp.rightMargin = Screen.dp(78f); @@ -5320,9 +5370,13 @@ public boolean onTouchEvent (MotionEvent event) { int count = getSelectedMediaCount(); counterView.initCounter(Math.max(count, 1), false); forceCounterFactor(count == 0 ? 0f : 1f); - contentView.addView(counterView); - if (chat != null) { + if (!inProfilePhotoEditMode) { + contentView.addView(checkView); + contentView.addView(counterView); + } + + if (chat != null || inProfilePhotoEditMode) { fp = FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, Screen.getStatusBarHeight()); if (HeaderView.getTopOffset() > 0) { fp.leftMargin = Screen.dp(8f); @@ -5343,7 +5397,11 @@ public boolean onTouchEvent (MotionEvent event) { ImageView imageView = new ImageView(context); imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - imageView.setImageResource(R.drawable.baseline_arrow_upward_18); + imageView.setImageResource(getResId( + R.drawable.baseline_arrow_upward_18, + R.drawable.dot_baseline_account_circle_18, + R.drawable.dot_baseline_group_circle_18, + R.drawable.dot_baseline_channel_circle_18)); imageView.setColorFilter(0xffffffff); imageView.setAlpha((float) 0xaa / (float) 0xff); imageView.setLayoutParams(lp); @@ -5352,13 +5410,14 @@ public boolean onTouchEvent (MotionEvent event) { lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.leftMargin = Screen.dp(6f); + final int textRes = getResId(0, R.string.ProfilePhoto, R.string.GroupPhoto, R.string.ChannelPhoto); TextView textView = new NoScrollTextView(context); textView.setTextColor(0xaaffffff); textView.setSingleLine(true); textView.setEllipsize(TextUtils.TruncateAt.END); textView.setTypeface(Fonts.getRobotoMedium()); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13f); - textView.setText(tdlib.chatTitle(chat)); + textView.setText(textRes != 0 ? Lang.getString(textRes) : (chat != null ? tdlib.chatTitle(chat) : null)); textView.setLayoutParams(lp); receiverView.addView(textView); @@ -5532,6 +5591,22 @@ public boolean onTouchEvent (MotionEvent event) { return contentView; } + private int getResId (int defaultResId, int profileResId, int groupResId, int channelResId) { + final int mode = getArgumentsStrict().avatarPickerMode; + if (mode == AvatarPickerMode.PROFILE) { + return profileResId; + } else if (mode == AvatarPickerMode.CHANNEL) { + return channelResId; + } else if (mode == AvatarPickerMode.GROUP) { + return groupResId; + } + return defaultResId; + } + + private boolean inProfilePhotoEditMode () { + return getArgumentsStrict().avatarPickerMode != AvatarPickerMode.NONE; + } + private int controlsMargin; private void setControlsMargin (int margin) { @@ -5584,6 +5659,10 @@ public void dispatchInnerMargins (int left, int top, int right, int bottom) { @Override public void destroy () { super.destroy(); + MediaItem current = stack.getCurrent(); + if (current != null && current.isViewOnce()) { + current.viewContent(true); + } if (!isMediaSent && getArguments() != null && getArgumentsStrict().deleteOnExit && stack != null) { for (int i = 0; i < stack.getCurrentSize(); i++) { MediaItem item = stack.get(i); @@ -5608,6 +5687,9 @@ public void destroy () { if (captionView instanceof Destroyable) { ((Destroyable) captionView).performDestroy(); } + if (sendButton != null) { + sendButton.destroySlowModeCounterController(); + } subscribeToChatId(0); } @@ -6182,11 +6264,11 @@ public boolean allowSliderChanges (SliderView view) { TextView textView = Views.newTextView(context(), 14f, Theme.getColor(ColorId.white), Gravity.LEFT, Views.TEXT_FLAG_SINGLE_LINE); textView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, Screen.dp(15f), Screen.dp(10f), Screen.dp(15f), 0)); - textView.setText(R.string.QualityWorse); + textView.setText(Lang.getString(R.string.QualityWorse)); qualityControlWrap.addView(textView); textView = Views.newTextView(context(), 14f, Theme.getColor(ColorId.white), Gravity.RIGHT, Views.TEXT_FLAG_SINGLE_LINE); textView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.TOP, Screen.dp(15f), Screen.dp(10f), Screen.dp(15f), 0)); - textView.setText(R.string.QualityBetter); + textView.setText(Lang.getString(R.string.QualityBetter)); qualityControlWrap.addView(textView); qualityInfo = Views.newTextView(context(), 15f, Theme.getColor(ColorId.white), Gravity.CENTER, Views.TEXT_FLAG_SINGLE_LINE); @@ -6199,6 +6281,7 @@ public boolean allowSliderChanges (SliderView view) { } case SECTION_CROP: { if (cropControlsWrap == null) { + final boolean inProfilePhotoEditMode = inProfilePhotoEditMode(); FrameLayoutFix.LayoutParams params; params = FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, getSectionHeight(SECTION_CROP), Gravity.BOTTOM); @@ -6221,10 +6304,19 @@ public boolean allowSliderChanges (SliderView view) { proportionButton.setOnClickListener(this); proportionButton.setIcon(R.drawable.baseline_image_aspect_ratio_24, false, false); proportionButton.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(56f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.LEFT)); - cropControlsWrap.addView(proportionButton); + if (!inProfilePhotoEditMode) { + cropControlsWrap.addView(proportionButton); + } + + mirrorButton = new EditButton(context()); + mirrorButton.setId(R.id.btn_mirrorHorizontal); + mirrorButton.setOnClickListener(this); + mirrorButton.setIcon(R.drawable.dot_baseline_flip_horizontal_24, false, false); + mirrorButton.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(56f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.LEFT, inProfilePhotoEditMode ? 0 : Screen.dp(56), 0, 0, 0)); + cropControlsWrap.addView(mirrorButton); params = FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - params.leftMargin = Screen.dp(56f); + params.leftMargin = Screen.dp(inProfilePhotoEditMode ? 56f : (56f * 2)); params.rightMargin = Screen.dp(56f); rotationControlView = new RotationControlView(context()); @@ -6440,6 +6532,8 @@ private void setInCrop (boolean inCrop) { if (!inCrop) { prepareSectionToHide(SECTION_CROP); mediaView.setVisibility(View.VISIBLE); + } else if (inProfilePhotoEditMode()) { + setCropProportion(1, 1, false); } cropAnimator.setDuration(inCrop ? (currentCropState.isEmpty() ? CROP_OUT_DURATION : CROP_IN_DURATION) : 120l); cropAnimator.setValue(inCrop, true); @@ -6570,6 +6664,42 @@ public void onPreciseRotationChanged (float newValue) { cropTargetView.setDegreesAroundCenter(newValue); } + /**/ + + private static final int ANIMATOR_IMAGE_FLIP_HORIZONTALLY = 192; + private static final int ANIMATOR_IMAGE_FLIP_VERTICALLY = 193; + private final BoolAnimator imageFlipAnimatorHorizontally = new BoolAnimator(ANIMATOR_IMAGE_FLIP_HORIZONTALLY, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 250L); + private final BoolAnimator imageFlipAnimatorVertically = new BoolAnimator(ANIMATOR_IMAGE_FLIP_VERTICALLY, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 250L); + + private boolean imageMirrorAnimate (int mirrorFlag, boolean needMirror, boolean animated) { + final BoolAnimator animator = mirrorFlag == CropState.FLAG_MIRROR_HORIZONTALLY ? + imageFlipAnimatorHorizontally : imageFlipAnimatorVertically; + + if (!animator.isAnimating()) { + animator.setValue(!needMirror, false); + } else { + return false; + } + currentCropState.setFlags(BitwiseUtils.setFlag(currentCropState.getFlags(), mirrorFlag, needMirror)); + animator.setValue(needMirror, animated); + return true; + } + + private void setImageMirrorFactors () { + cropTargetView.setMirrorFactors(imageFlipAnimatorHorizontally.getFloatValue(), imageFlipAnimatorVertically.getFloatValue()); + } + + private void applyImageMirror () { + cropTargetView.setMirrorFactors(currentCropState.hasFlag(CropState.FLAG_MIRROR_HORIZONTALLY) ? 1 : 0, currentCropState.hasFlag(CropState.FLAG_MIRROR_VERTICALLY) ? 1 : 0); + } + + private void cancelImageMirrorAnimations () { + imageFlipAnimatorHorizontally.cancel(); + imageFlipAnimatorVertically.cancel(); + } + + /**/ + private CropState currentCropState; private CropState oldCropState; @@ -6600,6 +6730,7 @@ private void prepareCropLayout () { cropLayout.addView(cropTargetView); cropAreaView = new CropAreaView(context()); + cropAreaView.setProfilePhotoMode(inProfilePhotoEditMode()); cropAreaView.setRectChangeListener((left, top, right, bottom) -> { if (inCrop) { currentCropState.setRect(left, top, right, bottom); @@ -6627,6 +6758,8 @@ private void prepareCropState () { proportionButton.setActive(false, false); int cropRotation = MathUtils.modulo(this.cropRotation + (oldCropState != null ? oldCropState.getRotateBy() : 0), 360); cropTargetView.resetState(cropBitmap, cropRotation, currentCropState.getDegreesAroundCenter(), currentPaintState); + cropTargetView.setMirrorFactors(currentCropState.hasFlag(CropState.FLAG_MIRROR_HORIZONTALLY) ? 1f : 0f, currentCropState.hasFlag(CropState.FLAG_MIRROR_VERTICALLY) ? 1f : 0f); + mirrorButton.setActive(currentCropState.needMirror(), false); rotationControlView.reset(currentCropState.getDegreesAroundCenter(), false); cropAreaView.resetProportion(); cropAreaView.resetState(U.getWidth(cropBitmap, cropRotation), U.getHeight(cropBitmap, cropRotation), currentCropState.getLeft(), currentCropState.getTop(), currentCropState.getRight(), currentCropState.getBottom(), false); @@ -6662,9 +6795,20 @@ private void resetCropState () { oldCropState = null; } - private void setCropProportion (int big, int small) { - cropAreaView.setFixedProportion(big, small); - proportionButton.setActive(big != 0 && small != 0, true); + private void setCropProportion (int big, int small, boolean animated) { + cropAreaView.setFixedProportion(big, small, animated); + proportionButton.setActive(big != 0 && small != 0, animated); + } + + public int getMirrorHorizontallyFlag () { + return stack != null && stack.getCurrent() != null && stack.getCurrent().isRotated() ? + CropState.FLAG_MIRROR_VERTICALLY : CropState.FLAG_MIRROR_HORIZONTALLY; + } + + private void setMirrorHorizontally (boolean newValue) { + if (imageMirrorAnimate(getMirrorHorizontallyFlag(), newValue, true)) { + mirrorButton.setActive(newValue, true); + } } private float cropStartDegrees, cropEndDegrees; @@ -6679,6 +6823,7 @@ private void resetCrop (boolean zero) { } cancelImageRotation(); + cancelImageMirrorAnimations(); cropStartDegrees = currentCropState.getDegreesAroundCenter(); cropEndDegrees = zero || oldCropState == null ? 0 : oldCropState.getDegreesAroundCenter(); @@ -6692,6 +6837,7 @@ private void resetCrop (boolean zero) { proportionButton.setActive(false, true); resettingCrop = resetCropDegrees || rotatingByDegrees != 0; closeCropAfterReset = !zero; + setMirrorHorizontally(!zero && oldCropState != null && oldCropState.hasFlag(getMirrorHorizontallyFlag())); if (zero || oldCropState == null || oldCropState.isEmpty()) { if (cropAreaView.resetArea(resettingCrop, !zero)) { resettingCrop = true; @@ -7300,6 +7446,10 @@ private boolean allowDataChanges () { } private void changeSection (int section, int mode) { + changeSection(section, mode, false); + } + + private void changeSection (int section, int mode, boolean useFastAnimation) { if (currentSection == section || !allowDataChanges()) { return; } @@ -7339,7 +7489,7 @@ private void changeSection (int section, int mode) { } } - changeSectionImpl(section); + changeSectionImpl(section, useFastAnimation); } private void applyFiltersAsync (final int futureSection) { @@ -7357,6 +7507,10 @@ private void applyFiltersAsync (final int futureSection) { } private void changeSectionImpl (int section) { + changeSectionImpl(section, false); + } + + private void changeSectionImpl (int section, boolean useFastAnimation) { if (scheduleSectionChange(currentSection, section)) { return; } @@ -7383,10 +7537,12 @@ private void changeSectionImpl (int section) { updateIconStates(true); + final long duration = useFastAnimation ? 220L : 380L; if (sectionChangeAnimator == null) { - sectionChangeAnimator = new FactorAnimator(ANIMATOR_SECTION, this, AnimatorUtils.LINEAR_INTERPOLATOR, 380l); + sectionChangeAnimator = new FactorAnimator(ANIMATOR_SECTION, this, AnimatorUtils.LINEAR_INTERPOLATOR, duration); } else { sectionChangeAnimator.forceFactor(0f); + sectionChangeAnimator.setDuration(duration); } sectionChangeAnimator.animateTo(1f); } @@ -7438,12 +7594,23 @@ private void fillIcons (int section) { if (activeButtonId != 0) { backButton.setIcon(R.drawable.baseline_close_24, true, false); sendButton.setIcon(R.drawable.baseline_check_24, true, false); + sendButton.setSlowModeVisibility(false, true); } else { backButton.setIcon(R.drawable.baseline_arrow_back_24, true, false); - sendButton.setIcon(R.drawable.deproko_baseline_send_24, true, false); + setDefaultSendButtonIcon(true); } } + private void setDefaultSendButtonIcon (boolean animated) { + sendButton.setIcon(getResId( + R.drawable.deproko_baseline_send_24, + R.drawable.dot_baseline_profile_accept_24, + R.drawable.dot_baseline_group_accept_24, + R.drawable.dot_baseline_channel_accept_24 + ), animated, false); + sendButton.setSlowModeVisibility(true, animated); + } + private boolean hasAppliedFilters () { FiltersState state = stack.getCurrent().getFiltersState(); return state != null && !state.isEmpty(); @@ -7465,8 +7632,23 @@ private void updateIconStates (boolean animated) { cropOrStickerButton.setEdited(allowEditState && hasAppliedCrop(), animated); paintOrMuteButton.setEdited(hasAppliedPaints(), animated); if (stopwatchButton != null) { - int ttl = stack.getCurrent().getTTL(); - String value = ttl != 0 ? TdlibUi.getDuration(ttl, TimeUnit.SECONDS, false) : null; + TdApi.MessageSelfDestructType selfDestructType = stack.getCurrent().getSelfDestructType(); + String value; + if (selfDestructType != null) { + switch (selfDestructType.getConstructor()) { + case TdApi.MessageSelfDestructTypeImmediately.CONSTRUCTOR: + value = TdlibUi.getDuration(0, TimeUnit.SECONDS, false); // FIXME + break; + case TdApi.MessageSelfDestructTypeTimer.CONSTRUCTOR: + value = TdlibUi.getDuration(((TdApi.MessageSelfDestructTypeTimer) selfDestructType).selfDestructTime, TimeUnit.SECONDS, false); + break; + default: + Td.assertMessageSelfDestructType_58882d8c(); + throw Td.unsupported(selfDestructType); + } + } else { + value = null; + } if (animated) { stopwatchButton.setValue(value, false); } else { @@ -7738,9 +7920,13 @@ private void openPaintCanvas () { } private void openCrop () { + openCrop(false); + } + + private void openCrop (boolean useFastAnimation) { if (Config.CROP_ENABLED) { if (currentSection != SECTION_CROP) { - changeSection(SECTION_CROP, MODE_OK); + changeSection(SECTION_CROP, MODE_OK, useFastAnimation); } } else { // UI.showToast(R.string.FeatureDisabled, Toast.LENGTH_SHORT); @@ -7858,6 +8044,10 @@ public void onClick (View v) { } else if (viewId == R.id.btn_send) { if (currentSection != SECTION_CAPTION) { changeSection(SECTION_CAPTION, MODE_OK); + } else if (inputView != null && !tdlib.isSelfChat(getOutputChatId()) && !tdlib.hasPremium() && inputView.hasOnlyPremiumFeatures()) { + context().tooltipManager().builder(sendButton).show(tdlib, Strings.buildMarkdown(this, Lang.getString(R.string.MessageContainsPremiumFeatures), null)).hideDelayed(); + } else if (needShowCropSectionInsteadSend()) { + changeSection(SECTION_CROP, MODE_OK); } else { send(v, Td.newSendOptions(), false, false); } @@ -7873,8 +8063,10 @@ public void onClick (View v) { } } else if (viewId == R.id.btn_rotate) { rotateBy90Degrees(); + } else if (viewId == R.id.btn_mirrorHorizontal) { + setMirrorHorizontally(!currentCropState.hasFlag(getMirrorHorizontallyFlag())); } else if (viewId == R.id.btn_proportion) { - if (allowDataChanges() && currentSection == SECTION_CROP) { + if (allowDataChanges() && currentSection == SECTION_CROP && !inProfilePhotoEditMode()) { IntList ids = new IntList(PROPORTION_MODES.length + 2); StringList strings = new StringList(PROPORTION_MODES.length + 2); IntList icons = new IntList(PROPORTION_MODES.length + 2); @@ -7953,11 +8145,11 @@ public void onClick (View v) { if (id == R.id.btn_crop_reset) { resetCrop(true); } else if (id == R.id.btn_proportion_free) { - setCropProportion(0, 0); + setCropProportion(0, 0, true); } else if (id == R.id.btn_proportion_original) { int targetWidth = cropAreaView.getTargetWidth(); int targetHeight = cropAreaView.getTargetHeight(); - setCropProportion(Math.max(targetWidth, targetHeight), Math.min(targetWidth, targetHeight)); + setCropProportion(Math.max(targetWidth, targetHeight), Math.min(targetWidth, targetHeight), true); } else { int[] mode = null; for (int[] proportionMode : PROPORTION_MODES) { @@ -7967,7 +8159,7 @@ public void onClick (View v) { } } if (mode != null) { - setCropProportion(mode[0], mode[1]); + setCropProportion(mode[0], mode[1], true); } } return true; @@ -8011,16 +8203,46 @@ public void onClick (View v) { } } + private boolean needShowCropSectionInsteadSend () { + if (!inProfilePhotoEditMode()) { + return false; + } + + if (cropAreaView == null) { + return true; + } + + final CropState cropState = obtainCropState(true); + + double targetWidth = (cropAreaView.getTargetWidth() * (cropState.getRight() - cropState.getLeft())); + double targetHeight = (cropAreaView.getTargetHeight() * (cropState.getBottom() - cropState.getTop())); + double proportion = Math.max(targetWidth, targetHeight) / Math.min(targetWidth, targetHeight); + + return Math.abs(proportion - 1d) > 0.02d; + } + // TTL private void showTTLOptions () { final MediaItem item = stack.getCurrent(); - tdlib.ui().showTTLPicker(context(), item.getTTL(), true, true, item.isVideo() ? R.string.MessageLifetimeVideo : R.string.MessageLifetimePhoto, result -> { + tdlib.ui().showTTLPicker(context(), item.getSelfDestructType(), !ChatId.isSecret(item.getSourceChatId()), true, true, item.isVideo() ? R.string.MessageLifetimeVideo : R.string.MessageLifetimePhoto, result -> { if (stack.getCurrent() == item) { - int newTTL = result.getTtlTime(); - item.setTTL(newTTL); - stopwatchButton.setValue(newTTL != 0 ? TdlibUi.getDuration(newTTL, TimeUnit.SECONDS, false) : null); - if (newTTL != 0) { + TdApi.MessageSelfDestructType selfDestructType; + String textRepresentation; + if (result.isOff()) { + selfDestructType = null; + textRepresentation = null; + } else if (result.isImmediate()) { + selfDestructType = new TdApi.MessageSelfDestructTypeImmediately(); + textRepresentation = TdlibUi.getDuration(0, TimeUnit.SECONDS, false); // FIXME + } else { + int newTTL = result.getTtlTime(); + textRepresentation = TdlibUi.getDuration(newTTL, TimeUnit.SECONDS, false); + selfDestructType = new TdApi.MessageSelfDestructTypeTimer(newTTL); + } + item.setSelfDestructType(selfDestructType); + stopwatchButton.setValue(textRepresentation); + if (selfDestructType != null) { selectMediaIfItsNot(); } } @@ -8131,6 +8353,10 @@ public void send (View view, TdApi.MessageSendOptions initialSendOptions, boolea return; } + if (initialSendOptions.schedulingState == null && showSlowModeRestriction(sendButton)) { + return; + } + if (initialSendOptions.schedulingState == null && getArgumentsStrict().areOnlyScheduled) { tdlib.ui().showScheduleOptions(this, getOutputChatId(), false, (modifiedSendOptions, disableMarkdown1) -> { send(view, modifiedSendOptions, disableMarkdown, asFiles); @@ -8182,7 +8408,7 @@ public static void openFromMedia (ViewController context, MediaItem item, @Nu Args args = new Args(context, MODE_MESSAGES, stack); args.reverseMode = stack.getReverseModeHint(true); args.forceThumbs = stack.getForceThumbsHint(true); - args.forceOpenIn = forceOpenIn || (filter != null && filter.getConstructor() == TdApi.SearchMessagesFilterDocument.CONSTRUCTOR); + args.forceOpenIn = forceOpenIn || (filter != null && Td.isDocumentFilter(filter)); args.filter = filter; if (context instanceof MediaCollectorDelegate) { ((MediaCollectorDelegate) context).modifyMediaArguments(item, args); @@ -8353,35 +8579,39 @@ public static void openFromMessage (TGMessageMedia messageContainer, long messag return; } + boolean allowLoadMore = !item.isSecret() && !item.isViewOnce(); TdApi.SearchMessagesFilter filter = null; - switch (msg.content.getConstructor()) { - case TdApi.MessagePhoto.CONSTRUCTOR: { - filter = new TdApi.SearchMessagesFilterPhotoAndVideo(); - break; - } - case TdApi.MessageChatChangePhoto.CONSTRUCTOR: { - filter = new TdApi.SearchMessagesFilterChatPhoto(); - break; - } - case TdApi.MessageVideo.CONSTRUCTOR: { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - TdApi.Video video = ((TdApi.MessageVideo) msg.content).video; - UI.openFile(messageContainer.controller(), null, new File(video.video.local.path), "video/mp4", TD.getViewCount(msg.interactionInfo)); + if (allowLoadMore) { + //noinspection SwitchIntDef + switch (msg.content.getConstructor()) { + case TdApi.MessagePhoto.CONSTRUCTOR: { + filter = new TdApi.SearchMessagesFilterPhotoAndVideo(); + break; + } + case TdApi.MessageChatChangePhoto.CONSTRUCTOR: { + filter = new TdApi.SearchMessagesFilterChatPhoto(); + break; + } + case TdApi.MessageVideo.CONSTRUCTOR: { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + TdApi.Video video = ((TdApi.MessageVideo) msg.content).video; + UI.openFile(messageContainer.controller(), null, new File(video.video.local.path), "video/mp4", TD.getViewCount(msg.interactionInfo)); + } + filter = new TdApi.SearchMessagesFilterPhotoAndVideo(); + break; + } + case TdApi.MessageAnimation.CONSTRUCTOR: { + filter = new TdApi.SearchMessagesFilterAnimation(); + break; + } + case TdApi.MessageText.CONSTRUCTOR: { + filter = new TdApi.SearchMessagesFilterUrl(); + break; + } + case TdApi.MessageDocument.CONSTRUCTOR: { + filter = new TdApi.SearchMessagesFilterDocument(); + break; } - filter = new TdApi.SearchMessagesFilterPhotoAndVideo(); - break; - } - case TdApi.MessageAnimation.CONSTRUCTOR: { - filter = new TdApi.SearchMessagesFilterAnimation(); - break; - } - case TdApi.MessageText.CONSTRUCTOR: { - filter = new TdApi.SearchMessagesFilterUrl(); - break; - } - case TdApi.MessageDocument.CONSTRUCTOR: { - filter = new TdApi.SearchMessagesFilterDocument(); - break; } } @@ -8390,7 +8620,7 @@ public static void openFromMessage (TGMessageMedia messageContainer, long messag if (context.isStackLocked()) { return; } - if (context instanceof MediaCollectorDelegate) { + if (allowLoadMore && context instanceof MediaCollectorDelegate) { stack = ((MediaCollectorDelegate) context).collectMedias(msg.id, filter); } @@ -8400,7 +8630,7 @@ public static void openFromMessage (TGMessageMedia messageContainer, long messag } Args args = new Args(context, MODE_MESSAGES, stack); - args.noLoadMore = messageContainer.isEventLog(); + args.noLoadMore = !allowLoadMore || messageContainer.isEventLog(); if (context instanceof MediaCollectorDelegate) { ((MediaCollectorDelegate) context).modifyMediaArguments(msg, args); } @@ -8429,11 +8659,11 @@ protected void handleLanguageDirectionChange () { private void openSetSenderPopup (TdApi.Chat chat) { if (chat == null) return; - tdlib().send(new TdApi.GetChatAvailableMessageSenders(chat.id), result -> { + tdlib().send(new TdApi.GetChatAvailableMessageSenders(chat.id), (result, error) -> { UI.post(() -> { - if (result.getConstructor() == TdApi.ChatMessageSenders.CONSTRUCTOR) { + if (result != null) { final SetSenderController c = new SetSenderController(context, tdlib()); - c.setArguments(new SetSenderController.Args(chat, ((TdApi.ChatMessageSenders) result).senders, chat.messageSenderId)); + c.setArguments(new SetSenderController.Args(chat, result.senders, chat.messageSenderId)); c.setShowOverEverything(true); c.setDelegate((s) -> setNewMessageSender(chat, s)); c.show(); @@ -8443,13 +8673,7 @@ private void openSetSenderPopup (TdApi.Chat chat) { } private void setNewMessageSender (TdApi.Chat chat, TdApi.ChatMessageSender sender) { - tdlib().send(new TdApi.SetChatMessageSender(chat.id, sender.sender), o -> { - UI.post(() -> { - if (senderSendIcon != null) { - senderSendIcon.update(chat.messageSenderId); - } - }); - }); + tdlib().send(new TdApi.SetChatMessageSender(chat.id, sender.sender), tdlib.typedOkHandler()); } private void setEmojiShown (boolean emojiShown) { @@ -8485,8 +8709,8 @@ public void onInputSelectionExistChanged (InputView v, boolean hasSelection) { private void setTextFormattingLayoutVisible (boolean visible) { textFormattingVisible = visible; if (emojiLayout != null && textFormattingLayout != null) { - textFormattingLayout.setVisibility(visible ? View.VISIBLE: View.GONE); - emojiLayout.optimizeForDisplayTextFormattingLayout(!visible); + textFormattingLayout.setVisibility(visible ? View.VISIBLE : View.GONE); + emojiLayout.optimizeForDisplayTextFormattingLayout(visible); if (visible) { textFormattingLayout.checkButtonsActive(false); } @@ -8500,6 +8724,20 @@ private void closeTextFormattingKeyboard () { } public @DrawableRes int getTargetIcon () { - return (textInputHasSelection || (textFormattingVisible && emojiShown)) ? R.drawable.baseline_format_text_24: R.drawable.deproko_baseline_insert_emoticon_26; + return (textInputHasSelection || (textFormattingVisible && emojiShown)) ? R.drawable.baseline_format_text_24 : R.drawable.deproko_baseline_insert_emoticon_26; + } + + public boolean showSlowModeRestriction (View v) { + if (selectDelegate == null) { + return false; + } + + CharSequence restriction = tdlib().getSlowModeRestrictionText(selectDelegate.getOutputChatId()); + if (restriction != null) { + context().tooltipManager().builder(v).show(tdlib, restriction).hideDelayed(); + return true; + } + + return false; } } diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropAreaView.java b/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropAreaView.java index 9cc7241698..4d0a52d4b5 100644 --- a/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropAreaView.java +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropAreaView.java @@ -18,7 +18,9 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.Path; import android.graphics.Rect; +import android.os.Build; import android.view.MotionEvent; import android.view.View; @@ -30,6 +32,7 @@ import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.tool.Views; import me.vkryl.android.AnimatorUtils; import me.vkryl.android.animator.BoolAnimator; @@ -78,6 +81,12 @@ public void setRotateModeChangeListener (@Nullable RotateModeChangeListener rota this.rotateModeChangeListener = rotateModeChangeListener; } + private boolean profilePhotoMode; // + + public void setProfilePhotoMode (boolean profilePhotoMode) { + this.profilePhotoMode = profilePhotoMode; + } + private static final int OUTER_LINE_COLOR = 0x99ffffff; private static final int CORNER_COLOR = 0xffffffff; private static final int INNER_LINE_COLOR = 0xeeffffff; @@ -241,6 +250,7 @@ private void checkPivotCoordinates () { private final Rect srcRect = new Rect(); private final Rect dstRect = new Rect(); + private final Path roundClipPath = new Path(); @Override protected void onDraw (Canvas c) { @@ -290,6 +300,9 @@ protected void onDraw (Canvas c) { int top = dstRect.top; int right = dstRect.right; int bottom = dstRect.bottom; + int centerX = dstRect.centerX(); + int centerY = dstRect.centerY(); + int radius = Math.min(dstRect.width(), dstRect.height()) / 2; int cornerWidth = Screen.dp(2f); int cornerHeight = Screen.dp(16f); @@ -307,6 +320,20 @@ protected void onDraw (Canvas c) { // bottom line c.drawRect(left + cornerHeight - cornerWidth, bottom, right - cornerHeight + cornerWidth, bottom + normalWidth, Paints.fillingPaint(OUTER_LINE_COLOR)); + if (profilePhotoMode) { + roundClipPath.reset(); + roundClipPath.addRect(dstRect.left, dstRect.top, dstRect.right, dstRect.bottom, Path.Direction.CW); + roundClipPath.addCircle(centerX, centerY, radius, Path.Direction.CCW); + roundClipPath.close(); + + final int s = Views.save(c); + c.clipPath(roundClipPath); + c.drawRect(dstRect, Paints.fillingPaint(ColorUtils.alphaColor(Color.alpha(overlayColor) / 300f, overlayColor))); + Views.restore(c, s); + + c.drawCircle(centerX, centerY, radius, Paints.strokeSmallPaint(OUTER_LINE_COLOR)); + } + if (cornerHeight > 0) { float cornerAlpha = mode == MODE_NORMAL || mode == MODE_INVISIBLE ? 1f - activeFactor : 1f; int cornerColor = ColorUtils.fromToArgb(OUTER_LINE_COLOR, CORNER_COLOR, cornerAlpha); @@ -785,7 +812,7 @@ private void cancelPositionAnimator () { } } - private void normalizeProportion () { + private void normalizeProportion (boolean animated) { float heightProportion = getProportion(); if (heightProportion == 0) { cancelPositionAnimator(); @@ -836,7 +863,11 @@ private void normalizeProportion () { newRight = 1.0; } - animateArea(newLeft, newTop, newRight, newBottom, false, false); + if (animated) { + animateArea(newLeft, newTop, newRight, newBottom, false, false); + } else { + setArea(newLeft, newTop, newRight, newBottom, true); + } } public boolean resetArea (boolean forceAnimation, boolean useFastAnimation) { @@ -903,11 +934,11 @@ public int getTargetHeight () { return targetHeight; } - public void setFixedProportion (int big, int small) { + public void setFixedProportion (int big, int small, boolean animated) { if (this.proportionBig != big || this.proportionSmall != small) { this.proportionBig = big; this.proportionSmall = small; - normalizeProportion(); + normalizeProportion(animated); } } diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropState.java b/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropState.java index 909364e813..4bbe0a69ae 100644 --- a/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropState.java +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropState.java @@ -18,23 +18,29 @@ import org.thunderdog.challegram.Log; +import me.vkryl.core.BitwiseUtils; import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; public class CropState { + public static final int FLAG_MIRROR_HORIZONTALLY = 1; + public static final int FLAG_MIRROR_VERTICALLY = 1 << 1; + private double left = 0.0, top = 0.0, right = 1.0, bottom = 1.0; private int rotateBy = 0; private float degreesAroundCenter = 0; + private int flags; public CropState () { } - public CropState (double left, double top, double right, double bottom, int rotateBy, float degreesAroundCenter) { + public CropState (double left, double top, double right, double bottom, int rotateBy, float degreesAroundCenter, int flags) { this.left = left; this.top = top; this.right = right; this.bottom = bottom; this.rotateBy = rotateBy; this.degreesAroundCenter = degreesAroundCenter; + this.flags = flags; } public CropState (CropState copy) { @@ -49,6 +55,7 @@ public void set (CropState copy) { this.bottom = copy.bottom; this.rotateBy = copy.rotateBy; this.degreesAroundCenter = copy.degreesAroundCenter; + this.flags = copy.flags; } else { this.left = 0.0; this.top = 0.0; @@ -56,6 +63,7 @@ public void set (CropState copy) { this.bottom = 1.0; this.rotateBy = 0; this.degreesAroundCenter = 0.0f; + this.flags = 0; } } @@ -65,8 +73,8 @@ public void set (CropState copy) { } try { String[] data = in.split(":"); - if (data.length != 6) { - throw new IllegalArgumentException("data.length != 6 (" + data.length + ", " + in + ")"); + if (data.length < 6 || data.length > 7) { + throw new IllegalArgumentException("data.length < 6 || data.length > 7 (" + data.length + ", " + in + ")"); } double left = Double.parseDouble(data[0]); double top = Double.parseDouble(data[1]); @@ -74,7 +82,8 @@ public void set (CropState copy) { double bottom = Double.parseDouble(data[3]); int rotateBy = Integer.parseInt(data[4]); float degreesAroundCenter = Float.parseFloat(data[5]); - return new CropState(left, top, right, bottom, rotateBy, degreesAroundCenter); + int flags = data.length > 6 ? Integer.parseInt(data[6]) : 0; + return new CropState(left, top, right, bottom, rotateBy, degreesAroundCenter, flags); } catch (Throwable t) { Log.e(t); } @@ -93,11 +102,12 @@ public String toString () { ':' + rotateBy + ':' + - degreesAroundCenter; + degreesAroundCenter + + (flags != 0 ? ":" + flags : ""); } public boolean isEmpty () { - return left == 0.0 && right == 1.0 && top == 0.0 && bottom == 1.0 && rotateBy == 0 && degreesAroundCenter == 0; + return isRegionEmpty() && rotateBy == 0 && degreesAroundCenter == 0 && flags == 0; } public boolean isRegionEmpty () { @@ -116,6 +126,26 @@ public boolean hasRotations () { return rotateBy != 0 || degreesAroundCenter != 0; } + public boolean hasFlag (int flag) { + return BitwiseUtils.hasAllFlags(flags, flag); + } + + public boolean needMirror () { + return BitwiseUtils.hasFlag(flags, FLAG_MIRROR_HORIZONTALLY | FLAG_MIRROR_VERTICALLY); + } + + public boolean needMirrorHorizontally () { + return hasFlag(FLAG_MIRROR_HORIZONTALLY); + } + + public boolean needMirrorVertically () { + return hasFlag(FLAG_MIRROR_VERTICALLY); + } + + public int getFlags () { + return flags; + } + public boolean compare (CropState state) { if (state == null) { return isEmpty(); @@ -126,12 +156,13 @@ public boolean compare (CropState state) { this.right == state.right && this.bottom == state.bottom && this.rotateBy == state.rotateBy && - this.degreesAroundCenter == state.degreesAroundCenter; + this.degreesAroundCenter == state.degreesAroundCenter && + this.flags == state.flags; } @Override public boolean equals (Object obj) { - return this == obj || (obj != null && obj instanceof CropState && compare((CropState) obj)); + return this == obj || (obj instanceof CropState && compare((CropState) obj)); } private void invokeCallbacks (boolean rectChanged) { @@ -154,6 +185,13 @@ public void setDegreesAroundCenter (float degreesAroundCenter) { } } + public void setFlags (int flags) { + if (this.flags != flags) { + this.flags = flags; + invokeCallbacks(false); + } + } + public double getLeft () { return left; } diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropTargetView.java b/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropTargetView.java index 522eafd611..6a3d7037d5 100644 --- a/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropTargetView.java +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/crop/CropTargetView.java @@ -160,10 +160,18 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { } } + private float mirrorHorizontallyFactor = 0; + private float mirrorVerticallyFactor = 0; + + public void setMirrorFactors (float mirrorHorizontallyFactor, float mirrorVerticallyFactor) { + this.mirrorHorizontallyFactor = mirrorHorizontallyFactor; + this.mirrorVerticallyFactor = mirrorVerticallyFactor; + invalidate(); + } + + @Override protected void onDraw (Canvas c) { - // c.drawColor(0xffff0000); - int cx = getMeasuredWidth() / 2; int cy = getMeasuredHeight() / 2; @@ -174,7 +182,7 @@ protected void onDraw (Canvas c) { c.scale(rotationScale, rotationScale, cx, cy); } - DrawAlgorithms.drawScaledBitmap(getMeasuredWidth(), getMeasuredHeight(), c, bitmap, rotation, paintState); + DrawAlgorithms.drawScaledBitmap(getMeasuredWidth(), getMeasuredHeight(), c, bitmap, rotation, mirrorHorizontallyFactor, mirrorVerticallyFactor, paintState); if (saved) { c.restore(); diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/data/MediaItem.java b/app/src/main/java/org/thunderdog/challegram/mediaview/data/MediaItem.java index a775d21445..2715dd784b 100644 --- a/app/src/main/java/org/thunderdog/challegram/mediaview/data/MediaItem.java +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/data/MediaItem.java @@ -927,9 +927,11 @@ public static MediaItem valueOf (BaseActivity context, Tdlib tdlib, TdApi.Messag if (msg == null) { return null; } + //noinspection SwitchIntDef switch (msg.content.getConstructor()) { case TdApiExt.MessageChatEvent.CONSTRUCTOR: { TdApiExt.MessageChatEvent event = ((TdApiExt.MessageChatEvent) msg.content); + //noinspection SwitchIntDef switch (event.event.action.getConstructor()) { case TdApi.ChatEventPhotoChanged.CONSTRUCTOR: { TdApi.ChatEventPhotoChanged changedPhoto = (TdApi.ChatEventPhotoChanged) event.event.action; @@ -937,6 +939,9 @@ public static MediaItem valueOf (BaseActivity context, Tdlib tdlib, TdApi.Messag return new MediaItem(context, tdlib, msg.chatId, 0, changedPhoto.newPhoto != null ? changedPhoto.newPhoto : changedPhoto.oldPhoto).setSourceSender(event.event.memberId).setSourceDate(event.event.date); } } + default: { + Td.assertChatEventAction_57377883(); + } } break; } @@ -1186,13 +1191,13 @@ public long getTotalDurationUs () { return -1; } - public int getTTL () { - return sourceGalleryFile != null ? sourceGalleryFile.getTTL() : 0; + public @Nullable TdApi.MessageSelfDestructType getSelfDestructType () { + return sourceGalleryFile != null ? sourceGalleryFile.getSelfDestructType() : null; } - public void setTTL (int ttl) { + public void setSelfDestructType (@Nullable TdApi.MessageSelfDestructType selfDestructType) { if (sourceGalleryFile != null) { - sourceGalleryFile.setTTL(ttl); + sourceGalleryFile.setSelfDestructType(selfDestructType); } } @@ -1467,9 +1472,19 @@ public boolean isSecret () { return secretPhoto != null; } - public void viewSecretContent () { - if (secretPhoto != null) { - secretPhoto.readContent(); + public boolean isViewOnce () { + return msg != null && msg.selfDestructType != null && msg.selfDestructType.getConstructor() == TdApi.MessageSelfDestructTypeImmediately.CONSTRUCTOR; + } + + public void viewContent (boolean isClosed) { + if (isClosed) { + if (isViewOnce()) { + tdlib.send(new TdApi.OpenMessageContent(msg.chatId, msg.id), tdlib.typedOkHandler()); + } + } else { + if (secretPhoto != null) { + secretPhoto.readContent(); + } } } @@ -1672,11 +1687,11 @@ public TdApi.InputMessageContent createShareContent (TdApi.FormattedText caption return new TdApi.InputMessageAnimation(file, null, null, 3, targetFile.length, targetFile.length, null, false); } } - return new TdApi.InputMessagePhoto(file, null, null, 640, 640, caption, 0, false); + return new TdApi.InputMessagePhoto(file, null, null, 640, 640, caption, null, false); case TYPE_PHOTO: - return new TdApi.InputMessagePhoto(file, null, null, width, height, caption, 0, false); + return new TdApi.InputMessagePhoto(file, null, null, width, height, caption, null, false); case TYPE_VIDEO: - return new TdApi.InputMessageVideo(file, null, null, sourceVideo.duration, sourceVideo.width, sourceVideo.height, sourceVideo.supportsStreaming, caption, 0, false); + return new TdApi.InputMessageVideo(file, null, null, sourceVideo.duration, sourceVideo.width, sourceVideo.height, sourceVideo.supportsStreaming, caption, null, false); case TYPE_GIF: return new TdApi.InputMessageAnimation(file, null, null, sourceAnimation.duration, sourceAnimation.width, sourceAnimation.height, caption, false); } diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/gl/EGLEditorView.java b/app/src/main/java/org/thunderdog/challegram/mediaview/gl/EGLEditorView.java index a7990878df..0410a2f330 100644 --- a/app/src/main/java/org/thunderdog/challegram/mediaview/gl/EGLEditorView.java +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/gl/EGLEditorView.java @@ -168,6 +168,7 @@ public boolean onSurfaceTextureDestroyed (SurfaceTexture surface) { @Override public void onSurfaceTextureUpdated (SurfaceTexture surface) { } }); + textureView.setScaleX(-1); contentWrap.addView(textureView); break; } @@ -219,12 +220,20 @@ private void applyCurrentStyles () { float scale = Math.max(W / w, H / h); contentWrap.setScaleX(scale); contentWrap.setScaleY(scale); + if (textureView != null) { + textureView.setScaleX(sourceCropState.needMirrorHorizontally() ? -1 : 1); + textureView.setScaleY(sourceCropState.needMirrorVertically() ? -1 : 1); + } } else { contentWrap.setRotation(0); contentWrap.setScaleX(1f); contentWrap.setScaleY(1f); contentWrap.setTranslationX(0f); contentWrap.setTranslationY(0f); + if (textureView != null) { + textureView.setScaleX(1); + textureView.setScaleY(1); + } } textureWrap.setRotation(sourceRotation); } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/ComplexHeaderView.java b/app/src/main/java/org/thunderdog/challegram/navigation/ComplexHeaderView.java index cbec982f9c..42c4b1b990 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/ComplexHeaderView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/ComplexHeaderView.java @@ -700,7 +700,21 @@ private void layoutSubtext () { } private TextColorSet getTitleColorSet () { - return this::getTitleColor; + return new TextColorSet() { + @Override + public int defaultTextColor () { + return getTitleColor(); + } + + @Override + public long mediaTextComplexColor () { + if (getAvatarExpandFactor() == 1f) { + return Theme.newComplexColor(true, ColorId.white); + } else { + return Theme.newComplexColor(false, getTitleColor()); + } + } + }; } private int getTypingColor () { @@ -898,7 +912,7 @@ protected void onDraw (Canvas c) { float baseIconLeft = trimmedTitle.getWidth() + (showLock ? Screen.dp(16f) : 0) - + (emojiStatusHelper.needDrawEmojiStatus() ? emojiStatusHelper.getWidth() + Screen.dp(6): 0); + + (emojiStatusHelper.needDrawEmojiStatus() ? emojiStatusHelper.getWidth() + Screen.dp(6) : 0); float toIconLeft = trimmedTitleExpanded != null ? trimmedTitleExpanded.getLastLineWidth() : baseIconLeft; float iconLeft = baseIconLeft + (toIconLeft - baseIconLeft) * avatarExpandFactor; float iconTop = trimmedTitleExpanded != null ? (trimmedTitleExpanded.getHeight() - trimmedTitle.getHeight()) * avatarExpandFactor : 0; diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/DoubleHeaderView.java b/app/src/main/java/org/thunderdog/challegram/navigation/DoubleHeaderView.java index 7527bed7ff..68da87f974 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/DoubleHeaderView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/DoubleHeaderView.java @@ -16,6 +16,8 @@ import android.content.Context; import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.text.Layout; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; @@ -23,12 +25,15 @@ import android.widget.FrameLayout; import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import org.thunderdog.challegram.U; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Fonts; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; @@ -44,6 +49,8 @@ public class DoubleHeaderView extends FrameLayoutFix implements RtlCheckListener, FactorAnimator.Target, TextChangeDelegate, Destroyable { private final EmojiTextView titleView, subtitleView; + private @Nullable Drawable titleIcon; + public DoubleHeaderView (Context context) { super(context); @@ -52,7 +59,20 @@ public DoubleHeaderView (Context context) { params = FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP | (Lang.rtl() ? Gravity.RIGHT : Gravity.LEFT)); params.topMargin = Screen.dp(5f); - titleView = new EmojiTextView(context); + titleView = new EmojiTextView(context) { + @Override + protected void onDraw (Canvas canvas) { + super.onDraw(canvas); + if (titleIcon != null && titleIcon.getMinimumWidth() > 0 && titleIcon.getMinimumHeight() > 0) { + Layout layout = getLayout(); + float left = getPaddingLeft() + (layout != null ? U.getWidth(layout) + Screen.dp(1f) : 0); + if (left + titleIcon.getMinimumWidth() <= getWidth() - getPaddingRight()) { + float top = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2f - titleIcon.getMinimumHeight() / 2f; + Drawables.draw(canvas, titleIcon, left, top, Paints.getIconGrayPorterDuffPaint()); + } + } + } + }; titleView.setScrollDisabled(true); titleView.setTextColor(Theme.headerTextColor()); titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18f); @@ -158,6 +178,14 @@ public void setTitle (CharSequence title) { Views.setMediumText(titleView, title); } + public void setTitleIcon (@DrawableRes int iconRes) { + Drawable icon = iconRes != 0 ? Drawables.get(iconRes) : null; + if (titleIcon != icon) { + titleIcon = icon; + titleView.invalidate(); + } + } + public void setSubtitle (@StringRes int subtitleRes) { subtitleView.setText(Lang.getString(subtitleRes)); } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/DrawerController.java b/app/src/main/java/org/thunderdog/challegram/navigation/DrawerController.java index 1437cc370d..f65f6422ee 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/DrawerController.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/DrawerController.java @@ -37,13 +37,11 @@ import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.BuildConfig; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.attach.CustomItemAnimator; import org.thunderdog.challegram.component.base.TogglerView; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; -import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.GlobalAccountListener; import org.thunderdog.challegram.telegram.GlobalCountersListener; @@ -65,6 +63,7 @@ import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.ui.CallListController; import org.thunderdog.challegram.ui.ChatsController; import org.thunderdog.challegram.ui.FeatureToggles; import org.thunderdog.challegram.ui.ListItem; @@ -259,6 +258,9 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { } items.add(new ListItem(ListItem.TYPE_DRAWER_ITEM, R.id.btn_contacts, R.drawable.baseline_perm_contact_calendar_24, R.string.Contacts)); + if (Settings.instance().chatFoldersEnabled()) { + items.add(new ListItem(ListItem.TYPE_DRAWER_ITEM, R.id.btn_calls, R.drawable.baseline_call_24, R.string.Calls)); + } items.add(new ListItem(ListItem.TYPE_DRAWER_ITEM, R.id.btn_savedMessages, R.drawable.baseline_bookmark_24, R.string.SavedMessages)); this.settingsErrorIcon = getSettingsErrorIcon(); items.add(new ListItem(ListItem.TYPE_DRAWER_ITEM, R.id.btn_settings, R.drawable.baseline_settings_24, R.string.Settings)); @@ -600,7 +602,7 @@ public ForceTouchView.ActionListener onCreateActions (View v, ForceTouchView.For context.setExcludeHeader(true); context.setTdlib(account.tdlib()); - context.setBoundUserId(account.tdlib().myUserId()); + context.setBoundAccountId(account.id); return new ForceTouchView.ActionListener() { @Override @@ -658,28 +660,18 @@ private void openSavedMessages () { } if (!creatingStorageChat) { creatingStorageChat = true; - tdlib.client().send(new TdApi.CreatePrivateChat(userId, true), object -> { - switch (object.getConstructor()) { - case TdApi.Chat.CONSTRUCTOR: { - final long chatId = TD.getChatId(object); - tdlib.ui().post(() -> { - creatingStorageChat = false; - if (factor == 1f) { - openChat(tdlib, chatId); - } - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - creatingStorageChat = false; - UI.showError(object); - break; - } - default: { + tdlib.send(new TdApi.CreatePrivateChat(userId, true), (remoteChat, error) -> { + if (error != null) { + creatingStorageChat = false; + UI.showError(error); + } else { + final long chatId = remoteChat.id; + tdlib.ui().post(() -> { creatingStorageChat = false; - Log.unexpectedTdlibResponse(object, TdApi.CreatePrivateChat.class, TdApi.Chat.class); - break; - } + if (factor == 1f) { + openChat(tdlib, chatId); + } + }); } }); } @@ -837,6 +829,8 @@ public void onClick (View v) { } else if (viewId == R.id.btn_contacts) { openContacts(); // openEmptyChat(); + } else if (viewId == R.id.btn_calls) { + openCallList(); } else if (viewId == R.id.btn_reportBug) { if (Test.NEED_CLICK) { Test.onClick(context); @@ -973,6 +967,10 @@ private void openContacts () { }); } + private void openCallList() { + openController(new CallListController(context, context.currentTdlib())); + } + private boolean ignoreClose; private void openController (ViewController c) { diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/DrawerHeaderView.java b/app/src/main/java/org/thunderdog/challegram/navigation/DrawerHeaderView.java index c76ec6beeb..f7b45154aa 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/DrawerHeaderView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/DrawerHeaderView.java @@ -40,8 +40,8 @@ import org.thunderdog.challegram.telegram.TdlibAccount; import org.thunderdog.challegram.telegram.TdlibBadgeCounter; import org.thunderdog.challegram.telegram.TdlibManager; -import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.telegram.TdlibUi; +import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; @@ -154,6 +154,14 @@ private int getTextColor (float factor) { return ColorUtils.fromToArgb(ColorUtils.compositeColor(Theme.headerTextColor(), Theme.getColor(ColorId.drawerText)), Theme.getColor(ColorId.white), factor); } + private long getMediaTextComplexColor (float factor) { + if (factor == 1f) { + return Theme.newComplexColor(true, ColorId.white); + } else { + return Theme.newComplexColor(false, getTextColor(factor)); + } + } + // Clicks @@ -399,7 +407,7 @@ public void draw (Canvas c, DoubleImageReceiver receiver, int viewWidth, int vie } else { RectF rectF = Paints.getRectF(); rectF.set(left, top, right, bottom); - c.drawRoundRect(rectF, cornerRadius, cornerRadius, Paints.fillingPaint(ColorUtils.alphaColor(avatarAlphaFactor, Theme.getColor(avatarPlaceholder.metadata.colorId)))); + c.drawRoundRect(rectF, cornerRadius, cornerRadius, Paints.fillingPaint(ColorUtils.alphaColor(avatarAlphaFactor, avatarPlaceholder.metadata.accentColor.getPrimaryColor()))); avatarPlaceholder.draw(c, cx, cy, avatarAlphaFactor, radius, false); } } @@ -449,6 +457,11 @@ public void draw (Canvas c, DoubleImageReceiver receiver, int viewWidth, int vie public int defaultTextColor () { return context.getTextColor(lastAvatarFactor); } + + @Override + public long mediaTextComplexColor () { + return context.getMediaTextComplexColor(lastAvatarFactor); + } } private DisplayInfo displayInfoFuture; diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/DrawerItemView.java b/app/src/main/java/org/thunderdog/challegram/navigation/DrawerItemView.java index 23dac657dc..63358657e8 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/DrawerItemView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/DrawerItemView.java @@ -25,9 +25,7 @@ import androidx.annotation.Nullable; import org.thunderdog.challegram.core.Lang; -import org.thunderdog.challegram.data.AvatarPlaceholder; import org.thunderdog.challegram.loader.AvatarReceiver; -import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.telegram.TGLegacyManager; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibAccount; @@ -198,8 +196,8 @@ public void setAvatar (Tdlib tdlib, long chatId) { public void setEmojiStatus (TdlibAccount account) { TextColorSet colorSet = new TextColorSetOverride(TextColorSets.Regular.NORMAL) { @Override - public int emojiStatusColor () { - return Theme.getColor(ColorId.iconActive); + public long mediaTextComplexColor () { + return Theme.newComplexColor(true, ColorId.iconActive); } }; emojiStatusHelper.setSharedUsageId("account_" + account.id); @@ -208,13 +206,7 @@ public int emojiStatusColor () { } public void setAvatar (TdlibAccount account) { - AvatarPlaceholder.Metadata placeholder = account.getAvatarPlaceholderMetadata(); - ImageFile imageFile = account.getAvatarFile(false); - if (imageFile != null) { - receiver.requestSpecific(tdlib, imageFile, AvatarReceiver.Options.NONE); - } else { - receiver.requestPlaceholder(tdlib, placeholder, AvatarReceiver.Options.NONE); - } + receiver.requestAccount(tdlib, account.id, AvatarReceiver.Options.NONE); } public void setError (boolean error, int errorIcon, boolean animated) { @@ -323,7 +315,7 @@ public void onDraw (Canvas c) { if (trimmedText != null) { trimmedText.draw(c, textLeft, textLeft + trimmedText.getWidth(), 0, Screen.dp(17f)); } - emojiStatusHelper.draw(c, textLeft + (trimmedText != null ? trimmedText.getWidth() + Screen.dp(6): 0), Screen.dp(17f)); + emojiStatusHelper.draw(c, textLeft + (trimmedText != null ? trimmedText.getWidth() + Screen.dp(6) : 0), Screen.dp(17f)); if (receiver != null) { layoutReceiver(); if (receiver.needPlaceholder()) { diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/EditHeaderView.java b/app/src/main/java/org/thunderdog/challegram/navigation/EditHeaderView.java index 0c6fa3b4f0..752a887653 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/EditHeaderView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/EditHeaderView.java @@ -16,6 +16,7 @@ import android.content.Context; import android.graphics.Canvas; +import android.graphics.Path; import android.graphics.drawable.Drawable; import android.text.Editable; import android.text.InputFilter; @@ -31,6 +32,7 @@ import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.ImageFileLocal; +import org.thunderdog.challegram.loader.ImageGalleryFile; import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Keyboard; @@ -51,6 +53,7 @@ public class EditHeaderView extends FrameLayoutFix implements RtlCheckListener, private final ViewController parent; private HeaderEditText input; private final ImageReceiver receiver; + private final Path clipPath = new Path(); private final Drawable icon; @@ -61,7 +64,8 @@ public EditHeaderView (Context context, ViewController parent) { setWillNotDraw(false); - receiver = new ImageReceiver(this, Screen.dp(30.5f)); + receiver = new ImageReceiver(this, 0); + receiver.prepareToBeCropped(); FrameLayoutFix.LayoutParams params; @@ -125,10 +129,18 @@ public void onTextChanged (CharSequence s, int start, int before, int count) { performReadyCallback(s.toString().trim().length() > 0); } + private Runnable onPhotoClickListener; + + public void setOnPhotoClickListener (Runnable onPhotoClickListener) { + this.onPhotoClickListener = onPhotoClickListener; + } + private void onPhotoClicked () { if (input.isEnabled()) { Keyboard.hide(input); - parent.tdlib().ui().showChangePhotoOptions(parent, file != null); + if (onPhotoClickListener != null) { + onPhotoClickListener.run(); + } ViewUtils.onClick(this); } } @@ -247,40 +259,38 @@ private void layoutReceiver () { avatarLeft = getMeasuredWidth() - avatarLeft - avatarSize; } - receiver.setRadius(avatarRadius); + // receiver.setRadius(avatarRadius); receiver.setBounds(avatarLeft, 0, avatarLeft + avatarSize, avatarSize); + clipPath.reset(); + clipPath.addCircle(receiver.centerX(), receiver.centerY(), avatarSize / 2f, Path.Direction.CW); + clipPath.close(); } @Override protected void onDraw (Canvas c) { layoutReceiver(); + int s = Views.save(c); + c.clipPath(clipPath); receiver.draw(c); + Views.restore(c, s); int cx = receiver.centerX(); int cy = receiver.centerY(); c.drawCircle(cx, cy, avatarRadius, Paints.fillingPaint(0x20000000)); - Drawables.draw(c, icon, cx - (int) (icon.getMinimumWidth() * .5f), cy - (int) (icon.getMinimumHeight() * .5f), Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, icon, cx - (int) (icon.getMinimumWidth() * .5f), cy - (int) (icon.getMinimumHeight() * .5f), Paints.whitePorterDuffPaint()); } public void setInputEnabled (boolean enabled) { input.setEnabled(enabled); } - private ImageFile file; + private ImageGalleryFile file; - public void setPhoto (ImageFile file) { + public void setPhoto (ImageGalleryFile file) { this.file = file; receiver.requestFile(file); } - public boolean isPhotoChanged (boolean changedIfNull) { - return (file == null && changedIfNull) || (file != null && file instanceof ImageFileLocal); - } - - public String getPhoto () { - return file == null || !(file instanceof ImageFileLocal) ? null : ((ImageFileLocal) file).getPath(); - } - - public ImageFile getImageFile () { + public ImageGalleryFile getImageFile () { return file; } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/HeaderFilling.java b/app/src/main/java/org/thunderdog/challegram/navigation/HeaderFilling.java index 39f210cbce..091df57b02 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/HeaderFilling.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/HeaderFilling.java @@ -51,6 +51,7 @@ import org.thunderdog.challegram.ui.CallController; import org.thunderdog.challegram.ui.PlaybackController; import org.thunderdog.challegram.unsorted.Size; +import org.thunderdog.challegram.util.RateLimiter; import org.thunderdog.challegram.util.text.Text; import org.thunderdog.challegram.util.text.TextColorSet; import org.thunderdog.challegram.util.text.TextColorSets; @@ -68,6 +69,7 @@ import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.lambda.Destroyable; +import me.vkryl.td.Td; public class HeaderFilling extends Drawable implements TGLegacyAudioManager.PlayListener, FactorAnimator.Target, CallManager.CurrentCallListener, TdlibCache.CallStateChangeListener, Runnable, TGPlayerController.TrackChangeListener, TGPlayerController.TrackListener, ClickHelper.Delegate, Destroyable, TGLegacyManager.EmojiLoadListener { private HeaderView headerView; // Header that holds the filling @@ -479,7 +481,7 @@ public void onTrackStateChanged (Tdlib tdlib, long chatId, long messageId, int f @Override public void onTrackPlayProgress (Tdlib tdlib, long chatId, long messageId, int fileId, float progress, long playPosition, long playDuration, boolean isBuffering) { - if (currentTrack != null && currentTrack.content.getConstructor() != TdApi.MessageVideoNote.CONSTRUCTOR) { + if (currentTrack != null && !Td.isVideoNote(currentTrack.content)) { setSeekFactor(progress); } } @@ -498,7 +500,7 @@ public void openPlayer () { if (navigationController == null || playingMessageTdlib == null || playingMessage == null) { return; } - if (playingMessage.content.getConstructor() != TdApi.MessageAudio.CONSTRUCTOR) { + if (!Td.isAudio(playingMessage.content)) { if (playingMessage.chatId == 0 || playingMessage.id == 0) { return; } @@ -556,7 +558,7 @@ private static String buildPrivateTitle (Tdlib tdlib, TdApi.Message message) { } else { title = tdlib.messageAuthor(message); if (StringUtils.isEmpty(title)) { - title = Lang.getString(message.content.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR ? R.string.AttachRound : R.string.AttachAudio); + title = Lang.getString(Td.isVideoNote(message.content) ? R.string.AttachRound : R.string.AttachAudio); } } return title; @@ -929,6 +931,7 @@ private void setCallFlashing (boolean isCallFlashing) { callFlashAnimator.animateTo(1f); } } else { + flashCallBar.cancelIfScheduled(); if (callFlashAnimator != null && callFlashAnimator.getFactor() == 0f) { callFlashAnimator.forceFactor(0f); } @@ -998,7 +1001,7 @@ private void drawOngoingCall (Canvas c, int playerTop, float rectWidth, int play c.drawRect(0, playerTop, rectWidth, playerBottom, Paints.fillingPaint(playerFillingColor)); - Paint iconPaint = Paints.getPorterDuffPaint(0xffffffff); + Paint iconPaint = Paints.whitePorterDuffPaint(); // Mute button iconPaint.setAlpha((int) (255f * dropShadowAlpha * callActiveFactor * (1f - callIncomingFactor))); @@ -1148,13 +1151,21 @@ public void onFactorChanged (int id, float factor, float fraction, FactorAnimato } } + private final RateLimiter flashCallBar = new RateLimiter(() -> { + if (isCallFlashing) { + callFlashAnimator.animateTo(1f); + } + }, 100L, null); + @Override public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { switch (id) { case ANIMATOR_CALL_FLASH_ID: { - callFlashAnimator.forceFactor(0f); - if (isCallFlashing) { - callFlashAnimator.animateTo(1f); + if (finalFactor == 1f) { + callFlashAnimator.forceFactor(0f); + if (isCallFlashing) { + flashCallBar.run(); + } } break; } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/HeaderView.java b/app/src/main/java/org/thunderdog/challegram/navigation/HeaderView.java index ab1c5950ae..d33e91b89e 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/HeaderView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/HeaderView.java @@ -782,9 +782,7 @@ public HeaderButton addMoreButton (LinearLayout menu, @NonNull ViewController } public HeaderButton addMoreButton (LinearLayout menu, @Nullable ViewController themeProvider, @ColorId int colorId) { - HeaderButton button; - menu.addView(button = genButton(R.id.menu_btn_more, R.drawable.baseline_more_vert_24, colorId, themeProvider, Screen.dp(49f), this), Lang.rtl() ? 0 : -1); - return button; + return addButton(menu, R.id.menu_btn_more, R.drawable.baseline_more_vert_24, 49f, themeProvider, colorId); } public HeaderButton addSearchButton (LinearLayout menu, @NonNull ViewController themeProvider) { @@ -792,8 +790,12 @@ public HeaderButton addSearchButton (LinearLayout menu, @NonNull ViewController< } public HeaderButton addSearchButton (LinearLayout menu, @Nullable ViewController themeProvider, @ColorId int colorId) { + return addButton(menu, R.id.menu_btn_search, R.drawable.baseline_search_24, 49f, themeProvider, colorId); + } + + public HeaderButton addButton (LinearLayout menu, @IdRes int buttonId, @DrawableRes int iconRes, float widthDp, @Nullable ViewController themeProvider, @ColorId int colorId) { HeaderButton button; - menu.addView(button = genButton(R.id.menu_btn_search, R.drawable.baseline_search_24, colorId, themeProvider, Screen.dp(49f), this), Lang.rtl() ? 0 : -1); + menu.addView(button = genButton(buttonId, iconRes, colorId, themeProvider, Screen.dp(widthDp), this), Lang.rtl() ? 0 : -1); return button; } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/MenuMoreWrap.java b/app/src/main/java/org/thunderdog/challegram/navigation/MenuMoreWrap.java index 8abf1ca8de..9d2790c767 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/MenuMoreWrap.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/MenuMoreWrap.java @@ -29,6 +29,7 @@ import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.ScrollView; import android.widget.TextView; import androidx.annotation.Nullable; @@ -85,11 +86,26 @@ public class MenuMoreWrap extends LinearLayout implements Animated { private @Nullable ThemeDelegate forcedTheme; private final ComplexReceiver complexAvatarReceiver; + private final LinearLayout itemsLayout; + public MenuMoreWrap (Context context) { + this(context, /* scrollable */ false); + } + + public MenuMoreWrap (Context context, boolean scrollable) { super(context); setWillNotDraw(false); complexAvatarReceiver = new ComplexReceiver(this); factorAnimator.forceFactor(-1); + if (scrollable) { + itemsLayout = new LinearLayout(context); + itemsLayout.setOrientation(LinearLayout.VERTICAL); + ScrollView scrollView = new ScrollView(context); + scrollView.addView(itemsLayout, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + addView(scrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + } else { + itemsLayout = this; + } } public void updateDirection () { @@ -186,7 +202,7 @@ public View addItem (@Nullable Tdlib tdlib, HapticMenuHelper.MenuItem menuItem, } final int maxWidth = Screen.dp(250); - final int textRightOffset = Screen.dp(menuItem.isLocked ? 41: 17); + final int textRightOffset = Screen.dp(menuItem.isLocked ? 41 : 17); final Drawable finalIcon = menuItem.iconResId != 0 ? Drawables.get(getResources(), menuItem.iconResId) : menuItem.icon; final AvatarReceiver receiver = (menuItem.messageSenderId != null && menuItem.iconResId == 0) ? complexAvatarReceiver.getAvatarReceiver(Td.getSenderId(menuItem.messageSenderId)) : null; @@ -208,7 +224,7 @@ protected void onDraw (Canvas canvas) { if (icon != null) { float x = getMeasuredWidth() - Screen.dp(17 + 16); float y = (getMeasuredHeight() - icon.getMinimumHeight()) / 2f; - Drawables.draw(canvas, icon, x, y, Paints.getPorterDuffPaint(Theme.getColor(ColorId.text))); + Drawables.draw(canvas, icon, x, y, PorterDuffPaint.get(ColorId.text)); } } @@ -338,7 +354,7 @@ protected void onDraw (Canvas canvas) { Views.setClickable(frameLayout); RippleSupport.setTransparentSelector(frameLayout); - addView(frameLayout); + itemsLayout.addView(frameLayout); frameLayout.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); frameLayout.setTag(frameLayout.getMeasuredWidth()); return frameLayout; @@ -386,7 +402,7 @@ public TextView addItem (int id, CharSequence title, int iconRes, Drawable icon, } Views.setClickable(menuItem); RippleSupport.setTransparentSelector(menuItem); - addView(menuItem); + itemsLayout.addView(menuItem); menuItem.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); menuItem.setTag(menuItem.getMeasuredWidth()); return menuItem; @@ -446,7 +462,7 @@ public void scaleOut (Animator.AnimatorListener listener) { Views.animate(this, START_SCALE, START_SCALE, 0f, 120l, 0l, AnimatorUtils.ACCELERATE_INTERPOLATOR, listener); } - private Runnable pendingAction; + private @Nullable Runnable pendingAction; @Override public void runOnceViewBecomesReady (View view, Runnable action) { @@ -462,7 +478,7 @@ protected void onLayout (boolean changed, int l, int t, int r, int b) { } } - private FactorAnimator factorAnimator = new FactorAnimator(0, (a, b, c, d) -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 250L); + private final FactorAnimator factorAnimator = new FactorAnimator(0, (a, b, c, d) -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 250L); private int lastSelectedIndex = -1; public void processMoveEvent (View v, float x, float y, float startX, float startY) { @@ -474,7 +490,7 @@ public void processMoveEvent (View v, float x, float y, float startX, float star int innerY = sourceY - location[1]; int index = Math.floorDiv(innerY - Screen.dp(PADDING), Screen.dp(ITEM_HEIGHT)); - setSelectedIndex(index == MathUtils.clamp(index, 0, getChildCount() - 1) ? index: -1); + setSelectedIndex(index == MathUtils.clamp(index, 0, getChildCount() - 1) ? index : -1); // Log.i("HAPTIC INNER", String.format("INDEX %d", index)); } @@ -521,7 +537,7 @@ protected void onDraw (Canvas canvas) { canvas.restore(); } - if (lastSelectedIndex != -1) { + if (lastSelectedIndex != -1 && itemsLayout == this) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final float alpha = MathUtils.clamp(1f - Math.abs(factorAnimator.getFactor() - i)); diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/OverlayButtonWrap.java b/app/src/main/java/org/thunderdog/challegram/navigation/OverlayButtonWrap.java index 4083ff403c..1f242e68d3 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/OverlayButtonWrap.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/OverlayButtonWrap.java @@ -316,7 +316,8 @@ private void setHideFactor (float factor) { mainButton.setScaleY(scale); mainButton.setAlpha(1f - factor); } else { - mainButton.setTranslationY((float) (Screen.dp(16f) * 2 + mainButton.getMeasuredHeight()) * factor); + mainButton.setTranslationY((float) (Screen.dp(16f) * 2 + mainButton.getMeasuredHeight() + getPaddingBottom()) * factor); + mainButton.setAlpha(factor < 1f ? 1f : 0f); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/PagerHeaderView.java b/app/src/main/java/org/thunderdog/challegram/navigation/PagerHeaderView.java index 6af9ae8cfe..ec44ed4497 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/PagerHeaderView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/PagerHeaderView.java @@ -14,6 +14,9 @@ */ package org.thunderdog.challegram.navigation; +import android.view.View; + public interface PagerHeaderView extends RtlCheckListener { + View getView(); ViewPagerTopView getTopView (); } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/ReactionsOverlayView.java b/app/src/main/java/org/thunderdog/challegram/navigation/ReactionsOverlayView.java index d6e8632eaa..b645454fa7 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/ReactionsOverlayView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/ReactionsOverlayView.java @@ -33,6 +33,7 @@ import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; @@ -140,9 +141,9 @@ public static class ReactionInfo implements Destroyable, FactorAnimator.Target { @Nullable private AnimatedEmojiEffect animatedEmojiEffect; - private int repaintingColorStart; - private int repaintingColorEnd; - private boolean needsRepainting; + private @PorterDuffColorId int repaintingColorIdStart = ColorId.iconActive; + private @PorterDuffColorId int repaintingColorIdEnd = ColorId.iconActive; + private boolean needThemedColorFilter; // animation private static final int POSITION_ANIMATOR = 0; @@ -169,7 +170,6 @@ public ReactionInfo (ReactionsOverlayView parentView) { this.parentView = parentView; imageReceiver = new ImageReceiver(parentView, 0); gifReceiver = new GifReceiver(parentView); - repaintingColorStart = repaintingColorEnd = Theme.getColor(ColorId.iconActive); } public ReactionInfo setUseDefaultSprayAnimation (boolean useDefaultSprayAnimation) { @@ -180,7 +180,7 @@ public ReactionInfo setUseDefaultSprayAnimation (boolean useDefaultSprayAnimatio public ReactionInfo setSticker (TGStickerObj sticker, boolean isPlayOnce) { if (sticker.isDefaultPremiumStar()) { displayScale = sticker.getDisplayScale(); - needsRepainting = true; + needThemedColorFilter = true; drawable = Drawables.get(R.drawable.baseline_premium_star_28).mutate(); parentView.invalidate(); return this; @@ -188,7 +188,7 @@ public ReactionInfo setSticker (TGStickerObj sticker, boolean isPlayOnce) { ImageFile imageFile = sticker.getImage(); animation = sticker.getPreviewAnimation(); displayScale = sticker.getDisplayScale(); - needsRepainting |= sticker.isNeedRepainting(); + needThemedColorFilter |= sticker.needThemedColorFilter(); if (animation != null) { if (isPlayOnce) { animation.setPlayOnce(true); @@ -206,7 +206,7 @@ public ReactionInfo setEmojiStatusEffect (TGStickerObj sticker) { AnimatedEmojiDrawable d = new AnimatedEmojiDrawable(parentView); d.setSticker(sticker, true); this.animatedEmojiEffect = AnimatedEmojiEffect.createFrom(d, false); - this.needsRepainting |= sticker.isNeedRepainting(); + this.needThemedColorFilter |= sticker.needThemedColorFilter(); } return this; } @@ -220,9 +220,9 @@ public ReactionInfo setAnimatedPosition (Rect startPosition, Rect finishPosition return setPosition(new Rect(startPosition)); } - public ReactionInfo setRepaintingColors (int colorStart, int colorEnd) { - repaintingColorStart = colorStart; - repaintingColorEnd = colorEnd; + public ReactionInfo setRepaintingColorIds (@PorterDuffColorId int colorStart, @PorterDuffColorId int colorEnd) { + repaintingColorIdStart = colorStart; + repaintingColorIdEnd = colorEnd; return this; } @@ -308,13 +308,29 @@ public void draw (Canvas canvas) { canvas.drawRect(gifReceiver.getLeft(), gifReceiver.getTop(), gifReceiver.getRight(), gifReceiver.getBottom(), Paints.fillingPaint(0xaaff0000)); } - if (needsRepainting) { - canvas.saveLayerAlpha( - gifReceiver.getLeft() - gifReceiver.getWidth() / 4f, - gifReceiver.getTop() - gifReceiver.getHeight() / 4f, - gifReceiver.getRight() + gifReceiver.getWidth() / 4f, - gifReceiver.getBottom() + gifReceiver.getHeight() / 4f, - 255, Canvas.ALL_SAVE_FLAG); + if (needThemedColorFilter) { + float factor = positionAnimator != null ? positionAnimator.getFactor() : 1f; + if (factor == 1f || factor == 0f) { + @PorterDuffColorId int colorId = factor == 0f ? repaintingColorIdStart : repaintingColorIdEnd; + imageReceiver.setThemedPorterDuffColorId(colorId); + gifReceiver.setThemedPorterDuffColorId(colorId); + if (animatedEmojiEffect != null) { + animatedEmojiEffect.setThemedPorterDuffColorId(colorId); + } + } else { + int color = ColorUtils.fromToArgb(Theme.getColor(repaintingColorIdStart), Theme.getColor(repaintingColorIdEnd), factor); + imageReceiver.setPorterDuffColorFilter(color); + gifReceiver.setPorterDuffColorFilter(color); + if (animatedEmojiEffect != null) { + animatedEmojiEffect.setThemedPorterDuffColorId(color); + } + } + } else { + imageReceiver.disablePorterDuffColorFilter(); + gifReceiver.disablePorterDuffColorFilter(); + if (animatedEmojiEffect != null) { + animatedEmojiEffect.disablePorterDuffColorFilter(); + } } if (gifReceiver.needPlaceholder() || Config.DEBUG_REACTIONS_ANIMATIONS) { @@ -323,7 +339,7 @@ public void draw (Canvas canvas) { gifReceiver.draw(canvas); //canvas.drawRect(position, Paints.strokeBigPaint(Color.RED)); if (drawable != null) { - drawable.setColorFilter(Paints.getPorterDuffPaint(0xFFFFFFFF).getColorFilter()); + drawable.setColorFilter(Paints.whitePorterDuffPaint().getColorFilter()); drawable.draw(canvas); } if (animatedEmojiEffect != null) { @@ -333,16 +349,6 @@ public void draw (Canvas canvas) { canvas.restore(); } - if (needsRepainting) { - canvas.drawRect( - gifReceiver.getLeft() - gifReceiver.getWidth() / 4f, - gifReceiver.getTop() - gifReceiver.getHeight() / 4f, - gifReceiver.getRight() + gifReceiver.getWidth() / 4f, - gifReceiver.getBottom() + gifReceiver.getHeight() / 4f, - Paints.getSrcInPaint(ColorUtils.fromToArgb(repaintingColorStart, repaintingColorEnd, positionAnimator != null ? positionAnimator.getFactor(): 1f))); - canvas.restore(); - } - Views.restore(canvas, saveCount); } @@ -406,7 +412,7 @@ public void performDestroy () { imageReceiver.destroy(); gifReceiver.destroy(); if (animatedEmojiEffect != null) { - + animatedEmojiEffect.performDestroy(); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/SimpleHeaderView.java b/app/src/main/java/org/thunderdog/challegram/navigation/SimpleHeaderView.java index 177e118f38..8212f72219 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/SimpleHeaderView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/SimpleHeaderView.java @@ -29,7 +29,7 @@ import me.vkryl.android.widget.FrameLayoutFix; public class SimpleHeaderView extends FrameLayoutFix implements ColorSwitchPreparator, TextChangeDelegate { - private TextView title; + private final TextView title; public SimpleHeaderView (Context context) { super(context); diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/TelegramViewController.java b/app/src/main/java/org/thunderdog/challegram/navigation/TelegramViewController.java index 4d6c6bcdf9..7c03d0f5a2 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/TelegramViewController.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/TelegramViewController.java @@ -39,6 +39,7 @@ import org.thunderdog.challegram.data.TGFoundMessage; import org.thunderdog.challegram.telegram.TGLegacyManager; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; @@ -69,6 +70,7 @@ public TelegramViewController (@NonNull Context context, Tdlib tdlib) { // Search chats private CustomRecyclerView chatSearchView; + private TdlibMessageViewer.Viewport chatSearchViewport; private SettingsAdapter chatSearchAdapter; private SearchManager chatSearchManager; private boolean chatSearchDisallowScreenshots; @@ -114,6 +116,8 @@ protected boolean filterChatMessageSearchResult (TdApi.Chat chat) { protected void modifyFoundChat (TGFoundChat chat) { } + protected void modifyFoundChatView (ListItem item, int position, BetterChatView chatView) { } + protected boolean needChatSearchManagerPreparation () { // Disable if it's not needed return true; @@ -141,8 +145,11 @@ private void removeRecentChat (final TGFoundChat chat) { protected final CustomRecyclerView generateChatSearchView (@Nullable ViewGroup parent) { final boolean noChatSearch = (getChatSearchFlags() & SearchManager.FLAG_NO_CHATS) != 0; + chatSearchViewport = tdlib.messageViewer().createViewport(new TdApi.MessageSourceSearch(), this); + chatSearchViewport.addIgnoreLock(() -> !this.isSearchContentVisible); chatSearchView = (CustomRecyclerView) Views.inflate(context(), R.layout.recycler_custom, parent); Views.setScrollBarPosition(chatSearchView); + tdlib.ui().attachViewportToRecyclerView(chatSearchViewport, chatSearchView); chatSearchView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { @@ -399,6 +406,7 @@ protected void setChatData (ListItem item, int position, BetterChatView chatView } else if (itemId == R.id.search_message) { chatView.setMessage((TGFoundMessage) item.getData()); } + TelegramViewController.this.modifyFoundChatView(item, position, chatView); } @Override @@ -889,6 +897,7 @@ private void setSearchContentVisible (boolean isVisible) { if (this.isSearchContentVisible != isVisible) { this.isSearchContentVisible = isVisible; chatSearchView.setScrollDisabled(!isVisible); + chatSearchViewport.notifyLockValueChanged(); context().checkDisallowScreenshots(); } } @@ -1081,7 +1090,7 @@ public int getRootColorId () { @Override @CallSuper public boolean shouldDisallowScreenshots () { - return isSearchContentVisible && chatSearchDisallowScreenshots; + return (isSearchContentVisible && chatSearchDisallowScreenshots) || super.shouldDisallowScreenshots(); } protected final void invalidateChatSearchResults () { @@ -1130,5 +1139,8 @@ public void destroy () { TGLegacyManager.instance().removeEmojiListener(chatSearchAdapter); Views.destroyRecyclerView(chatSearchView); } + if (chatSearchViewport != null) { + chatSearchViewport.performDestroy(); + } } } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/ToggleHeaderView2.java b/app/src/main/java/org/thunderdog/challegram/navigation/ToggleHeaderView2.java index c0d0378ad5..d2d7588097 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/ToggleHeaderView2.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/ToggleHeaderView2.java @@ -13,6 +13,7 @@ import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import me.vkryl.android.AnimatorUtils; @@ -51,10 +52,10 @@ public void setSubtitle (String subtitle, boolean animated) { private void trimTexts () { int avail = getMeasuredWidth() - textPadding - Screen.dp(12f); - for (ListAnimator.Entry entry: titleR) { + for (ListAnimator.Entry entry : titleR) { entry.item.measure(avail, Paints.getMediumTextPaint(18f, Theme.headerTextColor(), false)); } - for (ListAnimator.Entry entry: subtitleR) { + for (ListAnimator.Entry entry : subtitleR) { entry.item.measure(avail, Paints.getRegularTextPaint(14f, Theme.getColor(ColorId.textLight))); } } @@ -67,7 +68,7 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { public float getTitleWidth () { float width = 0f; - for (ListAnimator.Entry entry: titleR) { + for (ListAnimator.Entry entry : titleR) { width += entry.item.getWidth() * entry.getVisibility(); } @@ -76,20 +77,20 @@ public float getTitleWidth () { @Override protected void onDraw (Canvas c) { - for (ListAnimator.Entry entry: titleR) { + for (ListAnimator.Entry entry : titleR) { final int offset2 = (int) ((!entry.isAffectingList() ? ((entry.getVisibility() - 1f) * Screen.dp(18)): ((1f - entry.getVisibility()) * Screen.dp(18)))); entry.item.draw(c, getPaddingLeft(), textTop + offset2, entry.getVisibility(), Paints.getMediumTextPaint(18f, Theme.getColor(ColorId.text), false)); } - for (ListAnimator.Entry entry: subtitleR) { + for (ListAnimator.Entry entry : subtitleR) { final int offset2 = (int) ((!entry.isAffectingList() ? ((entry.getVisibility() - 1f) * Screen.dp(14)): ((1f - entry.getVisibility()) * Screen.dp(14)))); entry.item.draw(c, getPaddingLeft(), textTop + Screen.dp(19) + offset2, entry.getVisibility(), Paints.getRegularTextPaint(14f, Theme.getColor(ColorId.textLight))); } - Drawables.draw(c, arrowDrawable, getTitleWidth() + Screen.dp(2), triangleTop, Paints.getPorterDuffPaint(Theme.getColor(ColorId.icon))); + Drawables.draw(c, arrowDrawable, getTitleWidth() + Screen.dp(2), triangleTop, PorterDuffPaint.get(ColorId.icon)); } @@ -115,7 +116,7 @@ public void measure (int width, TextPaint paint) { } public float getWidth () { - return textTrimmed != null ? textTrimmedWidth: textWidth; + return textTrimmed != null ? textTrimmedWidth : textWidth; } public void draw (Canvas canvas, int x, int y, float alpha, TextPaint paint) { diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/TooltipOverlayView.java b/app/src/main/java/org/thunderdog/challegram/navigation/TooltipOverlayView.java index da6f4866d1..1efd3f1904 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/TooltipOverlayView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/TooltipOverlayView.java @@ -51,6 +51,7 @@ import org.thunderdog.challegram.theme.ThemeDelegate; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.unsorted.Settings; @@ -269,7 +270,7 @@ public TooltipLanguageSelectorView (TooltipOverlayView parentView, TGMessage mes this.listener = listener; final String currentLang = message.getCurrentTranslatedLanguage(); originalLanguage = Lang.getLanguageName(message.getOriginalMessageLanguage(), Lang.getString(R.string.TranslateLangUnknown)); - translatedLanguage = Lang.getLanguageName(message.getCurrentTranslatedLanguage(), currentLang != null ? currentLang: originalLanguage); + translatedLanguage = Lang.getLanguageName(message.getCurrentTranslatedLanguage(), currentLang != null ? currentLang : originalLanguage); arrowX = (int) U.measureText(originalLanguage, Paints.getRegularTextPaint(14)); width = (int)(arrowX + U.measureText(translatedLanguage, Paints.getRegularTextPaint(14)) + Screen.dp(18)); arrow = Drawables.get(R.drawable.round_keyboard_arrow_right_16); @@ -312,7 +313,7 @@ public void requestIcons (ComplexReceiver iconReceiver) { public void draw (Canvas c, ColorProvider colorProvider, int left, int top, int right, int bottom, float alpha, ComplexReceiver iconReceiver) { c.drawText(originalLanguage, left, top + Screen.dp(14), Paints.getRegularTextPaint(14, Theme.getColor(ColorId.tooltip_text))); c.drawText(translatedLanguage, left + arrowX + Screen.dp(18), top + Screen.dp(14), Paints.getRegularTextPaint(14, Theme.getColor(ColorId.tooltip_textLink))); - Drawables.draw(c, arrow, left + arrowX + Screen.dp(1), top, Paints.getPorterDuffPaint(Theme.getColor(ColorId.tooltip_text))); + Drawables.draw(c, arrow, left + arrowX + Screen.dp(1), top, PorterDuffPaint.get(ColorId.tooltip_text)); } } @@ -1276,11 +1277,12 @@ public TooltipBuilder source (TdlibUi.UrlOpenParameters openParameters) { return this; } - public void show (ViewController controller, Tdlib tdlib, int iconRes, CharSequence text) { + public TooltipInfo show (ViewController controller, Tdlib tdlib, int iconRes, CharSequence text) { if (originalView == null && viewProvider == null && locationProvider == null) { UI.showToast(text, Toast.LENGTH_SHORT); + return null; } else { - icon(iconRes).needBlink(iconRes == R.drawable.baseline_info_24 || iconRes == R.drawable.baseline_error_24).controller(controller != null ? controller.getParentOrSelf() : null).show(tdlib, text).hideDelayed(3500, TimeUnit.MILLISECONDS); + return icon(iconRes).needBlink(iconRes == R.drawable.baseline_info_24 || iconRes == R.drawable.baseline_error_24).controller(controller != null ? controller.getParentOrSelf() : null).show(tdlib, text).hideDelayed(3500, TimeUnit.MILLISECONDS); } } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/ViewController.java b/app/src/main/java/org/thunderdog/challegram/navigation/ViewController.java index 3adf1531d4..ecb42afe3e 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/ViewController.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/ViewController.java @@ -68,6 +68,10 @@ import org.thunderdog.challegram.core.Background; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.mediaview.AvatarPickerMode; +import org.thunderdog.challegram.mediaview.MediaSelectDelegate; +import org.thunderdog.challegram.mediaview.MediaSendDelegate; +import org.thunderdog.challegram.mediaview.MediaViewDelegate; import org.thunderdog.challegram.support.RippleSupport; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.Tdlib; @@ -131,6 +135,7 @@ import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.collection.IntList; +import me.vkryl.core.lambda.CancellableRunnable; import me.vkryl.core.lambda.Destroyable; import me.vkryl.core.lambda.Future; import me.vkryl.core.lambda.FutureBool; @@ -344,7 +349,7 @@ public void onThemeAutoNightModeChanged (int autoNightMode) { } @CallSuper protected void handleLanguageDirectionChange () { if (searchHeaderView != null) - HeaderView.updateEditTextDirection(searchHeaderView, Screen.dp(68f), Screen.dp(49f)); + HeaderView.updateEditTextDirection(searchHeaderView.editView(), Screen.dp(68f), Screen.dp(49f)); if (counterHeaderView != null) HeaderView.updateLayoutMargins(counterHeaderView, Screen.dp(68f), 0); View headerCell = getCustomHeaderCell(); @@ -445,6 +450,17 @@ public final ViewController destroyStackItemAt (int index) { return navigationController != null ? navigationController.getStack().destroy(index) : null; } + public final ViewController destroyPreviousStackItem () { + if (navigationController != null) { + NavigationStack stack = navigationController.getStack(); + int currentIndex = stack.size() - 1; + if (currentIndex > 0) { + return stack.destroy(currentIndex - 1); + } + } + return null; + } + public final ViewController removeStackItemById (int id) { return navigationController != null ? navigationController.getStack().removeById(id) : null; } @@ -627,14 +643,14 @@ protected void onLeaveSearchMode () { protected void updateSearchMode (boolean inSearch, boolean needUpdateKeyboard) { if (inSearch) { cachedLockFocusView = lockFocusView; - lockFocusView = searchHeaderView; + lockFocusView = searchHeaderView.editView(); if (needUpdateKeyboard) { - Keyboard.show(searchHeaderView); + Keyboard.show(searchHeaderView.editView()); } } else { lockFocusView = cachedLockFocusView; if (needUpdateKeyboard) { - Keyboard.hide(searchHeaderView); + Keyboard.hide(searchHeaderView.editView()); } cachedLockFocusView = null; } @@ -744,7 +760,7 @@ protected final View getTransformHeaderView (HeaderView headerView) { return getCounterHeaderView(headerView); } if ((flags & FLAG_IN_SEARCH_MODE) != 0) { - return getSearchHeaderView(headerView); + return getSearchHeaderView(headerView).view(); } if ((flags & FLAG_IN_CUSTOM_MODE) != 0) { return getCustomModeHeaderView(headerView); @@ -1043,7 +1059,7 @@ protected final int getSelectedCount () { // Search mode header - private HeaderEditText searchHeaderView; + private SearchEditTextDelegate searchHeaderView; protected void modifySearchHeaderView (HeaderEditText headerEditText) { // called only once @@ -1053,11 +1069,24 @@ protected boolean useGraySearchHeader () { return false; } - protected final HeaderEditText genSearchHeader (HeaderView headerView) { - return useGraySearchHeader() ? headerView.genGreySearchHeader(this) : headerView.genSearchHeader(useLightSearchHeader(), this); + protected SearchEditTextDelegate genSearchHeader (HeaderView headerView) { + HeaderEditText view = useGraySearchHeader() ? headerView.genGreySearchHeader(this) : headerView.genSearchHeader(useLightSearchHeader(), this); + return new SearchEditTextDelegate() { + @NonNull + @Override + public View view () { + return view; + } + + @NonNull + @Override + public HeaderEditText editView () { + return view; + } + }; } - protected HeaderEditText getSearchHeaderView (HeaderView headerView) { + protected SearchEditTextDelegate getSearchHeaderView (HeaderView headerView) { if (searchHeaderView == null) { FrameLayoutFix.LayoutParams params; @@ -1072,7 +1101,7 @@ protected HeaderEditText getSearchHeaderView (HeaderView headerView) { } searchHeaderView = genSearchHeader(headerView); - searchHeaderView.addTextChangedListener(new TextWatcher() { + searchHeaderView.editView().addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged (CharSequence s, int start, int count, int after) { @@ -1095,10 +1124,10 @@ public void afterTextChanged (Editable s) { } }); - searchHeaderView.setHint(Lang.getString(bindLocaleChanger(getSearchHint(), searchHeaderView, true, false))); - searchHeaderView.setLayoutParams(params); + searchHeaderView.editView().setHint(Lang.getString(bindLocaleChanger(getSearchHint(), searchHeaderView.editView(), true, false))); + searchHeaderView.view().setLayoutParams(params); - modifySearchHeaderView(searchHeaderView); + modifySearchHeaderView(searchHeaderView.editView()); } return searchHeaderView; } @@ -1113,7 +1142,7 @@ public final String getLastSearchInput () { private String lastSearchInput = ""; - protected void clearSearchInput () { + public void clearSearchInput () { clearSearchInput("", false); } @@ -1122,9 +1151,9 @@ private void clearSearchInput (String text, boolean reset) { if (reset) { lastSearchInput = text; } - searchHeaderView.setText(text); + searchHeaderView.editView().setText(text); if (!text.isEmpty()) { - searchHeaderView.setSelection(text.length()); + searchHeaderView.editView().setSelection(text.length()); } updateClearSearchButton(!text.isEmpty(), false); } @@ -1752,6 +1781,44 @@ public void openMissingGoogleMapsAlert () { showAlert(b); } + private AlertDialog linkWarningDialog; + private List linkWarningCallbacks; + + public final void openSecretLinkPreviewAlert (@Nullable RunnableBool onAcceptWarning) { + if (onAcceptWarning != null) { + if (this.linkWarningCallbacks == null) { + this.linkWarningCallbacks = new ArrayList<>(); + } + this.linkWarningCallbacks.add(onAcceptWarning); + } + if (linkWarningDialog != null && linkWarningDialog.isShowing()) { + return; + } + RunnableBool after = isAccepted -> { + linkWarningDialog = null; + Settings.instance().markTutorialAsComplete(Settings.TUTORIAL_SECRET_LINK_PREVIEWS); + Settings.instance().setUseSecretLinkPreviews(isAccepted); + List callbacks = linkWarningCallbacks; + this.linkWarningCallbacks = null; + if (callbacks != null) { + for (RunnableBool callback : callbacks) { + callback.runWithBool(isAccepted); + } + } + }; + AlertDialog.Builder b = new AlertDialog.Builder(context(), Theme.dialogTheme()); + b.setTitle(Lang.getString(R.string.AppName)); + b.setMessage(Lang.getString(R.string.SecretLinkPreviewAlert)); + b.setPositiveButton(Lang.getString(R.string.SecretLinkPreviewEnable), (dialog, which) -> + after.runWithBool(true) + ); + b.setNegativeButton(Lang.getString(R.string.SecretLinkPreviewDisable), (dialog, which) -> + after.runWithBool(false) + ); + b.setCancelable(false); + linkWarningDialog = showAlert(b); + } + public void openLinkAlert (final String url, @Nullable TdlibUi.UrlOpenParameters options) { tdlib.ui().openUrl(this, url, options == null ? new TdlibUi.UrlOpenParameters().requireOpenPrompt() : options.requireOpenPrompt()); } @@ -2213,6 +2280,8 @@ public final PopupLayout showWarning (CharSequence info, RunnableBool callback) } public static class OptionItem { + public static final OptionItem SEPARATOR = new OptionItem(0, null, 0, 0); + public final int id; public final CharSequence name; public final int color; @@ -2297,6 +2366,10 @@ public Builder cancelItem () { return item(new OptionItem.Builder().id(R.id.btn_cancel).name(R.string.Cancel).icon(R.drawable.baseline_cancel_24).build()); } + public int itemCount () { + return items.size(); + } + public Options build () { return new Options(info, items.toArray(new OptionItem[0])); } @@ -2368,8 +2441,24 @@ public final PopupLayout showOptions (Options options, final OptionDelegate dele } }; } + int totalHeight = shadowView.getLayoutParams().height + optionsWrap.getTextHeight() + popupAdditionalHeight; int index = 0; for (OptionItem item : options.items) { + if (item == OptionItem.SEPARATOR) { + ShadowView shadowViewBottom = new ShadowView(context); + shadowViewBottom.setSimpleBottomTransparentShadow(false); + ViewSupport.setThemedBackground(shadowViewBottom, ColorId.background, this); + addThemeInvalidateListener(shadowViewBottom); + optionsWrap.addView(shadowViewBottom, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(6f))); + + ShadowView shadowViewTop = new ShadowView(context); + shadowViewTop.setSimpleTopShadow(true, this); + addThemeInvalidateListener(shadowViewTop); + optionsWrap.addView(shadowViewTop, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(6f))); + index++; + totalHeight += shadowViewBottom.getLayoutParams().height + shadowViewTop.getLayoutParams().height; + continue; + } TextView text = OptionsLayout.genOptionView(context, item.id, item.name, item.color, item.icon, onClickListener, getThemeListeners(), forcedTheme); RippleSupport.setTransparentSelector(text); if (forcedTheme != null) @@ -2380,12 +2469,14 @@ public final PopupLayout showOptions (Options options, final OptionDelegate dele } optionsWrap.addView(text); index++; + totalHeight += text.getLayoutParams().height; } // Window - popupLayout.showSimplePopupView(optionsWrap, shadowView.getLayoutParams().height + Screen.dp(54f) * options.items.length + optionsWrap.getTextHeight() + popupAdditionalHeight); + popupLayout.showSimplePopupView(optionsWrap, totalHeight); onCreatePopupLayout(popupLayout); + return popupLayout; } @@ -2679,12 +2770,44 @@ public final void openTdlibLogs (int testerLevel, Crash crashInfo) { showWarning(Lang.getMarkdownString(this, R.string.TdlibLogsWarning), proceed -> { if (proceed) { SettingsBugController c = new SettingsBugController(context, tdlib); - c.setArguments(new SettingsBugController.Args(SettingsBugController.SECTION_TDLIB, crashInfo).setTesterLevel(testerLevel)); + c.setArguments(new SettingsBugController.Args(SettingsBugController.Section.TDLIB, crashInfo).setTesterLevel(testerLevel)); navigateTo(c); } }); } + public final void openExperimentalSettings (int testerLevel) { + showWarning(Lang.getMarkdownStringSecure(this, R.string.ExperimentalSettingsWarning), proceed -> { + if (proceed) { + SettingsBugController c = new SettingsBugController(context, tdlib); + c.setArguments(new SettingsBugController.Args(SettingsBugController.Section.EXPERIMENTS).setTesterLevel(testerLevel)); + navigateTo(c); + } + }); + } + + private CancellableRunnable pendingActivityRestart; + + public final void cancelPendingActivityRestart () { + if (pendingActivityRestart != null) { + pendingActivityRestart.cancel(); + pendingActivityRestart = null; + } + } + + public final void scheduleActivityRestart () { + cancelPendingActivityRestart(); + pendingActivityRestart = new CancellableRunnable() { + @Override + public void act () { + pendingActivityRestart = null; + context.recreate(); + } + }; + pendingActivityRestart.removeOnCancel(UI.getAppHandler()); + UI.getAppHandler().postDelayed(pendingActivityRestart, 300L); + } + public final boolean isSameTdlib (@NonNull Tdlib tdlib) { return tdlibId() == tdlib.id(); // && this.tdlib.isDebug() == tdlib.isDebug(); } @@ -3059,7 +3182,7 @@ public void onBlur () { @CallSuper public void hideSoftwareKeyboard () { if (inSearchMode()) { - Keyboard.hide(searchHeaderView); + Keyboard.hide(searchHeaderView.editView()); } if (lockFocusView != null) { Keyboard.hide(lockFocusView); @@ -3224,6 +3347,11 @@ public static class CameraOpenOptions { public CameraController.QrCodeListener qrCodeListener; public @StringRes int qrModeSubtitle; public boolean qrModeDebug; + public @AvatarPickerMode int avatarPickerMode; + + public MediaViewDelegate delegate; + public MediaSelectDelegate selectDelegate; + public MediaSendDelegate sendDelegate; public CameraOpenOptions anchor (View anchorView) { this.anchorView = anchorView; @@ -3265,6 +3393,18 @@ public CameraOpenOptions allowSystem (boolean allowSystem) { return this; } + public CameraOpenOptions setAvatarPickerMode (@AvatarPickerMode int avatarPickerMode) { + this.avatarPickerMode = avatarPickerMode; + return this; + } + + public CameraOpenOptions setMediaEditorDelegates (MediaViewDelegate delegate, MediaSelectDelegate selectDelegate, MediaSendDelegate sendDelegate) { + this.delegate = delegate; + this.selectDelegate = selectDelegate; + this.sendDelegate = sendDelegate; + return this; + } + public CameraOpenOptions optionalMicrophone (boolean optionalMicrophone) { this.optionalMicrophone = optionalMicrophone; return this; @@ -3387,7 +3527,23 @@ public final void setBoundForceTouchView (@NonNull ForceTouchView forceTouchView // Disabling screenshot + private List disallowScreenshotReasons; + + public void addDisallowScreenshotReason (FutureBool reason) { + if (disallowScreenshotReasons == null) { + disallowScreenshotReasons = new ArrayList<>(); + } + disallowScreenshotReasons.add(reason); + } + public boolean shouldDisallowScreenshots () { + if (disallowScreenshotReasons != null) { + for (FutureBool reason : disallowScreenshotReasons) { + if (reason.getBoolValue()) { + return true; + } + } + } return false; } @@ -3411,4 +3567,11 @@ public View getThumbnailWrap () { public final void forceFastAnimationOnce () { forceFadeModeOnce = true; } + + + public interface SearchEditTextDelegate { + @NonNull View view(); + @NonNull HeaderEditText editView(); + } + } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerController.java b/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerController.java index 9109d388e7..a97c52ab33 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerController.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerController.java @@ -23,30 +23,38 @@ import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.IdRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SparseArrayCompat; +import androidx.core.util.ObjectsCompat; import androidx.viewpager.widget.PagerAdapter; import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.telegram.Tdlib; -import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.unsorted.Size; import org.thunderdog.challegram.util.OptionDelegate; -import org.thunderdog.challegram.widget.rtl.RtlViewPager; import org.thunderdog.challegram.widget.ViewPager; +import org.thunderdog.challegram.widget.rtl.RtlViewPager; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.collection.LongSparseIntArray; public abstract class ViewPagerController extends TelegramViewController implements ViewPager.OnPageChangeListener, ViewPagerTopView.OnItemClickListener, OptionDelegate, SelectDelegate, Menu, MoreDelegate { @@ -54,6 +62,8 @@ public ViewPagerController (Context context, Tdlib tdlib) { super(context, tdlib); } + protected static final int NO_POSITION = -1; + public interface ScrollToTopDelegate { void onScrollToTopRequested (); } @@ -71,14 +81,36 @@ protected int getMenuButtonsWidth () { return 0; // override for performance } + /*@Override + public boolean onBackPressed (boolean fromTop) { + int currentPosition = getCurrentPagerItemPosition(); + ViewController currentController = getCachedControllerForPosition(currentPosition); + if (currentController != null && currentController.onBackPressed(fromTop)) { + return true; + } + SparseArrayCompat> controllers = getAllCachedControllers(); + if (controllers != null) { + for (int i = 0; i < controllers.size(); i++) { + int position = controllers.keyAt(i); + if (position != currentPosition) { + ViewController controller = controllers.valueAt(position); + if (controller != null && controller.onBackPressed(fromTop)) { + return true; + } + } + } + } + return super.onBackPressed(fromTop); + }*/ + @Override protected void handleLanguageDirectionChange () { super.handleLanguageDirectionChange(); if (pager != null) { pager.checkRtl(); } - if (getTitleStyle() == TITLE_STYLE_COMPACT_BIG) { - TextView textView = (TextView) ((ViewGroup) headerCell).getChildAt(((ViewGroup) headerCell).getChildCount() - 1); + if (getTitleStyle() == TITLE_STYLE_COMPACT_BIG && headerCell != null) { + TextView textView = headerCell.getView().findViewById(R.id.text_title); if (Views.setGravity(textView, Gravity.TOP | (Lang.rtl() ? Gravity.RIGHT : Gravity.LEFT))) { FrameLayout.LayoutParams params = ((FrameLayout.LayoutParams) textView.getLayoutParams()); if (Lang.rtl()) { @@ -112,11 +144,13 @@ public void handleLanguagePackEvent (int event, int arg1) { private void updateHeader () { if (headerCell != null) { - String[] sections = getPagerSections(); - if (sections != null && sections.length != getPagerItemCount()) { - throw new IllegalArgumentException("sections.length != " + getPagerItemCount()); + List sections = getPagerSectionItems(); + if (sections != null) { + if (sections.size() != getPagerItemCount()) { + throw new IllegalArgumentException("sections.size() != " + getPagerItemCount()); + } + headerCell.getTopView().setItems(sections); } - headerCell.getTopView().setItems(sections); } } @@ -135,12 +169,11 @@ protected void onDraw (Canvas c) { }; contentView.setWillNotDraw(false); - String[] sections = getPagerSections(); - if (sections != null && sections.length != getPagerItemCount()) { - throw new IllegalArgumentException("sections.length != " + getPagerItemCount()); - } - + List sections = getPagerSectionItems(); if (sections != null) { + if (sections.size() != getPagerItemCount()) { + throw new IllegalArgumentException("sections.size() != " + getPagerItemCount()); + } switch (getTitleStyle()) { case TITLE_STYLE_BIG: { headerCell = new ViewPagerHeaderView(context); @@ -150,7 +183,6 @@ protected void onDraw (Canvas c) { case TITLE_STYLE_COMPACT: case TITLE_STYLE_COMPACT_BIG: { headerCell = new ViewPagerHeaderViewCompact(context); - addThemeInvalidateListener(headerCell.getTopView()); FrameLayoutFix.LayoutParams params = (FrameLayoutFix.LayoutParams) ((ViewPagerHeaderViewCompact) headerCell).getRecyclerView().getLayoutParams(); if (getBackButton() != BackHeaderButton.TYPE_NONE && getMenuButtonsWidth() != 0) { if (Lang.rtl()) { @@ -166,7 +198,7 @@ protected void onDraw (Canvas c) { params.gravity = Gravity.CENTER_HORIZONTAL; } if (getTitleStyle() == TITLE_STYLE_COMPACT_BIG) { - headerCell.getTopView().setTextPadding(Screen.dp(12f)); + headerCell.getTopView().setItemPadding(Screen.dp(12f)); TextView title = SimpleHeaderView.newTitle(context); title.setTextColor(Theme.headerTextColor()); addThemeTextColorListener(title, ColorId.headerText); @@ -194,14 +226,20 @@ protected void onDraw (Canvas c) { } adapter = new ViewPagerAdapter(context, this); + addAttachStateListener(attachListener); + addFocusListener(focusListener); pager = new RtlViewPager(context); pager.setLayoutParams(params); pager.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? View.OVER_SCROLL_IF_CONTENT_SCROLLS : View.OVER_SCROLL_NEVER); pager.addOnPageChangeListener(new androidx.viewpager.widget.ViewPager.OnPageChangeListener() { @Override public void onPageScrolled (int position, float positionOffset, int positionOffsetPixels) { + boolean needUpdateAttachState = currentPosition != position || (currentPositionOffset == 0f) != (positionOffset == 0f); currentPosition = position; currentPositionOffset = positionOffset; + if (needUpdateAttachState) { + updateAttachedControllers(); + } context().checkDisallowScreenshots(); } @@ -227,6 +265,49 @@ public void onPageScrollStateChanged (int state) { } return contentView; } + private void updateControllerState (ViewController c, int position) { + boolean attachState = c.getAttachState(); + boolean desiredAttachState = getAttachState() && adapter.attachedControllers.contains(c); + + if (desiredAttachState) { + if (currentPositionOffset == 0f || Math.abs(currentPositionOffset) == 1f) { + desiredAttachState = (int) (currentPosition + currentPositionOffset) == position; + } else { + desiredAttachState = position == currentPosition || position == currentPosition + Math.signum(currentPositionOffset); + } + } + + boolean isFocused = c.isFocused(); + boolean desiredFocusState = desiredAttachState && isFocused(); + if (desiredFocusState) { + desiredFocusState = currentPositionOffset == 0f && position == currentPosition; + } + + if (isFocused != desiredFocusState && !desiredFocusState) { + c.onBlur(); + } + if (attachState != desiredAttachState) { + c.onAttachStateChanged(navigationController, desiredAttachState); + } + if (isFocused != desiredFocusState && desiredFocusState) { + c.onFocus(); + } + } + + private final ViewController.AttachListener attachListener = (context, navigation, isAttached) -> { + updateAttachedControllers(); + }; + + private final ViewController.FocusStateListener focusListener = (c, isFocused) -> { + updateAttachedControllers(); + }; + + private void updateAttachedControllers () { + for (ViewController c : adapter.attachedControllers) { + updateControllerState(c, adapter.getControllerPosition(c)); + } + } + protected boolean overridePagerParent () { return false; } @@ -245,7 +326,7 @@ public View getViewForApplyingOffsets () { public final void changeName (CharSequence newName) { if (getTitleStyle() == TITLE_STYLE_COMPACT_BIG && headerCell != null) { - TextView view = ((View) headerCell).findViewById(R.id.text_title); + TextView view = headerCell.getView().findViewById(R.id.text_title); if (view != null) { Views.setMediumText(view, newName); } @@ -310,7 +391,7 @@ public void onMenuItemPressed (int id, View view) { @Override public View getCustomHeaderCell () { - return (View) headerCell; + return headerCell != null ? headerCell.getView() : null; } @Override @@ -365,13 +446,23 @@ public final void onPageSelected (int position) { onPageSelected(adapter.reversePosition(position), position); } + private boolean disallowKeyboardHideOnPageScrolled; + + public void setDisallowKeyboardHideOnPageScrolled (boolean disallowKeyboardHideOnPageScrolled) { + this.disallowKeyboardHideOnPageScrolled = disallowKeyboardHideOnPageScrolled; + } + + public boolean isDisallowKeyboardHideOnPageScrolled () { + return disallowKeyboardHideOnPageScrolled; + } + @Override public void onPageScrolled (int position, float positionOffset, int positionOffsetPixels) { if (headerCell != null) { headerCell.getTopView().setSelectionFactor((float) position + positionOffset); } onPageScrolled(adapter.reversePosition(position), position, positionOffset, positionOffsetPixels); - if (getKeyboardState()) { + if (getKeyboardState() && !disallowKeyboardHideOnPageScrolled) { hideSoftwareKeyboard(); } } @@ -384,15 +475,20 @@ public final boolean isAtFirstPosition () { return adapter.reversePosition(pager.getCurrentItem()) == 0; } - protected final void replaceController (int position, ViewController newController) { - ViewController currentController = adapter.getCachedItemByPosition(position); - if (currentController != null) { - adapter.cachedItems.remove(position); - currentController.destroy(); + protected final void replaceController (long itemId, ViewController newController) { + int position = getPagerItemPosition(itemId); + if (position != NO_POSITION) { + ViewController currentController = adapter.getCachedItemByPosition(position); + if (currentController != null) { + currentController.destroy(); + } newController.setParentWrapper(this); newController.bindThemeListeners(this); adapter.cachedItems.put(position, newController); + adapter.cachedPositions.put(itemId, position); adapter.notifyDataSetChanged(); + } else { + newController.destroy(); } } @@ -408,6 +504,55 @@ public final int getCurrentPagerItemPosition() { return adapter.reversePosition(pager.getCurrentItem()); } + public final long getCurrentPagerItemId () { + return getPagerItemId(getCurrentPagerItemPosition()); + } + + public final void notifyPagerItemPositionsChanged () { + if (headerCell != null) { + updateHeader(); + } + if (adapter != null) { + int itemCount = getPagerItemCount(); + SparseArrayCompat> cachedItems = new SparseArrayCompat<>(itemCount); + LongSparseIntArray cachedPositions = new LongSparseIntArray(itemCount); + for (int position = 0; position < itemCount; position++) { + long itemId = getPagerItemId(position); + int oldPosition = adapter.getCachedItemPosition(itemId); + if (oldPosition != NO_POSITION) { + ViewController cachedItem = adapter.getCachedItemByPosition(oldPosition); + cachedItems.put(position, cachedItem); + cachedPositions.put(itemId, position); + adapter.cachedItems.remove(oldPosition); + adapter.cachedPositions.delete(itemId); + } + } + if (inTransformMode() && headerView != null) { + int currentPosition = adapter.reversePosition(pager.getCurrentItem()); + for (int index = 0; index < adapter.cachedItems.size(); index++) { + int position = adapter.cachedItems.keyAt(index); + if (currentPosition == position) { + if (headerView.inSelectMode()) { + headerView.finishSelectMode(); + } else if (headerView.inSearchMode()) { + headerView.closeSearchMode(true, /* after */ null); + } else if (headerView.inCustomMode()) { + headerView.closeCustomMode(); + } + break; + } + } + } + adapter.destroyCachedItems(); + adapter.cachedItems = cachedItems; + adapter.cachedPositions = cachedPositions; + adapter.notifyDataSetChanged(); + if (headerCell != null) { + headerCell.getTopView().setSelectionFactor(pager.getCurrentItem()); + } + } + } + public final ViewController getCurrentPagerItem () { return getCachedControllerForPosition(getCurrentPagerItemPosition()); } @@ -435,7 +580,7 @@ public boolean shouldDisallowScreenshots () { return true; } } - return false; + return super.shouldDisallowScreenshots(); } protected void setCurrentPagerPosition (int position, boolean animated) { @@ -451,8 +596,28 @@ public boolean onOptionItemPressed (View optionItemView, int id) { return c instanceof OptionDelegate && ((OptionDelegate) c).onOptionItemPressed(optionItemView, id); } - public final @Nullable ViewController getCachedControllerForId (int id) { - return adapter != null ? adapter.getCachedItemById(id) : null; + public int getPagerItemPosition (long itemId) { + if (adapter != null) { + int cachedItemPosition = adapter.getCachedItemPosition(itemId); + if (cachedItemPosition != NO_POSITION) { + return cachedItemPosition; + } + } + int pagerItemCount = getPagerItemCount(); + for (int position = 0; position < pagerItemCount; position++) { + if (getPagerItemId(position) == itemId) { + return position; + } + } + return NO_POSITION; + } + + public final @Nullable ViewController getCachedControllerForId (@IdRes int id) { + return adapter != null ? adapter.getCachedItemByControllerId(id) : null; + } + + public final @Nullable ViewController getCachedControllerForItemId (long itemId) { + return adapter != null ? adapter.getCachedItemByItemId(itemId) : null; } public final @Nullable ViewController getCachedControllerForPosition (int position) { @@ -487,10 +652,42 @@ public final void prepareControllerForPosition (int position, @Nullable Runnable } } + private @Nullable List cachedPagerSectionItems; + protected abstract int getPagerItemCount (); + protected long getPagerItemId (int position) { + return position; + } protected abstract void onCreateView (Context context, FrameLayoutFix contentView, ViewPager pager); protected abstract ViewController onCreatePagerItemForPosition (Context context, int position); - protected abstract String[] getPagerSections (); + protected @Nullable abstract String[] getPagerSections (); + protected @Nullable List getPagerSectionItems () { + String[] pagerSections = getPagerSections(); + if (pagerSections == null) { + return cachedPagerSectionItems = null; + } + if (pagerSections.length == 0) { + return cachedPagerSectionItems = Collections.emptyList(); + } + if (cachedPagerSectionItems != null && cachedPagerSectionItems.size() == pagerSections.length) { + boolean hasChanges = false; + for (int i = 0; i < pagerSections.length; i++) { + if (!ObjectsCompat.equals(pagerSections[i], cachedPagerSectionItems.get(i).string)) { + hasChanges = true; + break; + } + } + if (!hasChanges) { + return cachedPagerSectionItems; + } + } + List pagerSectionItems = new ArrayList<>(pagerSections.length); + for (String pagerSection : pagerSections) { + pagerSectionItems.add(new ViewPagerTopView.Item(pagerSection)); + } + cachedPagerSectionItems = pagerSectionItems; + return pagerSectionItems; + } @Override public void onPagerItemClick (int index) { @@ -520,19 +717,30 @@ public boolean canSlideBackFrom (NavigationController navigationController, floa public static class ViewPagerAdapter extends PagerAdapter { private final Context context; private final ViewPagerController parent; - private final SparseArrayCompat> cachedItems; + private /*final*/ SparseArrayCompat> cachedItems; + private /*final*/ LongSparseIntArray cachedPositions; public ViewPagerAdapter (Context context, ViewPagerController parent) { this.context = context; this.parent = parent; - this.cachedItems = new SparseArrayCompat<>(parent.getPagerItemCount()); + int itemCount = parent.getPagerItemCount(); + this.cachedItems = new SparseArrayCompat<>(itemCount); + this.cachedPositions = new LongSparseIntArray(itemCount); + } + + public int getCachedItemPosition (long itemId) { + return cachedPositions.get(itemId, NO_POSITION); } public @Nullable ViewController getCachedItemByPosition (int position) { - return cachedItems.get(position); + return position != NO_POSITION ? cachedItems.get(position) : null; + } + + public @Nullable ViewController getCachedItemByItemId (long itemId) { + return getCachedItemByPosition(getCachedItemPosition(itemId)); } - public @Nullable ViewController getCachedItemById (int id) { + public @Nullable ViewController getCachedItemByControllerId (@IdRes int id) { int size = cachedItems.size(); for (int i = 0; i < size; i++) { ViewController c = cachedItems.valueAt(i); @@ -548,9 +756,14 @@ public int getCount () { return parent.getPagerItemCount(); } + private final Set> attachedControllers = new HashSet<>(); + @Override public void destroyItem (ViewGroup container, int position, @NonNull Object object) { - container.removeView(((ViewController) object).getValue()); + ViewController c = (ViewController) object; + container.removeView(c.getValue()); + attachedControllers.remove(c); + parent.updateControllerState(c, position); } public void destroyCachedItems () { @@ -562,6 +775,7 @@ public void destroyCachedItems () { } } cachedItems.clear(); + cachedPositions.clear(); } private int reversePosition (int position) { @@ -570,9 +784,17 @@ private int reversePosition (int position) { @Override public int getItemPosition (@NonNull Object object) { + if (object instanceof ViewController) { + return getControllerPosition((ViewController) object); + } else { + return POSITION_NONE; + } + } + + public int getControllerPosition (ViewController controller) { int count = cachedItems.size(); for (int i = 0; i < count; i++) { - if (cachedItems.valueAt(i) == object) { + if (cachedItems.valueAt(i) == controller) { return reversePosition(cachedItems.keyAt(i)); } } @@ -586,6 +808,7 @@ public ViewController prepareViewController (int position) { c.setParentWrapper(parent); c.bindThemeListeners(parent); cachedItems.put(position, c); + cachedPositions.put(parent.getPagerItemId(position), position); } return c; } @@ -595,6 +818,8 @@ public ViewController prepareViewController (int position) { public Object instantiateItem (@NonNull ViewGroup container, int position) { ViewController c = prepareViewController(reversePosition(position)); container.addView(c.getValue()); + attachedControllers.add(c); + parent.updateControllerState(c, position); if ((position == parent.currentPosition || (parent.currentPositionOffset != 0f && position == parent.currentPosition + (parent.currentPositionOffset > 0f ? 1 : -1))) && c.shouldDisallowScreenshots()) { parent.context().checkDisallowScreenshots(); } diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderView.java b/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderView.java index 4986a58db1..3b90143ffc 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderView.java @@ -16,9 +16,9 @@ import android.content.Context; import android.view.Gravity; +import android.view.View; import android.view.ViewGroup; -import org.thunderdog.challegram.R; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.unsorted.Size; @@ -46,6 +46,11 @@ public void checkRtl () { topView.checkRtl(); } + @Override + public View getView () { + return this; + } + @Override public ViewPagerTopView getTopView () { return topView; diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderViewCompact.java b/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderViewCompact.java index 1ef02c0035..21fb6973fb 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderViewCompact.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderViewCompact.java @@ -15,11 +15,20 @@ package org.thunderdog.challegram.navigation; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Shader; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.Dimension; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -99,6 +108,8 @@ public int getItemCount () { private final A adapter; private final RecyclerView recyclerView; + private @Dimension(unit = Dimension.DP) float fadingEdgeLength; + public ViewPagerHeaderViewCompact (Context context) { super(context); @@ -110,7 +121,58 @@ public ViewPagerHeaderViewCompact (Context context) { adapter = new A(topView); - recyclerView = new RecyclerView(context); + recyclerView = new RecyclerView(context) { + private final Paint paint = new Paint(); + private final Matrix matrix = new Matrix(); + + { + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + paint.setShader(new LinearGradient(0, 0, 1, 0, Color.BLACK, 0, Shader.TileMode.CLAMP)); + } + + @Override + public void draw (Canvas c) { + int length = Screen.dp(fadingEdgeLength); + if (getChildCount() == 0 || length <= 1 /* px */) { + super.draw(c); + return; + } + View child = getChildAt(0); + int leftSpan = -child.getLeft(); + float leftFadeStrength = leftSpan < length ? Math.max(0f, leftSpan / (float) length) : 1f; + float leftLength = leftFadeStrength * length; + boolean drawLeft = leftLength > 1f /* px */; + + int rightSpan = child.getRight() - getWidth(); + float rightFadeStrength = rightSpan < length ? Math.max(0f, rightSpan / (float) length) : 1f; + float rightLength = rightFadeStrength * length; + boolean drawRight = rightLength > 1f /* px */; + + if (!drawLeft && !drawRight) { + super.draw(c); + return; + } + + int selectionHeight = Screen.dp(ViewPagerTopView.SELECTION_HEIGHT); + int top = topView.isDrawSelectionAtTop() ? selectionHeight : 0; + int bottom = getHeight() - (topView.isDrawSelectionAtTop() ? 0 : selectionHeight); + int saveCount = c.saveLayerAlpha(0, 0, getWidth(), getHeight(), 0xFF, Canvas.ALL_SAVE_FLAG); + super.draw(c); + if (drawLeft) { + matrix.setScale(leftLength, 1f); + paint.getShader().setLocalMatrix(matrix); + c.drawRect(0, top, length, bottom, paint); + } + if (drawRight) { + matrix.setScale(rightLength, 1f); + matrix.postRotate(180); + matrix.postTranslate(getWidth(), 0); + paint.getShader().setLocalMatrix(matrix); + c.drawRect(getWidth() - length, top, getWidth(), bottom, paint); + } + c.restoreToCount(saveCount); + } + }; recyclerView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, Size.getHeaderPortraitSize(), Gravity.TOP)); recyclerView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? OVER_SCROLL_IF_CONTENT_SCROLLS :OVER_SCROLL_NEVER); recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, Lang.rtl())); @@ -120,6 +182,13 @@ public ViewPagerHeaderViewCompact (Context context) { setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, Size.getHeaderBigPortraitSize(true))); } + public void setFadingEdgeLength (@Dimension(unit = Dimension.DP) float length) { + if (fadingEdgeLength != length) { + fadingEdgeLength = length; + recyclerView.invalidate(); + } + } + @Override public void onSelectionChanged (int selectionLeft, int selectionWidth, int firstItemWidth, int lastItemWidth, float totalFactor, boolean animated) { View view = recyclerView.getLayoutManager().findViewByPosition(0); @@ -128,7 +197,9 @@ public void onSelectionChanged (int selectionLeft, int selectionWidth, int first } final int viewWidth = view.getMeasuredWidth(); final int parentWidth = recyclerView.getMeasuredWidth(); - if (viewWidth <= parentWidth) { + final int parentPaddingLeft = recyclerView.getPaddingLeft(); + final int parentPaddingRight = recyclerView.getPaddingRight(); + if (viewWidth <= parentWidth - parentPaddingLeft - parentPaddingRight) { return; } if (recyclerView.isComputingLayout()) { @@ -145,7 +216,7 @@ public void onSelectionChanged (int selectionLeft, int selectionWidth, int first int viewX = -scrolledX; if ((getParent() != null && ((View) getParent()).getMeasuredWidth() > getMeasuredWidth()) || (viewWidth - parentWidth) < lastItemWidth / 2) { - int desiredViewLeft = (int) ((float) -(viewWidth - parentWidth) * totalFactor); + int desiredViewLeft = (int) (parentPaddingLeft * (1f - totalFactor) - (viewWidth - parentWidth + parentPaddingRight) * totalFactor); if (viewX != desiredViewLeft) { recyclerView.stopScroll(); int diff = (desiredViewLeft - viewX) * (Lang.rtl() ? 1 : -1); @@ -157,13 +228,18 @@ public void onSelectionChanged (int selectionLeft, int selectionWidth, int first } } else { int visibleSelectionX = selectionLeft + viewX; - int desiredSelectionX = (int) ((float) Screen.dp(16f) * (selectionLeft >= selectionWidth ? 1f : (float) selectionLeft / (float) selectionWidth)); + int desiredSelectionX; + if (parentPaddingLeft > 0) { + desiredSelectionX = parentPaddingLeft; + } else { + desiredSelectionX = (int) ((float) Screen.dp(16f) * (selectionLeft >= selectionWidth ? 1f : (float) selectionLeft / (float) selectionWidth)); + } if (visibleSelectionX != desiredSelectionX) { int newViewX = viewX + (desiredSelectionX - visibleSelectionX); - int maxX = parentWidth - viewWidth; - if (newViewX < maxX) { - newViewX = maxX; + int minX = parentWidth - parentPaddingRight - viewWidth; + if (newViewX < minX) { + newViewX = minX; } if (newViewX != viewX) { recyclerView.stopScroll(); @@ -207,8 +283,9 @@ public boolean canScrollLeft () { if (i != 0) { return true; } + int maxLeft = recyclerView.getClipToPadding() ? 0 : recyclerView.getPaddingLeft(); View view = recyclerView.getLayoutManager().findViewByPosition(0); - return view == null || view.getLeft() < 0; + return view == null || view.getLeft() < maxLeft; } public boolean canScrollInAnyDirection () { @@ -216,8 +293,10 @@ public boolean canScrollInAnyDirection () { if (i != 0) { return i != RecyclerView.NO_POSITION; } + int maxLeft = recyclerView.getClipToPadding() ? 0 : recyclerView.getPaddingLeft(); + int minRight = recyclerView.getMeasuredWidth() - (recyclerView.getClipToPadding() ? 0 : recyclerView.getPaddingRight()); View view = recyclerView.getLayoutManager().findViewByPosition(0); - return view == null || view.getLeft() < 0 || view.getRight() > recyclerView.getMeasuredWidth(); + return view == null || view.getLeft() < maxLeft || view.getRight() > minRight; } public RecyclerView getRecyclerView () { @@ -229,7 +308,7 @@ public boolean onTouchEvent (MotionEvent e) { return !(e.getAction() == MotionEvent.ACTION_DOWN && !canTouchAt(e.getX(), e.getY())) && super.onTouchEvent(e); } - private boolean canTouchAt (float x, float y) { + protected boolean canTouchAt (float x, float y) { y -= recyclerView.getTop() + (int) recyclerView.getTranslationY(); return y >= 0 && y < adapter.topView.getMeasuredHeight(); } @@ -239,6 +318,11 @@ public boolean onInterceptTouchEvent (MotionEvent e) { return (e.getAction() == MotionEvent.ACTION_DOWN && !canTouchAt(e.getX(), e.getY())) || super.onInterceptTouchEvent(e); } + @Override + public View getView () { + return this; + } + @Override public ViewPagerTopView getTopView () { return adapter.topView; diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderViewReactionsCompact.java b/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderViewReactionsCompact.java index 36e90e56e4..43e421aedc 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderViewReactionsCompact.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerHeaderViewReactionsCompact.java @@ -14,6 +14,7 @@ */ package org.thunderdog.challegram.navigation; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.LinearGradient; @@ -23,7 +24,9 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -31,20 +34,22 @@ import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; -import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.support.RippleSupport; -import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ThemeInvalidateListener; +import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.ui.MessageOptionsPagerController; import org.thunderdog.challegram.unsorted.Size; import org.thunderdog.challegram.widget.ReactionsSelectorRecyclerView; import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.ColorUtils; import me.vkryl.core.MathUtils; +@SuppressLint("ViewConstructor") public class ViewPagerHeaderViewReactionsCompact extends FrameLayoutFix implements PagerHeaderView, StretchyHeaderView, ViewPagerTopView.SelectionChangeListener, ThemeInvalidateListener { private static class VH extends RecyclerView.ViewHolder { public VH (View itemView) { @@ -66,7 +71,8 @@ public A (ViewPagerTopView topView) { } @Override - public VH onCreateViewHolder (ViewGroup parent, int viewType) { + @NonNull + public VH onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { if (topView.getParent() != null) { Log.w("ViewPagerHeaderViewCompact: topView is already attached to another cel"); ((ViewGroup) topView.getParent()).removeView(topView); @@ -75,7 +81,7 @@ public VH onCreateViewHolder (ViewGroup parent, int viewType) { } @Override - public void onBindViewHolder (VH holder, int position) { + public void onBindViewHolder (@NonNull VH holder, int position) { } @Override @@ -89,17 +95,24 @@ public int getItemCount () { @Nullable private final ReactionsSelectorRecyclerView reactionsSelectorRecyclerView; private final BackHeaderButton backButton; + private final @Nullable ImageView moreButton; private boolean isScrollEnabled = true; - private int rightOffset; + + private final MessageOptionsPagerController.State state; + private final boolean needReactionSelector; private final boolean needShowReactions, needShowViews; - public ViewPagerHeaderViewReactionsCompact (Context context, Tdlib tdlib, TGMessage message, int rightOffset, boolean needReactionSelector, boolean needShowReactions, boolean needShowViews) { + public ViewPagerHeaderViewReactionsCompact (Context context, MessageOptionsPagerController.State state) { super(context); - this.rightOffset = rightOffset; // - (needShowReactions || needShowViews ? Screen.dp(12) : 0); - this.needReactionSelector = needReactionSelector; - this.needShowReactions = needShowReactions; - this.needShowViews = needShowViews; + this.state = state; + + this.needReactionSelector = state.needShowMessageOptions; + this.needShowReactions = state.needShowMessageReactionSenders; + this.needShowViews = state.needShowMessageViews; + + final boolean needShowMoreButton = state.needShowReactionsPopupPicker; + final int rightOffset = state.headerAlwaysVisibleCountersWidth; ViewPagerTopView topView = new ViewPagerTopView(context); topView.setSelectionColorId(ColorId.headerTabActive); @@ -109,51 +122,44 @@ public ViewPagerHeaderViewReactionsCompact (Context context, Tdlib tdlib, TGMess adapter = new A(topView); - if (needReactionSelector) { - reactionsSelectorRecyclerView = new ReactionsSelectorRecyclerView(context) { - @Override - protected void dispatchDraw (Canvas c) { - super.dispatchDraw(c); - if (rightOffset > 0 && shadowPaint2 != null) { - int width = getMeasuredWidth(); - float s = computeHorizontalScrollRange() - computeHorizontalScrollOffset() - computeHorizontalScrollExtent(); - int alpha = (int) (MathUtils.clamp(s / Screen.dp(20f)) * 255); - - shadowPaint2.setAlpha(alpha); - c.save(); - c.translate(width - shadowSize, 0); - c.drawRect(0, 0, shadowSize, Screen.dp(52), shadowPaint2); - c.restore(); - shadowPaint2.setAlpha(255); - } - } - - @Override - public void onScrolled (int dx, int dy) { - if (rightOffset > 0) { - invalidate(); - } - } - }; - reactionsSelectorRecyclerView.setMessage(message); + if (this.needReactionSelector) { + final int rightOffsetR = state.headerAlwaysVisibleCountersWidth + Screen.dp(needShowMoreButton ? 56: 0); + reactionsSelectorRecyclerView = new ReactionsSelectorRecyclerView(context, state); reactionsSelectorRecyclerView.setLayoutParams(FrameLayoutFix.newParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, - Gravity.CENTER_VERTICAL, 0, 0, this.rightOffset, 0)); + Gravity.CENTER_VERTICAL, 0, 0, rightOffsetR, 0)); + reactionsSelectorRecyclerView.setNeedDrawBorderGradient(rightOffset > 0); addView(reactionsSelectorRecyclerView); + if (state.needShowReactionsPopupPicker) { + reactionsSelectorRecyclerView.setVisibility(GONE); + } } else { reactionsSelectorRecyclerView = null; } backButton = new BackHeaderButton(context); backButton.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(56f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); - backButton.setButtonFactor(needReactionSelector ? BackHeaderButton.TYPE_BACK : BackHeaderButton.TYPE_CLOSE); + backButton.setButtonFactor(this.needReactionSelector ? BackHeaderButton.TYPE_BACK : BackHeaderButton.TYPE_CLOSE); RippleSupport.setTransparentSelector(backButton); Views.setClickable(backButton); - setBackButtonAlpha(1f); - addView(backButton); + if (needShowMoreButton) { + moreButton = new ImageView(context); + moreButton.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(56f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); + moreButton.setImageResource(R.drawable.baseline_small_arrow_down_24); + moreButton.setScaleType(ImageView.ScaleType.CENTER); + moreButton.setColorFilter(Paints.getColorFilter(Theme.getColor(ColorId.icon))); + RippleSupport.setTransparentSelector(moreButton); + Views.setClickable(moreButton); + addView(moreButton); + } else { + moreButton = null; + } + + setBackButtonAlpha(this.needReactionSelector ? 0f: 1f); + recyclerView = new RecyclerView(context) { @Override protected void dispatchDraw (Canvas c) { @@ -199,18 +205,24 @@ public boolean canScrollHorizontally () { updatePaints(Theme.backgroundColor()); } - LinearGradient shader1; - LinearGradient shader2; - private Paint shadowPaint1; - private Paint shadowPaint2; - private final int shadowSize = Screen.dp(35); + private float getReactionPickerHiddenZoneLeft () { + final int x = (int) (reactionsSelectorRecyclerView != null ? reactionsSelectorRecyclerView.getTranslationX() : 0); + final float width = MessageOptionsPagerController.getReactionsPickerRightHiddenWidth(state); + return getMeasuredWidth() - width + x; + } - public void setReactionsSelectorDelegate (ReactionsSelectorRecyclerView.ReactionSelectDelegate delegate) { - if (reactionsSelectorRecyclerView != null) { - reactionsSelectorRecyclerView.setDelegate(delegate); + @Override + protected void dispatchDraw (Canvas c) { + if (state.needShowReactionsPopupPicker) { + c.drawRect(getReactionPickerHiddenZoneLeft(), 0, getMeasuredWidth(), getMeasuredHeight(), Paints.fillingPaint(Theme.backgroundColor())); } + super.dispatchDraw(c); } + LinearGradient shader1; + private Paint shadowPaint1; + private final int shadowSize = Screen.dp(35); + private int oldPaintsColor = 0; public void updatePaints (int color) { @@ -219,15 +231,10 @@ public void updatePaints (int color) { } else return; shader1 = new LinearGradient(0, 0, shadowSize / 2f, 0, color, 0, Shader.TileMode.CLAMP); - shader2 = new LinearGradient(0, 0, shadowSize, 0, 0, color, Shader.TileMode.CLAMP); if (shadowPaint1 == null) { shadowPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); } - if (shadowPaint2 == null) { - shadowPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); - } shadowPaint1.setShader(shader1); - shadowPaint2.setShader(shader2); if (reactionsSelectorRecyclerView != null) { reactionsSelectorRecyclerView.invalidate(); } @@ -241,7 +248,7 @@ public void onPageScrolled (int position, float positionOffset) { if (needReactionSelector && reactionsSelectorRecyclerView != null) { int width = getMeasuredWidth(); if (position == 0) { - float offset = (width - rightOffset - Screen.dp(56)) * (1f - positionOffset); + float offset = (width - state.headerAlwaysVisibleCountersWidth - Screen.dp(56)) * (1f - positionOffset); if (needShowReactions && needShowViews) { getTopView().setItemTranslationX(1, (int) (Screen.dp(-8) * (1f - positionOffset))); getTopView().setItemTranslationX(2, (int) (Screen.dp(-8) * (1f - positionOffset))); @@ -251,6 +258,9 @@ public void onPageScrolled (int position, float positionOffset) { recyclerView.setTranslationX(offset); reactionsSelectorRecyclerView.setTranslationX(-width * positionOffset); backButton.setTranslationX(offset); + if (moreButton != null) { + moreButton.setTranslationX(offset); + } setBackButtonAlpha(positionOffset); isScrollEnabled = false; } else { @@ -259,10 +269,14 @@ public void onPageScrolled (int position, float positionOffset) { recyclerView.setTranslationX(0); reactionsSelectorRecyclerView.setTranslationX(-width); backButton.setTranslationX(0); + if (moreButton != null) { + moreButton.setTranslationX(0); + } setBackButtonAlpha(1f); isScrollEnabled = true; } recyclerView.invalidate(); + invalidate(); } } @@ -278,6 +292,17 @@ private void setBackButtonAlpha (float alpha) { if (alpha == 0f && backButton.getVisibility() != GONE) { backButton.setVisibility(View.GONE); } + + if (moreButton != null) { + final float alphaMore = 1f - alpha; + moreButton.setAlpha(alphaMore); + if (alphaMore > 0f && moreButton.getVisibility() != VISIBLE) { + moreButton.setVisibility(View.VISIBLE); + } + if (alphaMore == 0f && moreButton.getVisibility() != GONE) { + moreButton.setVisibility(View.GONE); + } + } } } @@ -383,7 +408,8 @@ public RecyclerView getRecyclerView () { @Override public boolean onTouchEvent (MotionEvent e) { - return !(e.getAction() == MotionEvent.ACTION_DOWN && !canTouchAt(e.getX(), e.getY())) && super.onTouchEvent(e); + return !(e.getAction() == MotionEvent.ACTION_DOWN && !canTouchAt(e.getX(), e.getY())) && super.onTouchEvent(e) + || (state.needShowReactionsPopupPicker && (e.getX() > getReactionPickerHiddenZoneLeft())); } private boolean canTouchAt (float x, float y) { @@ -396,6 +422,11 @@ public boolean onInterceptTouchEvent (MotionEvent e) { return (e.getAction() == MotionEvent.ACTION_DOWN && !canTouchAt(e.getX(), e.getY())) || super.onInterceptTouchEvent(e); } + @Override + public View getView () { + return this; + } + @Override public ViewPagerTopView getTopView () { return adapter.topView; @@ -405,6 +436,10 @@ public BackHeaderButton getBackButton () { return backButton; } + @Nullable public ImageView getMoreButton () { + return moreButton; + } + private static final float TOP_SCALE_LIMIT = .25f; @Override diff --git a/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerTopView.java b/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerTopView.java index ca2f0eeb9c..d228dc6e2e 100644 --- a/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerTopView.java +++ b/app/src/main/java/org/thunderdog/challegram/navigation/ViewPagerTopView.java @@ -18,6 +18,8 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.text.Layout; import android.text.TextPaint; import android.text.TextUtils; import android.view.Gravity; @@ -26,8 +28,13 @@ import android.view.ViewGroup; import android.view.ViewParent; +import androidx.annotation.Dimension; import androidx.annotation.DrawableRes; +import androidx.annotation.FloatRange; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.Px; import org.thunderdog.challegram.U; import org.thunderdog.challegram.component.sticker.TGStickerObj; @@ -36,7 +43,9 @@ import org.thunderdog.challegram.loader.ComplexReceiver; import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.support.RippleSupport; +import org.thunderdog.challegram.telegram.TGLegacyManager; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PropertyId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; @@ -46,6 +55,8 @@ import org.thunderdog.challegram.util.text.Counter; import org.thunderdog.challegram.util.text.Text; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; @@ -55,9 +66,16 @@ import me.vkryl.core.StringUtils; import me.vkryl.core.lambda.Destroyable; -public class ViewPagerTopView extends FrameLayoutFix implements RtlCheckListener, View.OnClickListener, View.OnLongClickListener, Destroyable { +public class ViewPagerTopView extends FrameLayoutFix implements RtlCheckListener, View.OnClickListener, View.OnLongClickListener, Destroyable, TGLegacyManager.EmojiLoadListener { + public static final @Dimension(unit = Dimension.DP) float SELECTION_HEIGHT = 2f; + public static final @Dimension(unit = Dimension.DP) float ICON_SIZE = 24f; + public static final @Dimension(unit = Dimension.DP) float DEFAULT_ITEM_PADDING = 19f; + public static final @Dimension(unit = Dimension.DP) float COMPACT_ITEM_PADDING = 10f; + public static final @Dimension(unit = Dimension.DP) float DEFAULT_ITEM_SPACING = 6f; + public static final @Dimension(unit = Dimension.DP) float COMPACT_ITEM_SPACING = 4f; + public static class Item { - public final String string; + public final CharSequence string; public final boolean needFakeBold; public final @DrawableRes int iconRes; public ImageReceiver imageReceiver; @@ -68,52 +86,48 @@ public static class Item { public final DrawableProvider provider; public final boolean hidden; - public Item (String string) { - this.string = string; - this.needFakeBold = Text.needFakeBold(string); - this.iconRes = 0; - this.counter = null; - this.provider = null; - this.hidden = false; + public Item (CharSequence string) { + this(string, 0, null, null, false); } - public Item (int iconRes) { - this.string = null; - this.needFakeBold = false; - this.iconRes = iconRes; - this.counter = null; - this.provider =null; - this.hidden = false; + public Item (@DrawableRes int iconRes) { + this(null, iconRes, null, null, false); + } + + public Item (@DrawableRes int iconRes, Counter counter) { + this(null, iconRes, counter, null, false); + } + + public Item (CharSequence string, Counter counter) { + this(string, 0, counter, null, false); + } + + public Item (CharSequence string, @DrawableRes int iconRes, Counter counter) { + this(string, iconRes, counter, null, false); } public Item (Counter counter, DrawableProvider provider, int addWidth) { - this.string = null; - this.needFakeBold = false; - this.iconRes = 0; - this.counter = counter; - this.provider = provider; + this(null, 0, counter, provider, false); this.addWidth = addWidth; - this.hidden = false; } public Item (TGReaction reaction, Counter counter, DrawableProvider provider, int addWidth) { - this.string = null; - this.needFakeBold = false; - this.iconRes = 0; - this.counter = counter; - this.provider = provider; + this(null, 0, counter, provider, false); this.addWidth = addWidth; this.reaction = reaction; - this.hidden = false; } public Item () { - this.string = null; - this.needFakeBold = false; - this.iconRes = 0; - this.counter = null; - this.provider = null; - this.hidden = true; + this(null, 0, null, null, true); + } + + private Item (CharSequence string, @DrawableRes int iconRes, Counter counter, DrawableProvider provider, boolean hidden) { + this.string = string; + this.needFakeBold = string != null && Text.needFakeBold(string); + this.iconRes = iconRes; + this.counter = counter; + this.provider = provider; + this.hidden = hidden; } private Drawable icon; @@ -129,33 +143,43 @@ public boolean equals (Object obj) { return obj instanceof Item && ((Item) obj).iconRes == iconRes && StringUtils.equalsOrBothEmpty(((Item) obj).string, string) && (((Item) obj).counter == counter); } - private int width; + private int width, contentWidth; private int addWidth = 0; + private int minWidth = 0; private int staticWidth = -1; private int translationX = 0; - public void setStaticWidth (int staticWidth) { + public void setMinWidth (@Px int minWidth) { + this.minWidth = minWidth; + } + + public void setStaticWidth (@Px int staticWidth) { this.staticWidth = staticWidth; } - public int calculateWidth (TextPaint paint) { + public int calculateWidth (TextPaint paint, @Px int horizontalSpacing) { final int width; if (staticWidth != -1) { width = staticWidth; } else if (counter != null) { - if (imageReceiver != null) { + if (string != null) { + width = (int) (U.measureEmojiText(string, paint) + counter.getScaledWidth(horizontalSpacing)) + (iconRes != 0 ? Screen.dp(ICON_SIZE) + horizontalSpacing : 0); + } else if (imageReceiver != null) { width = (int) counter.getWidth() + imageReceiverSize; + } else if (iconRes != 0) { + width = Screen.dp(ICON_SIZE) + (int) counter.getScaledWidth(horizontalSpacing); } else { - width = (int) counter.getWidth() + Screen.dp(6f); + width = (int) counter.getWidth()/* + Screen.dp(6f) */; // ??? } } else if (string != null) { - width = (int) U.measureText(string, paint); + width = (int) U.measureEmojiText(string, paint) + (iconRes != 0 ? Screen.dp(ICON_SIZE) + horizontalSpacing : 0); } else if (iconRes != 0) { - width = Screen.dp(24f) + Screen.dp(6f); + width = Screen.dp(ICON_SIZE)/* + Screen.dp(6f)*/; // ??? } else { width = 0; } - this.width = width + addWidth; + this.contentWidth = width + addWidth; + this.width = Math.max(contentWidth, minWidth); return this.width; } @@ -163,40 +187,50 @@ public void setTranslationX (int translationX) { this.translationX = translationX; } - private String ellipsizedString; + private Layout ellipsizedStringLayout; private int actualWidth; public void trimString (int availWidth, TextPaint paint) { if (string != null) { - ellipsizedString = TextUtils.ellipsize(string, paint, availWidth, TextUtils.TruncateAt.END).toString(); - actualWidth = (int) U.measureText(ellipsizedString, paint); + CharSequence ellipsizedString = TextUtils.ellipsize(string, paint, availWidth, TextUtils.TruncateAt.END); + ellipsizedStringLayout = U.createLayout(ellipsizedString, availWidth, paint); + actualWidth = ellipsizedStringLayout.getWidth(); // FIXME counter, icon } else { - ellipsizedString = null; + ellipsizedStringLayout = null; actualWidth = width; } } - public void untrimString () { - ellipsizedString = string; + public void untrimString (TextPaint paint) { + if (string != null) { + ellipsizedStringLayout = U.createLayout(string, (int) Math.ceil(U.measureEmojiText(string, paint)), paint); + } else { + ellipsizedStringLayout = null; + } actualWidth = width; } } private List items; private int maxItemWidth; - private int textPadding; - private ComplexReceiver complexReceiver; + private @Px int itemPadding; + private @Px int itemSpacing; + private final ComplexReceiver complexReceiver; + private CounterAlphaProvider counterAlphaProvider = DEFAULT_COUNTER_ALPHA_PROVIDER; - private @ColorId int - fromTextColorId = ColorId.NONE, - toTextColorId = ColorId.headerText; - private @ColorId int selectionColorId; + private @ColorId int fromTextColorId = ColorId.NONE, toTextColorId = ColorId.headerText; + private @PropertyId int fromTextColorAlphaId = PropertyId.NONE; + + private @ColorId int selectionColorId = ColorId.NONE; + private @FloatRange(from = 0.0, to = 1.0) float selectionAlpha = 1f; public ViewPagerTopView (Context context) { super(context); - this.textPadding = Screen.dp(19f); + this.itemPadding = Screen.dp(DEFAULT_ITEM_PADDING); + this.itemSpacing = Screen.dp(DEFAULT_ITEM_SPACING); this.complexReceiver = new ComplexReceiver(this); setWillNotDraw(false); + TGLegacyManager.instance().addEmojiListener(this); } @Override @@ -210,8 +244,20 @@ public void checkRtl () { } } - public void setTextPadding (int textPadding) { - this.textPadding = textPadding; + public void setItemPadding (@Px int itemPadding) { + if (this.itemPadding != itemPadding) { + this.itemPadding = itemPadding; + this.lastMeasuredWidth = 0; + requestLayout(); + } + } + + public void setItemSpacing (@Px int itemSpacing) { + if (this.itemSpacing != itemSpacing) { + this.itemSpacing = itemSpacing; + this.lastMeasuredWidth = 0; + requestLayout(); + } } private boolean fitsParentWidth; @@ -220,6 +266,17 @@ public void setFitsParentWidth (boolean fits) { this.fitsParentWidth = fits; } + private boolean drawSelectionAtTop; + + public void setDrawSelectionAtTop (boolean drawSelectionAtTop) { + this.drawSelectionAtTop = drawSelectionAtTop; + invalidate(); + } + + public boolean isDrawSelectionAtTop () { + return drawSelectionAtTop; + } + private OnItemClickListener listener; public void setOnItemClickListener (OnItemClickListener listener) { @@ -241,7 +298,37 @@ public void onClick (View v) { private boolean isDark; public void setUseDarkBackground () { - isDark = true; + setUseDarkBackground(true); + } + + public void setUseDarkBackground (boolean useDark) { + if (isDark != useDark) { + isDark = useDark; + int childCount = getChildCount(); + for (int index = 0; index < childCount; index++) { + View childView = getChildAt(index); + if (childView instanceof BackgroundView) { + if (useDark) { + RippleSupport.setTransparentBlackSelector(childView); + } else { + RippleSupport.setTransparentWhiteSelector(childView); + } + } + } + } + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({SLIDE_OFF_DIRECTION_TOP, SLIDE_OFF_DIRECTION_BOTTOM}) + public @interface SlideOffDirection { } + + public static final int SLIDE_OFF_DIRECTION_TOP = -1; + public static final int SLIDE_OFF_DIRECTION_BOTTOM = 1; + + private @SlideOffDirection int slideOffDirection = SLIDE_OFF_DIRECTION_BOTTOM; + + public void setSlideOffDirection (@SlideOffDirection int slideOffDirection) { + this.slideOffDirection = slideOffDirection; } private BackgroundView newBackgroundView (int i) { @@ -261,6 +348,14 @@ private BackgroundView newBackgroundView (int i) { @Override public boolean onLongClick (View v) { + if (listener != null && v instanceof BackgroundView) { + BackgroundView backgroundView = (BackgroundView) v; + if (backgroundView.inSlideOff()) { + return false; + } + backgroundView.cancelSlideOff(); + return listener.onPagerItemLongClick(backgroundView.index); + } return false; } @@ -283,16 +378,19 @@ public void setItems (int[] iconItems) { } public void setItemAt (int index, String text) { + setItemAt(index, new Item(text)); + } + + public void setItemAt (int index, Item item) { Item oldItem = this.items.get(index); - Item item = new Item(text); this.items.set(index, item); onUpdateItems(); - totalWidth -= oldItem.width + textPadding * 2; + totalWidth -= oldItem.width + itemPadding * 2; int textColor = Theme.headerTextColor(); TextPaint paint = Paints.getViewPagerTextPaint(textColor, item.needFakeBold); - item.calculateWidth(paint); - totalWidth += item.width + textPadding * 2; + item.calculateWidth(paint, itemSpacing); + totalWidth += item.width + itemPadding * 2; maxItemWidth = totalWidth / items.size(); this.lastMeasuredWidth = 0; @@ -335,8 +433,8 @@ public void setItems (@NonNull List items) { int textColor = Theme.headerTextColor(); for (Item item : items) { TextPaint paint = Paints.getViewPagerTextPaint(textColor, item.needFakeBold); - item.calculateWidth(paint); - totalWidth += item.width + textPadding * 2; + item.calculateWidth(paint, itemSpacing); + totalWidth += item.width + itemPadding * 2; addView(newBackgroundView(i)); i++; } @@ -348,7 +446,7 @@ public void addItem (String item) { } public void addItemAtIndex (String item, int index) { - addItemAtIndex(new Item(item), index); + addItemAtIndex(new Item(item), index); } public void addItem (int item) { @@ -374,9 +472,9 @@ public void addItemAtIndex (Item item, int index) { int textColor = Theme.headerTextColor(); TextPaint paint = Paints.getViewPagerTextPaint(textColor, item.needFakeBold); - item.calculateWidth(paint); + item.calculateWidth(paint, itemSpacing); int width = item.width; - totalWidth += width + textPadding * 2; + totalWidth += width + itemPadding * 2; maxItemWidth = totalWidth / items.size(); commonItemWidth = calculateCommonItemWidth(width); @@ -385,11 +483,11 @@ public void addItemAtIndex (Item item, int index) { selectionFactor++; } - final int availTextWidth = commonItemWidth - textPadding * 2; + final int availTextWidth = commonItemWidth - itemPadding * 2; if (!shouldWrapContent() && width < availTextWidth) { item.trimString(availTextWidth, paint); } else { - item.untrimString(); + item.untrimString(paint); } addView(newBackgroundView(items.size() - 1)); invalidate(); @@ -442,10 +540,16 @@ private void onUpdateItems () { } } + public void requestItemLayoutAt (int index) { + if (index >= 0 && index < items.size()) { + setItemAt(index, items.get(index)); + } + } + @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { if (shouldWrapContent()) { - int totalWidth = textPadding * 2 * items.size() + getTotalWidth(); + int totalWidth = itemPadding * 2 * items.size() + getTotalWidth(); super.onMeasure(MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY), heightMeasureSpec); layout(totalWidth, true); } else { @@ -478,13 +582,14 @@ private void layout (int width, boolean wrapContent) { commonItemWidth = calculateCommonItemWidth(width); int textColor = Theme.headerTextColor(); - final int availTextWidth = commonItemWidth - textPadding * 2; + final int availTextWidth = commonItemWidth - itemPadding * 2; for (Item item : items) { + TextPaint textPaint = Paints.getViewPagerTextPaint(textColor, item.needFakeBold); if (!wrapContent && item.width < availTextWidth) { - item.trimString(availTextWidth, Paints.getViewPagerTextPaint(textColor, item.needFakeBold)); + item.trimString(availTextWidth, textPaint); } else { - item.untrimString(); + item.untrimString(textPaint); } } @@ -509,19 +614,19 @@ private void recalculateSelection (float selectionFactor, boolean set) { float remainFactor = selectionFactor - (float) ((int) selectionFactor); int selectionIndex = MathUtils.clamp((int) selectionFactor, 0, items.size() - 1); if (remainFactor == 0f) { - selectionWidth = items.get(selectionIndex).actualWidth + textPadding * 2; + selectionWidth = items.get(selectionIndex).actualWidth + itemPadding * 2; } else { - int fromWidth = items.get(selectionIndex).actualWidth + textPadding * 2; + int fromWidth = items.get(selectionIndex).actualWidth + itemPadding * 2; int nextIndex = MathUtils.clamp((int) selectionFactor + 1, 0, items.size() - 1); - int toWidth = items.get(nextIndex).actualWidth + textPadding * 2; + int toWidth = items.get(nextIndex).actualWidth + itemPadding * 2; selectionWidth = fromWidth + (int) ((float) (toWidth - fromWidth) * remainFactor); } selectionLeft = 0; for (int i = 0; i < (int) selectionFactor; i++) { - selectionLeft += items.get(i).actualWidth + textPadding * 2; + selectionLeft += items.get(i).actualWidth + itemPadding * 2; } if (remainFactor != 0f) { - selectionLeft += (int) ((float) (items.get((int) selectionFactor).actualWidth + textPadding * 2) * remainFactor); + selectionLeft += (int) ((float) (items.get((int) selectionFactor).actualWidth + itemPadding * 2) * remainFactor); } } else { selectionLeft = (int) (selectionFactor * (float) commonItemWidth); @@ -544,6 +649,12 @@ private void recalculateSelection (float selectionFactor, boolean set) { } } + public void updateAnchorPosition (boolean animated) { + if (selectionChangeListener != null) { + selectionChangeListener.onSelectionChanged(lastCallSelectionLeft, lastCallSelectionWidth, items.get(0).actualWidth, items.get(items.size() - 1).actualWidth, lastCallSelectionFactor, animated); + } + } + /*public void resendSectionChangeEvent (boolean animated) { if (items != null && !items.isEmpty()) { selectionChangeListener.onSelectionChanged(lastCallSelectionLeft, lastCallSelectionWidth, items.get(0).actualWidth, items.get(items.size() - 1).actualWidth, lastCallSelectionFactor, animated); @@ -587,8 +698,13 @@ public void setFromTo (int fromIndex, int toIndex) { } public boolean setTextFromToColorId (@ColorId int fromColorId, @ColorId int toColorId) { - if (this.fromTextColorId != fromColorId || this.toTextColorId != toColorId) { + return setTextFromToColorId(fromColorId, toColorId, PropertyId.NONE); + } + + public boolean setTextFromToColorId (@ColorId int fromColorId, @ColorId int toColorId, @PropertyId int fromColorAlphaId) { + if (this.fromTextColorId != fromColorId || this.toTextColorId != toColorId || this.fromTextColorAlphaId != fromColorAlphaId) { this.fromTextColorId = fromColorId; + this.fromTextColorAlphaId = fromColorAlphaId; this.toTextColorId = toColorId; invalidate(); return true; @@ -597,8 +713,13 @@ public boolean setTextFromToColorId (@ColorId int fromColorId, @ColorId int toCo } public boolean setSelectionColorId (@ColorId int colorId) { - if (this.selectionColorId != colorId) { + return setSelectionColorId(colorId, 1f); + } + + public boolean setSelectionColorId (@ColorId int colorId, @FloatRange(from = 0.0, to = 1.0) float alpha) { + if (this.selectionColorId != colorId || this.selectionAlpha != alpha) { this.selectionColorId = colorId; + this.selectionAlpha = alpha; invalidate(); return true; } @@ -666,14 +787,22 @@ public void draw (Canvas c) { if (overlayFactor != 1f) { int textToColor = Theme.getColor(toTextColorId); - int textFromColor = fromTextColorId != 0 ? Theme.getColor(fromTextColorId) : ColorUtils.alphaColor(Theme.getSubtitleAlpha(), Theme.getColor(ColorId.headerText)); - int selectionColor = selectionColorId != 0 ? Theme.getColor(selectionColorId) : ColorUtils.alphaColor(.9f, Theme.getColor(ColorId.headerText)); + int textFromColor = fromTextColorId != ColorId.NONE ? + ColorUtils.alphaColor(fromTextColorAlphaId != PropertyId.NONE ? Theme.getProperty(fromTextColorAlphaId) : 1f, Theme.getColor(fromTextColorId)) : + ColorUtils.alphaColor(Theme.getSubtitleAlpha(), Theme.getColor(ColorId.headerText)); + int selectionColor = selectionColorId != ColorId.NONE ? + ColorUtils.alphaColor(selectionAlpha, Theme.getColor(selectionColorId)) : + ColorUtils.alphaColor(.9f, Theme.getColor(ColorId.headerText)); boolean rtl = Lang.rtl(); + int selectionHeight = Screen.dp(SELECTION_HEIGHT); int selectionLeft = rtl ? this.totalWidth - this.selectionLeft - this.selectionWidth : this.selectionLeft; + int selectionRight = selectionLeft + this.selectionWidth; + int selectionTop = this.drawSelectionAtTop ? 0 : viewHeight - selectionHeight; + int selectionBottom = selectionTop + selectionHeight; - c.drawRect(selectionLeft, viewHeight - Screen.dp(2f), selectionLeft + selectionWidth, viewHeight, Paints.fillingPaint(disabledFactor == 0f ? selectionColor : ColorUtils.fromToArgb(selectionColor, textFromColor, disabledFactor))); + c.drawRect(selectionLeft, selectionTop, selectionRight, selectionBottom, Paints.fillingPaint(disabledFactor == 0f ? selectionColor : ColorUtils.fromToArgb(selectionColor, textFromColor, disabledFactor))); int cx = rtl ? totalWidth : 0; int itemIndex = 0; @@ -707,36 +836,72 @@ public void draw (Canvas c) { final int itemWidth; if (wrapContent) { - itemWidth = item.actualWidth + textPadding * 2; + itemWidth = item.actualWidth + itemPadding * 2; } else { itemWidth = commonItemWidth; } if (rtl) cx -= itemWidth; if (!item.hidden) { + int contentWidth = Math.min(item.contentWidth, item.actualWidth); + int horizontalPadding = Math.max(itemWidth - contentWidth, 0) / 2; int color = ColorUtils.fromToArgb(textFromColor, textToColor, factor * (1f - disabledFactor)); if (item.counter != null) { float alphaFactor = 1f - MathUtils.clamp(Math.abs(selectionFactor - i)); - float imageAlpha = .5f + .5f * alphaFactor; + float imageAlpha = counterAlphaProvider.getDrawableAlpha(item.counter, alphaFactor); if (items.get(0).hidden) { alphaFactor = Math.max(alphaFactor, 1f - MathUtils.clamp(selectionFactor)); if (i == 1 && selectionFactor < 1) { alphaFactor = 1f; } } - float counterAlpha = .5f + .5f * alphaFactor; - if (item.imageReceiver != null) { + float textAlpha = counterAlphaProvider.getTextAlpha(item.counter, alphaFactor); + float backgroundAlpha = counterAlphaProvider.getBackgroundAlpha(item.counter, alphaFactor); + if (item.ellipsizedStringLayout != null) { + int stringX; + if (item.iconRes != 0) { + Drawable drawable = item.getIcon(); + Drawables.draw(c, drawable, cx + horizontalPadding, viewHeight / 2 - drawable.getMinimumHeight() / 2, Paints.getPorterDuffPaint(color)); + stringX = cx + horizontalPadding + Screen.dp(ICON_SIZE) + itemSpacing; + } else { + stringX = cx + horizontalPadding; + } + int stringY = viewHeight / 2 - item.ellipsizedStringLayout.getHeight() / 2; + c.translate(stringX, stringY); + item.ellipsizedStringLayout.getPaint().setColor(color); + item.ellipsizedStringLayout.draw(c); + c.translate(-stringX, -stringY); + item.counter.draw(c, cx + itemWidth - horizontalPadding - item.counter.getWidth() / 2f, viewHeight / 2f, Gravity.CENTER, textAlpha, backgroundAlpha, imageAlpha, item.provider, ColorId.NONE); + } else if (item.imageReceiver != null) { int size = item.imageReceiverSize; int imgY = (viewHeight - size) / 2; item.imageReceiver.setAlpha(imageAlpha); item.imageReceiver.setBounds(cx, imgY, cx + size, imgY + size); item.imageReceiver.drawScaled(c, item.imageReceiverScale); - item.counter.draw(c, cx + size, viewHeight / 2f, Gravity.LEFT, counterAlpha, item.provider, 0); + item.counter.draw(c, cx + size, viewHeight / 2f, Gravity.LEFT, textAlpha, backgroundAlpha, imageAlpha, item.provider, 0); + } else if (item.iconRes != 0) { + Drawable drawable = item.getIcon(); + Drawables.draw(c, drawable, cx + horizontalPadding, viewHeight / 2 - drawable.getMinimumHeight() / 2, Paints.getPorterDuffPaint(color)); + item.counter.draw(c, cx + itemWidth - horizontalPadding - item.counter.getWidth() / 2f, viewHeight / 2f, Gravity.CENTER, textAlpha, backgroundAlpha, imageAlpha, item.provider, ColorId.NONE); } else { - item.counter.draw(c, cx + itemWidth / 2f, viewHeight / 2f, Gravity.CENTER, counterAlpha, imageAlpha, item.provider, 0); + float counterWidth = item.counter.getWidth(); + float addX = -Math.min((itemWidth - counterWidth) / 2f + item.translationX, 0); + item.counter.draw(c, cx + itemWidth / 2f + addX, viewHeight / 2f, Gravity.CENTER, textAlpha, backgroundAlpha, imageAlpha, item.provider, 0); } - } else if (item.ellipsizedString != null) { - c.drawText(item.ellipsizedString, cx + itemWidth / 2 - item.actualWidth / 2, viewHeight / 2 + Screen.dp(6f), Paints.getViewPagerTextPaint(color, item.needFakeBold)); + } else if (item.ellipsizedStringLayout != null) { + int stringX; + if (item.iconRes != 0) { + Drawable drawable = item.getIcon(); + Drawables.draw(c, drawable, cx + horizontalPadding, viewHeight / 2 - drawable.getMinimumHeight() / 2, Paints.getPorterDuffPaint(color)); + stringX = cx + horizontalPadding + Screen.dp(ICON_SIZE) + itemSpacing; + } else { + stringX = cx + itemWidth / 2 - item.actualWidth / 2; + } + int stringY = viewHeight / 2 - item.ellipsizedStringLayout.getHeight() / 2; + c.translate(stringX, stringY); + item.ellipsizedStringLayout.getPaint().setColor(color); + item.ellipsizedStringLayout.draw(c); + c.translate(-stringX, -stringY); } else if (item.iconRes != 0) { Drawable drawable = item.getIcon(); Drawables.draw(c, drawable, cx + itemWidth / 2 - drawable.getMinimumWidth() / 2, viewHeight / 2 - drawable.getMinimumHeight() / 2, Paints.getPorterDuffPaint(color)); @@ -775,8 +940,30 @@ public void draw (Canvas c) { } } + private static final CounterAlphaProvider DEFAULT_COUNTER_ALPHA_PROVIDER = new CounterAlphaProvider() { + }; + + public interface CounterAlphaProvider { + default float getTextAlpha (Counter counter, @FloatRange(from = 0f, to = 1f) float alphaFactor) { + return .5f + .5f * alphaFactor; + } + default float getDrawableAlpha (Counter counter, @FloatRange(from = 0f, to = 1f) float alphaFactor) { + return .5f + .5f * alphaFactor; + } + default float getBackgroundAlpha (Counter counter, @FloatRange(from = 0f, to = 1f) float alphaFactor) { + return .5f + .5f * alphaFactor; + } + } + + public void setCounterAlphaProvider (CounterAlphaProvider counterAlphaProvider) { + this.counterAlphaProvider = counterAlphaProvider; + } + public interface OnItemClickListener { void onPagerItemClick (int index); + default boolean onPagerItemLongClick (int index) { + return false; + } } public interface OnSlideOffListener { @@ -816,7 +1003,11 @@ public BackgroundView (Context context) { Views.setClickable(this); } - private boolean inSlideOff, needSlideOff; + private long touchDownTime; + private float touchDownY; + private float touchX; + private float touchY; + private boolean inSlideOff; private ViewParent lockedParent; @Override @@ -826,63 +1017,80 @@ public boolean onTouchEvent (MotionEvent e) { return ((View) getParent()).getAlpha() >= 1f && super.onTouchEvent(e); } super.onTouchEvent(e); - if (e.getAction() == MotionEvent.ACTION_DOWN) { - if (lockedParent != null) { - lockedParent.requestDisallowInterceptTouchEvent(false); - lockedParent = null; - } - needSlideOff = slideOffListener.onSlideOffPrepare(this, e, index); - if (needSlideOff) { - lockedParent = getParent(); - if (lockedParent != null) { - lockedParent.requestDisallowInterceptTouchEvent(true); - } - } - } - if (!needSlideOff) { - return true; - } switch (e.getAction()) { + case MotionEvent.ACTION_DOWN: + touchX = e.getX(); + touchY = e.getY(); + touchDownY = e.getY(); + touchDownTime = e.getDownTime(); + break; case MotionEvent.ACTION_MOVE: { - int start = getMeasuredHeight(); - boolean inSlideOff = e.getY() >= start; - if (this.inSlideOff != inSlideOff) { - this.inSlideOff = inSlideOff; + if (touchDownTime < 0L) { + break; + } + touchX = e.getX(); + touchY = e.getY(); + if (lockedParent == null) { + if (Math.abs(touchY - touchDownY) > Screen.getTouchSlop()) { + boolean needSlideOff = slideOffListener.onSlideOffPrepare(this, e, index); + if (needSlideOff) { + lockedParent = getParent(); + if (lockedParent != null) { + lockedParent.requestDisallowInterceptTouchEvent(true); + } + } + } + } else { + int start = getMeasuredHeight(); + boolean inSlideOff = topView.slideOffDirection == SLIDE_OFF_DIRECTION_TOP ? touchY <= start : touchY >= start; + if (this.inSlideOff != inSlideOff) { + this.inSlideOff = inSlideOff; + if (inSlideOff) { + slideOffListener.onSlideOffStart(this, e, index); + } else { + slideOffListener.onSlideOffFinish(this, e, index, false); + } + } if (inSlideOff) { - slideOffListener.onSlideOffStart(this, e, index); - } else { - slideOffListener.onSlideOffFinish(this, e, index, false); + slideOffListener.onSlideOffMovement(this, e, index); } } - if (inSlideOff) { - slideOffListener.onSlideOffMovement(this, e, index); - } break; } case MotionEvent.ACTION_CANCEL: - if (inSlideOff) { - inSlideOff = false; - slideOffListener.onSlideOffFinish(this, e, index, false); - } - if (lockedParent != null) { - lockedParent.requestDisallowInterceptTouchEvent(false); - lockedParent = null; - } + finishSlideOff(e, slideOffListener, /* apply */ false); break; case MotionEvent.ACTION_UP: - if (inSlideOff) { - inSlideOff = false; - slideOffListener.onSlideOffFinish(this, e, index, true); - } - if (lockedParent != null) { - lockedParent.requestDisallowInterceptTouchEvent(false); - lockedParent = null; - } + finishSlideOff(e, slideOffListener, /* apply */ true); break; } return true; } + public boolean inSlideOff() { + return inSlideOff; + } + + public void cancelSlideOff () { + MotionEvent e = MotionEvent.obtain(touchDownTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_CANCEL, touchX, touchY, /* metaState */ 0); + OnSlideOffListener slideOffListener = topView != null ? topView.onSlideOffListener : null; + finishSlideOff(e, slideOffListener, /* apply */ false); + } + + public void finishSlideOff (MotionEvent e, @Nullable OnSlideOffListener slideOffListener, boolean apply) { + touchDownTime = Long.MIN_VALUE; + if (inSlideOff) { + inSlideOff = false; + if (slideOffListener != null) { + slideOffListener.onSlideOffFinish(this, e, index, apply); + } + } + if (lockedParent != null) { + lockedParent.requestDisallowInterceptTouchEvent(false); + lockedParent = null; + } + } + private ViewPagerTopView topView; public void setBoundView (ViewPagerTopView topView) { @@ -900,9 +1108,9 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { if (topView.shouldWrapContent()) { int left = 0; for (int i = 0; i < index; i++) { - left += topView.items.get(i).width + topView.textPadding * 2; + left += topView.items.get(i).width + topView.itemPadding * 2; } - int itemWidth = topView.items.get(index).width + topView.textPadding * 2; + int itemWidth = topView.items.get(index).width + topView.itemPadding * 2; if (Lang.rtl()) { left = MeasureSpec.getSize(widthMeasureSpec) - left - itemWidth; } @@ -915,4 +1123,9 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { } } } + + @Override + public void onEmojiUpdated (boolean isPackSwitch) { + invalidate(); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/player/AudioController.java b/app/src/main/java/org/thunderdog/challegram/player/AudioController.java index 12174feee7..85fe8ebda4 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/AudioController.java +++ b/app/src/main/java/org/thunderdog/challegram/player/AudioController.java @@ -62,6 +62,7 @@ import me.vkryl.android.animator.FactorAnimator; import me.vkryl.core.ArrayUtils; import me.vkryl.core.MathUtils; +import me.vkryl.td.Td; public class AudioController extends BasePlaybackController implements TGAudio.PlayListener, TGPlayerController.TrackListChangeListener, FactorAnimator.Target { private final TdlibManager context; @@ -173,6 +174,7 @@ private void setPlaybackMode (int mode, boolean needForeground) { @Override protected boolean isSupported (TdApi.Message message) { + //noinspection SwitchIntDef switch (message.content.getConstructor()) { case TdApi.MessageAudio.CONSTRUCTOR: case TdApi.MessageVoiceNote.CONSTRUCTOR: @@ -186,7 +188,7 @@ protected boolean isPlayingSomething () { } protected boolean isPlayingVoice () { - return isPlayingSomething() && playList.get(playIndex).content.getConstructor() == TdApi.MessageVoiceNote.CONSTRUCTOR; + return isPlayingSomething() && Td.isVoiceNote(playList.get(playIndex).content); } @Override @@ -276,12 +278,12 @@ protected void playPause (boolean isPlaying) { @Override protected void startPlayback (Tdlib tdlib, TdApi.Message message, boolean byUserRequest, boolean hadObject, Tdlib previousTdlib, int previousFileId) { if (playbackMode == PLAYBACK_MODE_UNSET) { - setPlaybackMode(determineBestPlaybackMode(message.content.getConstructor() == TdApi.MessageVoiceNote.CONSTRUCTOR), message.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR); + setPlaybackMode(determineBestPlaybackMode(Td.isVoiceNote(message.content)), Td.isAudio(message.content)); } Log.i(Log.TAG_PLAYER, "startPlayback mode:%d byUserRequest:%b, hadObject:%b, previousFileId:%d", playbackMode, byUserRequest, hadObject, previousFileId); switch (playbackMode) { case PLAYBACK_MODE_LEGACY: { - if (message.content.getConstructor() == TdApi.MessageVoiceNote.CONSTRUCTOR) { + if (Td.isVoiceNote(message.content)) { legacyAudio = new TGAudio(tdlib, message, ((TdApi.MessageVoiceNote) message.content).voiceNote); } else { legacyAudio = new TGAudio(tdlib, message, ((TdApi.MessageAudio) message.content).audio); diff --git a/app/src/main/java/org/thunderdog/challegram/player/ProximityManager.java b/app/src/main/java/org/thunderdog/challegram/player/ProximityManager.java index 0136574760..c64e99352f 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/ProximityManager.java +++ b/app/src/main/java/org/thunderdog/challegram/player/ProximityManager.java @@ -42,6 +42,8 @@ import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.unsorted.Settings; +import me.vkryl.td.Td; + public class ProximityManager implements Settings.RaiseToSpeakListener, SensorEventListener, UI.StateListener { public interface Delegate { void onUpdateAttributes (); @@ -57,7 +59,7 @@ public ProximityManager (TGPlayerController player, Delegate delegate) { } private boolean isPlayingVideo () { - return playbackObject != null && playbackObject.content.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR; + return playbackObject != null && Td.isVideoNote(playbackObject.content); } public void setPlaybackObject (@Nullable TdApi.Message playbackObject) { @@ -67,7 +69,7 @@ public void setPlaybackObject (@Nullable TdApi.Message playbackObject) { if (hadObject != hasObject) { if (hasObject) { Settings.instance().addRaiseToSpeakListener(this); - uiPaused = UI.getUiState() != UI.STATE_RESUMED; + uiPaused = UI.getUiState() != UI.State.RESUMED; UI.addStateListener(this); this.isVideo = isPlayingVideo(); setEarpieceMode(Settings.instance().getEarpieceMode(isVideo)); @@ -124,7 +126,7 @@ private boolean setEarpieceMode (int mode) { @Override public void onUiStateChanged (int newState) { - boolean isPaused = newState != UI.STATE_RESUMED; + boolean isPaused = newState != UI.State.RESUMED; if (this.uiPaused != isPaused) { this.uiPaused = isPaused; checkProximitySensorEnabled(); diff --git a/app/src/main/java/org/thunderdog/challegram/player/RecordAudioVideoController.java b/app/src/main/java/org/thunderdog/challegram/player/RecordAudioVideoController.java index dc9a90fce1..9ffb8156b8 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/RecordAudioVideoController.java +++ b/app/src/main/java/org/thunderdog/challegram/player/RecordAudioVideoController.java @@ -62,6 +62,7 @@ import org.thunderdog.challegram.util.HapticMenuHelper; import org.thunderdog.challegram.widget.CircleFrameLayout; import org.thunderdog.challegram.widget.NoScrollTextView; +import org.thunderdog.challegram.widget.SendButton; import org.thunderdog.challegram.widget.ShadowView; import org.thunderdog.challegram.widget.SimpleVideoPlayer; import org.thunderdog.challegram.widget.VideoTimelineView; @@ -111,7 +112,8 @@ public class RecordAudioVideoController implements private CircleFrameLayout videoLayout; private View videoPlaceholderView; private RoundProgressView progressView; - private ImageView deleteButton, sendButton; + private SendButton sendButton; + private ImageView deleteButton; private HapticMenuHelper sendHelper; private VideoTimelineView videoTimelineView; private SimpleVideoPlayer videoPreviewView; @@ -159,7 +161,6 @@ private void updateColors () { this.cancelView.setTextColor(Theme.getColor(ColorId.textNeutral)); this.videoPlaceholderView.setBackgroundColor(Theme.fillingColor()); this.deleteButton.setColorFilter(Theme.iconColor()); - this.sendButton.setColorFilter(Theme.chatSendButtonColor()); this.videoBackgroundView.setBackgroundColor(Theme.getColor(ColorId.previewBackground)); this.cornerView.invalidate(); @@ -421,17 +422,17 @@ public boolean onTouchEvent (MotionEvent event) { this.deleteButton.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(56f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.LEFT)); this.inputOverlayView.addView(deleteButton); - this.sendButton = new ImageView(context) { + this.sendButton = new SendButton(context, R.drawable.deproko_baseline_send_24) { @Override public boolean onTouchEvent (MotionEvent event) { return editFactor > 0f && Views.isValid(this) && super.onTouchEvent(event); } }; - this.sendButton.setScaleType(ImageView.ScaleType.CENTER); - this.sendButton.setImageResource(R.drawable.deproko_baseline_send_24); Views.setClickable(sendButton); this.sendButton.setOnClickListener(v -> { - sendVideo(Td.newSendOptions()); + if (!targetController.showSlowModeRestriction(v, null)) { + sendVideo(Td.newSendOptions()); + } }); this.sendButton.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(55f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.RIGHT)); this.inputOverlayView.addView(sendButton); @@ -667,6 +668,7 @@ private void resetViews () { videoTimelineView.performDestroy(); videoPreviewView.setMuted(true); videoPreviewView.setPlaying(true); + sendButton.destroySlowModeCounterController(); setReleased(false, false); resetState(); } @@ -921,7 +923,7 @@ public boolean startRecording (View view, boolean inRaiseMode) { return false; } - CharSequence restrictionText = tdlib.getVoiceVideoRestricitonText(tdlib.chat(targetChatId), needVideo); + CharSequence restrictionText = tdlib.getVoiceVideoRestrictionText(tdlib.chat(targetChatId), needVideo); if (restrictionText != null) { if (view != null) { context.tooltipManager().builder(view).controller(targetController).icon(R.drawable.baseline_warning_24).show(tdlib, restrictionText).hideDelayed(); @@ -996,7 +998,8 @@ private boolean canSendRecording () { } public boolean finishRecording (boolean needPreview) { - return stopRecording(canSendRecording() ? (needPreview ? CLOSE_MODE_PREVIEW : (hasValidOutputTarget() && targetController.areScheduledOnly()) ? CLOSE_MODE_PREVIEW_SCHEDULE : CLOSE_MODE_SEND) : CLOSE_MODE_CANCEL, true); + boolean forcePreview = tdlib.getSlowModeRestrictionText(targetChatId) != null; + return stopRecording(canSendRecording() ? (needPreview || forcePreview ? CLOSE_MODE_PREVIEW : (hasValidOutputTarget() && targetController.areScheduledOnly()) ? CLOSE_MODE_PREVIEW_SCHEDULE : CLOSE_MODE_SEND) : CLOSE_MODE_CANCEL, true); } private boolean stopRecording (int closeMode, boolean showPrompt) { @@ -1017,6 +1020,9 @@ private boolean stopRecording (int closeMode, boolean showPrompt) { if (async) { mode = RECORD_MODE_VIDEO_EDIT; editAnimator.setValue(true, false); + if (sendButton != null) { + sendButton.getSlowModeCounterController(tdlib).setCurrentChat(targetChatId); + } } if (recordingVideo && (closeMode == CLOSE_MODE_PREVIEW || closeMode == CLOSE_MODE_PREVIEW_SCHEDULE)) { @@ -1075,13 +1081,21 @@ private boolean awaitingRoundResult () { return roundCloseMode != CLOSE_MODE_CANCEL; } + private Throwable releasedTrace; + private boolean cleanupVideoPending; + private void cleanupVideoRecording () { if (recordingVideo && Math.max(recordFactor, editFactor) * (1f - renderFactor) == 0f && ownedCamera != null && !awaitingRoundResult()) { + if (recordingRoundVideo) { + cleanupVideoPending = true; + return; + } ownedCamera.onCleanAfterHide(); ownedCamera.releaseCameraLayout(); setupCamera(false); context.releaseCameraOwnership(); + releasedTrace = Log.generateException(); ownedCamera = null; resetRoundState(); @@ -1211,6 +1225,7 @@ private void onRecordFocus () { } private void onRecordRemoved () { + // note: when animations disabled happens before finishVideoRecording context.setScreenFlagEnabled(BaseActivity.SCREEN_FLAG_RECORDING, false); context.setOrientationLockFlagEnabled(BaseActivity.ORIENTATION_FLAG_RECORDING, false); cleanupVideoRecording(); @@ -1450,15 +1465,27 @@ private void setRoundGenerationFile (final TdApi.File file) { private int savedRoundDurationSeconds; private void startVideoRecording () { + if (this.recordingRoundVideo) + throw new IllegalStateException(); this.recordingRoundVideo = true; ownedCamera.getLegacyManager().requestRoundVideoCapture(roundKey, this, roundOutputPath); } private void finishVideoRecording (int closeMode) { + // note: when animations disabled, happens after cleanupVideoRecording is called + if (!this.recordingRoundVideo) + throw new IllegalStateException(); this.recordingRoundVideo = false; this.roundCloseMode = closeMode; final boolean needResult = closeMode != CLOSE_MODE_CANCEL; + if (ownedCamera == null) { + throw new RuntimeException(releasedTrace); + } ownedCamera.getLegacyManager().finishOrCancelRoundVideoCapture(roundKey, needResult); + if (cleanupVideoPending) { + cleanupVideoPending = false; + cleanupVideoRecording(); + } } @Override @@ -1482,14 +1509,14 @@ private void sendVideoNote (TdApi.InputMessageVideoNote videoNote, TdApi.Message TdApi.InputMessageVideoNote newVideoNote = tdlib.filegen().createThumbnail(videoNote, isSecretChat, helperFile); long chatId = targetController.getChatId(); long messageThreadId = targetController.getMessageThreadId(); - long replyToMessageId = targetController.obtainReplyId(); + TdApi.InputMessageReplyTo replyTo = targetController.obtainReplyTo(); final TdApi.MessageSendOptions finalSendOptions = Td.newSendOptions(initialSendOptions, tdlib.chatDefaultDisableNotifications(chatId)); if (newVideoNote.thumbnail == null && helperFile != null) { tdlib.client().send(new TdApi.DownloadFile(helperFile.id, 1, 0, 0, true), result -> { - tdlib.sendMessage(chatId, messageThreadId, replyToMessageId, finalSendOptions, result.getConstructor() == TdApi.File.CONSTRUCTOR ? tdlib.filegen().createThumbnail(videoNote, isSecretChat, (TdApi.File) result) : newVideoNote, null); + tdlib.sendMessage(chatId, messageThreadId, replyTo, finalSendOptions, result.getConstructor() == TdApi.File.CONSTRUCTOR ? tdlib.filegen().createThumbnail(videoNote, isSecretChat, (TdApi.File) result) : newVideoNote, null); }); } else { - tdlib.sendMessage(chatId, messageThreadId, replyToMessageId, finalSendOptions, newVideoNote, null); + tdlib.sendMessage(chatId, messageThreadId, replyTo, finalSendOptions, newVideoNote, null); } }); } @@ -1653,6 +1680,6 @@ private void closeVideoEditMode (@NonNull TdApi.MessageSendOptions initialSendOp } videoPreviewView.setPlaying(false); editAnimator.setValue(false, true); - + sendButton.destroySlowModeCounterController(); } } diff --git a/app/src/main/java/org/thunderdog/challegram/player/RoundVideoController.java b/app/src/main/java/org/thunderdog/challegram/player/RoundVideoController.java index f1db33bd35..a4452e28bf 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/RoundVideoController.java +++ b/app/src/main/java/org/thunderdog/challegram/player/RoundVideoController.java @@ -99,7 +99,7 @@ public RoundVideoController (BaseActivity context) { @Override protected boolean isSupported (TdApi.Message message) { - return message.content.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR; + return Td.isVideoNote(message.content); } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/player/RoundVideoRecorder.java b/app/src/main/java/org/thunderdog/challegram/player/RoundVideoRecorder.java index b7f366cfb3..e889fadbc8 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/RoundVideoRecorder.java +++ b/app/src/main/java/org/thunderdog/challegram/player/RoundVideoRecorder.java @@ -1446,7 +1446,7 @@ public Surface getInputSurface() { } private void didWriteData(File file, boolean last) { - if (videoConvertFirstWrite) { + if (videoConvertFirstWrite && !last) { videoConvertFirstWrite = false; } else if (last) { dispatchVideoRecordFinished(workingKey, file.length(), SystemClock.uptimeMillis() - recordStartTime, TimeUnit.MILLISECONDS); diff --git a/app/src/main/java/org/thunderdog/challegram/player/TGPlayerController.java b/app/src/main/java/org/thunderdog/challegram/player/TGPlayerController.java index 80a023b45c..f32d7c43e5 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/TGPlayerController.java +++ b/app/src/main/java/org/thunderdog/challegram/player/TGPlayerController.java @@ -342,12 +342,12 @@ private void clearFilesImpl () { public boolean isPlayingMusic () { synchronized (this) { - return message != null && message.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR; + return message != null && Td.isAudio(message.content); } } public int canAddToPlayList (Tdlib tdlib, TdApi.Message track) { - if (track.content.getConstructor() != TdApi.MessageAudio.CONSTRUCTOR) { + if (!Td.isAudio(track.content)) { return ADD_MODE_NONE; } synchronized (this) { @@ -391,7 +391,7 @@ private int canAddToPlayListImpl (Tdlib tdlib, TdApi.Message track, int currentP public void moveTrack (int fromPosition, int toPosition) { synchronized (this) { - if (this.message != null && playState != STATE_NONE && this.message.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR) { + if (this.message != null && playState != STATE_NONE && Td.isAudio(this.message.content)) { TdApi.Message track = messageList.remove(fromPosition); messageList.add(toPosition, track); notifyTrackListItemMoved(trackListChangeListeners, tdlib, track, fromPosition, toPosition); @@ -402,7 +402,7 @@ public void moveTrack (int fromPosition, int toPosition) { public void removeTrack (TdApi.Message track, boolean byUserRequest) { synchronized (this) { - if (this.message != null && playState != STATE_NONE && this.message.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR && messageList.size() > 1) { + if (this.message != null && playState != STATE_NONE && Td.isAudio(this.message.content) && messageList.size() > 1) { int position = indexOfMessage(track); removeTrackImpl(track, position, byUserRequest); } @@ -510,7 +510,7 @@ public static int getPlayRepeatFlag (int flags) { } private static boolean supportsPlaybackFlags (TdApi.Message message) { - return message != null && message.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR; + return message != null && Td.isAudio(message.content); } private static int getPlaybackFlags (TdApi.Message message, int flags) { @@ -565,7 +565,7 @@ public boolean compare (Tdlib tdlib, long chatId, long messageId) { } } - public int getContentType () { + public @TdApi.MessageContent.Constructors int getContentType () { synchronized (this) { return message != null ? message.content.getConstructor() : 0; } @@ -579,13 +579,13 @@ public int getContentType () { public boolean isPlayingRoundVideo () { synchronized (this) { - return message != null && message.content.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR; + return message != null && Td.isVideoNote(message.content); } } public boolean isPlayingVoice () { synchronized (this) { - return message != null && message.content.getConstructor() == TdApi.MessageVoiceNote.CONSTRUCTOR; + return message != null && Td.isVoiceNote(message.content); } } @@ -735,7 +735,7 @@ private static void notifyTrackChange (ReferenceList list, } private void updateProximityMessageImpl () { - if (message != null && (message.content.getConstructor() == TdApi.MessageVoiceNote.CONSTRUCTOR || message.content.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR)) { + if (message != null && (Td.isVoiceNote(message.content) || Td.isVideoNote(message.content))) { proximityManager.setPlaybackObject(message); } else { proximityManager.setPlaybackObject(null); @@ -893,7 +893,7 @@ public void stopPlayback (boolean byUserRequest) { } public void stopRoundPlayback (boolean byUserRequest) { - if (message != null && message.content.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR) { + if (message != null && Td.isVideoNote(message.content)) { playPauseMessageImpl(null, byUserRequest, false, tdlib, null); } } @@ -923,6 +923,7 @@ public void removeMessages (Tdlib tdlib, long chatId, long[] messageIds) { TdApi.Message track = messageList.get(i); if (track.chatId != 0 && track.chatId == chatId && ArrayUtils.indexOf(messageIds, track.id) != -1) { if (i == currentIndex) { + //noinspection SwitchIntDef switch (track.content.getConstructor()) { case TdApi.MessageAudio.CONSTRUCTOR: // Do nothing. Let user finish playback @@ -950,7 +951,7 @@ public void skip (boolean next) { handler.sendMessage(Message.obtain(handler, ACTION_SKIP, next ? 1 : 0, 0)); } else { synchronized (this) { - if (message != null && message.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR) { + if (message != null && Td.isAudio(message.content)) { context.audio().skip(next); } } @@ -1278,11 +1279,11 @@ public TdApi.Message getCurrentTrack () { } private static boolean canControlQueue (TdApi.Message message) { - return message.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR; + return Td.isAudio(message.content); } - private static boolean matchesFilter (TdApi.MessageContent content, int contentType) { - int ctr = content.getConstructor(); + private static boolean matchesFilter (TdApi.MessageContent content, @TdApi.MessageContent.Constructors int contentType) { + final @TdApi.MessageContent.Constructors int ctr = content.getConstructor(); return ctr == contentType || ((ctr == TdApi.MessageVoiceNote.CONSTRUCTOR || ctr == TdApi.MessageVideoNote.CONSTRUCTOR) && (contentType == TdApi.MessageVoiceNote.CONSTRUCTOR || contentType == TdApi.MessageVideoNote.CONSTRUCTOR)); } @@ -1390,8 +1391,7 @@ private Client.ResultHandler newStackHandler (final int contextId, final boolean break; } default: { - Log.unexpectedTdlibResponse(object, TdApi.SearchSecretMessages.class, TdApi.SearchChatMessages.class, TdApi.Messages.class, TdApi.Error.class); - return; + throw new UnsupportedOperationException(object.toString()); } } addMessages(contextId, moreMessages, areNew); @@ -1481,7 +1481,7 @@ private void prepareStack (boolean allowNewer, boolean allowOlder, int forceReas final long minMessageId = playlistMinMessageId; final long maxMessageId = playlistMaxMessageId; - final int contentType = message.content.getConstructor(); + final @TdApi.MessageContent.Constructors int contentType = message.content.getConstructor(); final int contextId = messageListContextId; final boolean reverse = (playListFlags & PLAYLIST_FLAG_REVERSE) != 0; @@ -1632,7 +1632,7 @@ private void addNewMessage (Tdlib tdlib, TdApi.Message message) { @Override public void onNewMessage (Tdlib tdlib, TdApi.Message message) { final long chatId = getChatId(); - final int contentType = getContentType(); + final @TdApi.MessageContent.Constructors int contentType = getContentType(); if (chatId != 0 && contentType != 0 && message.chatId == chatId && message.content.getConstructor() == contentType && message.sendingState == null) { addNewMessage(tdlib, message); } @@ -1641,7 +1641,7 @@ public void onNewMessage (Tdlib tdlib, TdApi.Message message) { @Override public void onNewMessages (Tdlib tdlib, TdApi.Message[] messages) { final long chatId = getChatId(); - final int contentType = getContentType(); + final @TdApi.MessageContent.Constructors int contentType = getContentType(); if (chatId != 0 && contentType != 0) { for (TdApi.Message message : messages) { if (message.chatId == chatId && message.content.getConstructor() == contentType && message.sendingState == null) { @@ -1661,7 +1661,7 @@ public void onMessageSendSucceeded (Tdlib tdlib, final TdApi.Message message, fi } @Override - public void onMessageSendFailed (Tdlib tdlib, final TdApi.Message message, final long oldMessageId, int errorCode, String errorMessage) { + public void onMessageSendFailed (Tdlib tdlib, final TdApi.Message message, final long oldMessageId, TdApi.Error error) { moveListeners(tdlib, message, oldMessageId); } diff --git a/app/src/main/java/org/thunderdog/challegram/receiver/RefreshRateLimiter.java b/app/src/main/java/org/thunderdog/challegram/receiver/RefreshRateLimiter.java index 6bc8382d2d..591db08078 100644 --- a/app/src/main/java/org/thunderdog/challegram/receiver/RefreshRateLimiter.java +++ b/app/src/main/java/org/thunderdog/challegram/receiver/RefreshRateLimiter.java @@ -20,16 +20,24 @@ import android.os.SystemClock; import android.view.View; +import androidx.annotation.CallSuper; + import org.thunderdog.challegram.loader.ComplexReceiverUpdateListener; import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.loader.ReceiverUpdateListener; import org.thunderdog.challegram.tool.Screen; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + public class RefreshRateLimiter implements ComplexReceiverUpdateListener, ReceiverUpdateListener { private final View target; private final Handler handler; private final float maxFrameRate; + private final Set otherRefreshRateLimiters = new HashSet<>(); + public RefreshRateLimiter (View target, float maxRefreshRate) { this.target = target; this.maxFrameRate = maxRefreshRate != 0f ? maxRefreshRate : 60.0f; @@ -39,12 +47,43 @@ public RefreshRateLimiter (View target, float maxRefreshRate) { }); } + public void attachOtherRefreshLimiter (RefreshRateLimiter limiter) { + otherRefreshRateLimiters.add(limiter); + limiter.otherRefreshRateLimiters.add(this); + } + + private void forceInvalidate () { + target.invalidate(); + onInvalidatePerformed(); + if (!otherRefreshRateLimiters.isEmpty()) { + for (RefreshRateLimiter otherLimiter : otherRefreshRateLimiters) { + otherLimiter.onInvalidatePerformed(); + } + } + } + + public ReceiverUpdateListener passThroughUpdateListener () { + return receiver -> + forceInvalidate(); + } + + public ComplexReceiverUpdateListener passThroughComplexUpdateListener () { + return (key, receiver) -> + forceInvalidate(); + } + private boolean isScheduled; - private void onPerformInvalidate () { + @CallSuper + protected void onPerformInvalidate () { isScheduled = false; target.invalidate(); lastInvalidateTime = SystemClock.uptimeMillis(); + if (!otherRefreshRateLimiters.isEmpty()) { + for (RefreshRateLimiter otherLimiter : otherRefreshRateLimiters) { + otherLimiter.onInvalidatePerformed(); + } + } } private long minRefreshDelay () { @@ -53,6 +92,17 @@ private long minRefreshDelay () { private long lastInvalidateTime; + private static final int WHAT = 0; + + public void onInvalidatePerformed () { + // Call when invalidate() was called on `target` + if (isScheduled) { + isScheduled = false; + handler.removeMessages(WHAT); + lastInvalidateTime = SystemClock.uptimeMillis(); + } + } + public void invalidate () { if (isScheduled) { // Do nothing, invalidate() will happen soon. @@ -66,7 +116,7 @@ public void invalidate () { onPerformInvalidate(); } else { isScheduled = true; - handler.sendEmptyMessageDelayed(0, minDelay - timeElapsedSinceLastInvalidate); + handler.sendEmptyMessageDelayed(WHAT, minDelay - timeElapsedSinceLastInvalidate); } } diff --git a/app/src/main/java/org/thunderdog/challegram/service/AudioService.java b/app/src/main/java/org/thunderdog/challegram/service/AudioService.java index 2911c891a2..5c88ef91bf 100644 --- a/app/src/main/java/org/thunderdog/challegram/service/AudioService.java +++ b/app/src/main/java/org/thunderdog/challegram/service/AudioService.java @@ -64,6 +64,7 @@ import java.util.List; import me.vkryl.core.ArrayUtils; +import me.vkryl.td.Td; public class AudioService extends Service implements TGPlayerController.TrackListChangeListener, TGPlayerController.TrackListener, AudioController.ApicListener, AudioManager.OnAudioFocusChangeListener { @@ -134,7 +135,7 @@ public int onStartCommand (Intent intent, int flags, int startId) { // List management private static boolean isSupportedTrack (TdApi.Message message) { - return message != null && message.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR; + return message != null && Td.isAudio(message.content); } private boolean isInitialized () { diff --git a/app/src/main/java/org/thunderdog/challegram/service/PushProcessor.java b/app/src/main/java/org/thunderdog/challegram/service/PushProcessor.java index 898e14f5ca..a5981a7fd9 100644 --- a/app/src/main/java/org/thunderdog/challegram/service/PushProcessor.java +++ b/app/src/main/java/org/thunderdog/challegram/service/PushProcessor.java @@ -53,31 +53,32 @@ public PushProcessor (Context context) { this.context = context; } - public void processPush (long pushId, String payload, long sentTime, int ttl) { - Settings.instance().trackPushMessageReceived(sentTime, System.currentTimeMillis(), ttl); - - // Trying to find accountId for the push - TdApi.Object result = Client.execute(new TdApi.GetPushReceiverId(payload)); - final int accountId; - if (result instanceof TdApi.PushReceiverId) { - long pushReceiverId = ((TdApi.PushReceiverId) result).id; - accountId = Settings.instance().findAccountByReceiverId(pushReceiverId); + private static int determineAccountId (long pushId, String payload, long sentTime) { + try { + TdApi.PushReceiverId receiverId = Client.execute(new TdApi.GetPushReceiverId(payload)); + long pushReceiverId = receiverId.id; + int accountId = Settings.instance().findAccountByReceiverId(pushReceiverId); if (accountId != TdlibAccount.NO_ID) { TDLib.Tag.notifications(pushId, accountId, "Found account for receiverId: %d, payload: %s, sentTime: %d", pushReceiverId, payload, sentTime); } else { TDLib.Tag.notifications(pushId, accountId, "Couldn't find account for receiverId: %d. Sending to all accounts, payload: %s, sentTime: %d", pushReceiverId, payload, sentTime); } - } else { - accountId = TdlibAccount.NO_ID; - if (StringUtils.isEmpty(payload) || payload.equals("{}") || payload.equals("{\"badge\":\"0\"}")) { - TDLib.Tag.notifications(pushId, accountId, "Empty payload: %s, error: %s. Quitting task.", payload, TD.toErrorString(result)); - return; - } else { - TDLib.Tag.notifications(pushId, accountId, "Couldn't fetch receiverId: %s, payload: %s. Sending to all instances.", TD.toErrorString(result), payload); - } + return accountId; + } catch (Client.ExecutionException error) { + TDLib.Tag.notifications(pushId, TdlibAccount.NO_ID, "Couldn't fetch receiverId: %s, payload: %s. Sending to all instances.", TD.toErrorString(error.error), payload); + return TdlibAccount.NO_ID; } + } + + public void processPush (long pushId, String payload, long sentTime, int ttl) { + Settings.instance().trackPushMessageReceived(sentTime, System.currentTimeMillis(), ttl); - TdlibManager.instanceForAccountId(accountId).runWithWakeLock(manager -> processPush(manager, pushId, payload, accountId)); + final int accountId = determineAccountId(pushId, payload, sentTime); + if (accountId == TdlibAccount.NO_ID && (StringUtils.isEmpty(payload) || payload.equals("{}") || payload.equals("{\"badge\":\"0\"}"))) { + TDLib.Tag.notifications(pushId, accountId, "Empty payload: %s. Quitting task.", payload); + } else { + TdlibManager.instanceForAccountId(accountId).runWithWakeLock(manager -> processPush(manager, pushId, payload, accountId)); + } } private boolean hasActiveNetwork () { diff --git a/app/src/main/java/org/thunderdog/challegram/service/TGCallService.java b/app/src/main/java/org/thunderdog/challegram/service/TGCallService.java index f5c8b96889..460b76c9c2 100644 --- a/app/src/main/java/org/thunderdog/challegram/service/TGCallService.java +++ b/app/src/main/java/org/thunderdog/challegram/service/TGCallService.java @@ -526,7 +526,7 @@ public void onCallSettingsChanged (int callId, CallSettings settings) { private void acceptIncomingCall () { if (call != null) { tdlib.context().calls().acceptCall(this, tdlib, call.id); - if (UI.getUiState() != UI.STATE_RESUMED) { + if (UI.getUiState() != UI.State.RESUMED) { bringCallToFront(); } } @@ -553,10 +553,10 @@ public void onUiStateChanged (int newState) { updateCurrentState(); boolean isPendingIncoming = call != null && !call.isOutgoing && call.state.getConstructor() == TdApi.CallStatePending.CONSTRUCTOR; if (isPendingIncoming) { - if (newState != UI.STATE_RESUMED && needShowIncomingNotification) { + if (newState != UI.State.RESUMED && needShowIncomingNotification) { needShowIncomingNotification = false; showIncomingNotification(); - } else if (newState == UI.STATE_RESUMED) { + } else if (newState == UI.State.RESUMED) { needShowIncomingNotification = true; cleanupChannels((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)); U.stopForeground(this, true, TdlibNotificationManager.ID_INCOMING_CALL_NOTIFICATION); @@ -661,7 +661,7 @@ public void onAccuracyChanged (Sensor sensor, int accuracy) { private static final @DrawableRes int CALL_ICON_RES = R.drawable.baseline_phone_24_white; private void showNotification () { - boolean needNotification = call != null && (call.isOutgoing || call.state.getConstructor() == TdApi.CallStateExchangingKeys.CONSTRUCTOR || call.state.getConstructor() == TdApi.CallStateReady.CONSTRUCTOR) && !TD.isFinished(call) && UI.getUiState() != UI.STATE_RESUMED; + boolean needNotification = call != null && (call.isOutgoing || call.state.getConstructor() == TdApi.CallStateExchangingKeys.CONSTRUCTOR || call.state.getConstructor() == TdApi.CallStateReady.CONSTRUCTOR) && !TD.isFinished(call) && UI.getUiState() != UI.State.RESUMED; if (needNotification == (ongoingCallNotification != null)) { return; @@ -720,7 +720,7 @@ private void showNotification () { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { builder.setColor(tdlib.accountColor()); } - Bitmap bitmap = TdlibNotificationUtils.buildLargeIcon(tdlib, user.profilePhoto != null ? user.profilePhoto.small : null, TD.getAvatarColorId(user, tdlib.myUserId()), TD.getLetters(user), false, true); + Bitmap bitmap = TdlibNotificationUtils.buildLargeIcon(tdlib, user.profilePhoto != null ? user.profilePhoto.small : null, tdlib.cache().userAccentColor(user), TD.getLetters(user), false, true); if (bitmap != null) { builder.setLargeIcon(bitmap); } @@ -766,7 +766,7 @@ private boolean showIncomingNotification () { return needNotification; } - if (UI.getUiState() == UI.STATE_RESUMED) { + if (UI.getUiState() == UI.State.RESUMED) { needShowIncomingNotification = true; Log.i("No need to show incoming notification right now, but may in future."); return true; @@ -840,7 +840,7 @@ private boolean showIncomingNotification () { builder.setCategory(Notification.CATEGORY_CALL); builder.setFullScreenIntent(PendingIntent.getActivity(this, PendingIntent.FLAG_ONE_SHOT, Intents.valueOfCall(), Intents.mutabilityFlags(false)), true); } - Bitmap bitmap = user != null ? TdlibNotificationUtils.buildLargeIcon(tdlib, user.profilePhoto != null ? user.profilePhoto.small : null, TD.getAvatarColorId(user, tdlib.myUserId()), TD.getLetters(user), false, true) : null; + Bitmap bitmap = user != null ? TdlibNotificationUtils.buildLargeIcon(tdlib, user.profilePhoto != null ? user.profilePhoto.small : null, tdlib.cache().userAccentColor(user), TD.getLetters(user), false, true) : null; if (bitmap != null) { builder.setLargeIcon(bitmap); } @@ -894,7 +894,7 @@ private void startRinging () { if (!showIncomingNotification()) { Log.v(Log.TAG_VOIP, "Starting incall activity for incoming call"); - if (UI.getUiState() != UI.STATE_RESUMED) { + if (UI.getUiState() != UI.State.RESUMED) { bringCallToFront(); } } @@ -1139,13 +1139,19 @@ private void updateStats () { private CharSequence lastDebugLog; - private void releaseTgCalls (Tdlib tdlib, TdApi.Call call) { + private void releaseTgCalls (@Nullable Tdlib tdlib, @Nullable TdApi.Call call) { if (tgcalls != null) { + if (call == null) { + call = tgcalls.getCall(); + } + if (tdlib == null) { + tdlib = tgcalls.tdlib(); + } lastDebugLog = tgcalls.collectDebugLog(); tgcalls.performDestroy(); tgcalls = null; } - if (callListener != null) { + if (callListener != null && tdlib != null && call != null) { tdlib.listeners().unsubscribeFromCallUpdates(call.id, callListener); callListener = null; } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/CallManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/CallManager.java index 6c486c58ce..e6acef909d 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/CallManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/CallManager.java @@ -85,7 +85,7 @@ private void setCurrentCall (final Tdlib tdlib, @Nullable final TdApi.Call call) if (currentCall == null || call == null) { this.currentCallTdlib = tdlib; this.currentCall = call; - this.currentCallAcknowledged = call == null || UI.getUiState() != UI.STATE_RESUMED || UI.isNavigationBusyWithSomething(); + this.currentCallAcknowledged = call == null || UI.getUiState() != UI.State.RESUMED || UI.isNavigationBusyWithSomething(); if (currentCallAcknowledged) { notifyCallListeners(); } @@ -98,7 +98,7 @@ private void setCurrentCall (final Tdlib tdlib, @Nullable final TdApi.Call call) intent.putExtra("account_id", tdlib.id()); intent.putExtra("call_id", call.id); serviceCancellationSignal = new CancellationSignal(); - UI.startService(intent, UI.getUiState() != UI.STATE_RESUMED, true, serviceCancellationSignal); + UI.startService(intent, UI.getUiState() != UI.State.RESUMED, true, serviceCancellationSignal); navigateToCallController(currentCallTdlib, currentCall); } @@ -218,7 +218,7 @@ public void onCallSettingsChanged (Tdlib tdlib, int callId, CallSettings setting private boolean navigateToCallController (Tdlib tdlib, TdApi.Call call) { BaseActivity activity = UI.getUiContext(); - if (activity != null && activity.getActivityState() == UI.STATE_RESUMED) { + if (activity != null && activity.getActivityState() == UI.State.RESUMED) { NavigationController navigation = UI.getNavigation(); if (navigation != null) { ViewController c = !navigation.isAnimating() ? navigation.getCurrentStackItem() : null; @@ -422,20 +422,11 @@ public void makeCall (final ViewController context, final long userId, @Nulla return; } if (userFull == null) { - context.tdlib().client().send(new TdApi.GetUserFullInfo(userId), object -> { - switch (object.getConstructor()) { - case TdApi.UserFullInfo.CONSTRUCTOR: { - makeCall(context, userId, (TdApi.UserFullInfo) object, needPrompt); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.GetUserFullInfo.class, TdApi.UserFullInfo.class); - break; - } + context.tdlib().send(new TdApi.GetUserFullInfo(userId), (remoteUserFull, error) -> { + if (error != null) { + UI.showError(error); + } else { + makeCall(context, userId, remoteUserFull, needPrompt); } }); return; @@ -455,18 +446,12 @@ public void makeCall (final ViewController context, final long userId, @Nulla return; } context.context().closeAllMedia(false); - context.tdlib().client().send(new TdApi.CreateCall(userId, VoIP.getProtocol(), false), object -> { - switch (object.getConstructor()) { - case TdApi.CallId.CONSTRUCTOR: - Log.v(Log.TAG_VOIP, "#%d: call created, user_id:%d", ((TdApi.CallId) object).id, userId); - break; - case TdApi.Error.CONSTRUCTOR: - Log.e(Log.TAG_VOIP, "Failed to create call: %s", TD.toErrorString(object)); - UI.showError(object); - break; - default: - Log.unexpectedTdlibResponse(object, TdApi.CreateCall.class, TdApi.CallId.class, TdApi.Error.class); - break; + context.tdlib().send(new TdApi.CreateCall(userId, VoIP.getProtocol(), false), (callId, error) -> { + if (error != null) { + Log.e(Log.TAG_VOIP, "Failed to create call: %s", TD.toErrorString(error)); + UI.showError(error); + } else { + Log.v(Log.TAG_VOIP, "#%d: call created, user_id:%d", callId.id, userId); } }); } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/ChatFolderOptions.java b/app/src/main/java/org/thunderdog/challegram/telegram/ChatFolderOptions.java new file mode 100644 index 0000000000..a4db039ec9 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/ChatFolderOptions.java @@ -0,0 +1,26 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 19/10/2023 + */ +package org.thunderdog.challegram.telegram; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +@IntDef(value = {ChatFolderOptions.DISPLAY_AT_TOP}, flag = true) +public @interface ChatFolderOptions { + int DISPLAY_AT_TOP = 1; +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/ChatFolderStyle.java b/app/src/main/java/org/thunderdog/challegram/telegram/ChatFolderStyle.java new file mode 100644 index 0000000000..fc78afce52 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/ChatFolderStyle.java @@ -0,0 +1,26 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 17/07/2023 + */ +package org.thunderdog.challegram.telegram; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +@IntDef({ChatFolderStyle.LABEL_ONLY, ChatFolderStyle.ICON_ONLY, ChatFolderStyle.LABEL_AND_ICON}) +public @interface ChatFolderStyle { + int LABEL_ONLY = 0, ICON_ONLY = 1, LABEL_AND_ICON = 2; +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/ChatFoldersListener.java b/app/src/main/java/org/thunderdog/challegram/telegram/ChatFoldersListener.java new file mode 100644 index 0000000000..dff72a252f --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/ChatFoldersListener.java @@ -0,0 +1,21 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 17/07/2023 + */ +package org.thunderdog.challegram.telegram; + +import org.drinkless.tdlib.TdApi; + +public interface ChatFoldersListener { + default void onChatFoldersChanged (TdApi.ChatFolderInfo[] chatFolders, int mainChatListPosition) { } +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/ChatListener.java b/app/src/main/java/org/thunderdog/challegram/telegram/ChatListener.java index 2e964b944c..2c03992914 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/ChatListener.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/ChatListener.java @@ -14,6 +14,7 @@ */ package org.thunderdog.challegram.telegram; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.drinkless.tdlib.TdApi; @@ -26,6 +27,12 @@ default void onChatTitleChanged (long chatId, String title) { } default void onChatThemeChanged (long chatId, String themeName) { } default void onChatBackgroundChanged (long chatId, @Nullable TdApi.ChatBackground background) { } + default void onChatAccentColorsChanged (long chatId, + int accentColorId, long backgroundCustomEmojiId, + int profileAccentColorId, long profileBackgroundCustomEmojiId) { } + default void onChatEmojiStatusChanged (long chatId, @Nullable TdApi.EmojiStatus emojiStatus) { } + + default void onChatBackgroundCustomEmojiChanged (long chatId, long customEmojiId) { } default void onChatActionBarChanged (long chatId, TdApi.ChatActionBar actionBar) { } default void onChatPhotoChanged (long chatId, @Nullable TdApi.ChatPhotoInfo photo) { } default void onChatReadInbox (long chatId, long lastReadInboxMessageId, int unreadCount, boolean availabilityChanged) { } @@ -34,15 +41,17 @@ default void onChatHasProtectedContentChanged (long chatId, boolean hasProtected default void onChatReadOutbox (long chatId, long lastReadOutboxMessageId) { } default void onChatMarkedAsUnread (long chatId, boolean isMarkedAsUnread) { } default void onChatIsTranslatableChanged (long chatId, boolean isTranslatable) { } - default void onChatBlocked (long chatId, boolean isBlocked) { } + default void onChatBlockListChanged (long chatId, @Nullable TdApi.BlockList blockList) { } default void onChatOnlineMemberCountChanged (long chatId, int onlineMemberCount) { } default void onChatMessageTtlSettingChanged (long chatId, int messageTtlSetting) { } + default void onChatActiveStoriesChanged (@NonNull TdApi.ChatActiveStories activeStories) { } default void onChatVideoChatChanged (long chatId, TdApi.VideoChat videoChat) { } + default void onChatViewAsTopics (long chatId, boolean viewAsTopics) { } default void onChatPendingJoinRequestsChanged (long chatId, TdApi.ChatJoinRequestsInfo pendingJoinRequests) { } default void onChatReplyMarkupChanged (long chatId, long replyMarkupMessageId) { } default void onChatDraftMessageChanged (long chatId, @Nullable TdApi.DraftMessage draftMessage) { } default void onChatUnreadMentionCount(long chatId, int unreadMentionCount, boolean availabilityChanged) { } - default void onChatUnreadReactionCount(long chatId, int unreadReactionCount, boolean availabilityChanged) { } + default void onChatUnreadReactionCount (long chatId, int unreadReactionCount, boolean availabilityChanged) { } default void onChatDefaultDisableNotifications (long chatId, boolean defaultDisableNotifications) { } default void onChatDefaultMessageSenderIdChanged (long chatId, TdApi.MessageSender senderId) { } default void onChatClientDataChanged (long chatId, @Nullable String clientData) { } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/CounterChangeListener.java b/app/src/main/java/org/thunderdog/challegram/telegram/CounterChangeListener.java index a73896c181..4d1d99ed9d 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/CounterChangeListener.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/CounterChangeListener.java @@ -19,6 +19,6 @@ import org.drinkless.tdlib.TdApi; public interface CounterChangeListener { - default void onChatCounterChanged (@NonNull TdApi.ChatList chatList, boolean availabilityChanged, int totalCount, int unreadCount, int unreadUnmutedCount) { } - default void onMessageCounterChanged (@NonNull TdApi.ChatList chatList, int unreadCount, int unreadUnmutedCount) { } + default void onChatCounterChanged (@NonNull TdApi.ChatList chatList, TdlibCounter counter, boolean availabilityChanged, int totalCount, int unreadCount, int unreadUnmutedCount) { } + default void onMessageCounterChanged (@NonNull TdApi.ChatList chatList, TdlibCounter counter, int unreadCount, int unreadUnmutedCount) { } } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/DateChangeListener.java b/app/src/main/java/org/thunderdog/challegram/telegram/DateChangeListener.java new file mode 100644 index 0000000000..7c7e40cb79 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/DateChangeListener.java @@ -0,0 +1,26 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 15/02/2018 + */ +package org.thunderdog.challegram.telegram; + +import androidx.annotation.UiThread; + +public interface DateChangeListener { + @UiThread + default void onDateChanged () { } + @UiThread + default void onTimeZoneChanged () { } + @UiThread + default void onTimeChanged () { } +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/DateManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/DateManager.java new file mode 100644 index 0000000000..7c3b5cf93d --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/DateManager.java @@ -0,0 +1,131 @@ +package org.thunderdog.challegram.telegram; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import androidx.annotation.NonNull; + +import org.thunderdog.challegram.Log; +import org.thunderdog.challegram.tool.UI; + +import java.util.Calendar; + +import me.vkryl.core.DateUtils; +import me.vkryl.core.StringUtils; +import me.vkryl.core.lambda.RunnableData; +import me.vkryl.core.reference.ReferenceList; + +public class DateManager { + private final BroadcastReceiver dateChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive (Context context, Intent intent) { + String action = intent.getAction(); + if (!StringUtils.isEmpty(action)) { + DateUtils.clearCache(); + switch (action) { + case Intent.ACTION_TIMEZONE_CHANGED: + notifyTimeZoneChanged(); + UI.execute(DateManager.this::checkCurrentDate); + break; + case Intent.ACTION_DATE_CHANGED: + UI.execute(DateManager.this::checkCurrentDate); + break; + case Intent.ACTION_TIME_CHANGED: + notifyTimeChanged(); + UI.execute(DateManager.this::checkCurrentDate); + break; + } + } + } + }; + + private final TdlibManager context; + private final ReferenceList dateListeners; + + public DateManager (TdlibManager context) { + this.context = context; + this.dateListeners = new ReferenceList<>(true, true, (list, isFull) -> { + UI.post(() -> + setNeedDateWatcher(isFull) + ); + }); + } + + // Date change + + public void addListener (@NonNull DateChangeListener listener) { + this.dateListeners.add(listener); + } + + public void removeListener (@NonNull DateChangeListener listener) { + this.dateListeners.remove(listener); + } + + private void notifyListeners (RunnableData act) { + UI.execute(() -> { + for (DateChangeListener listener : dateListeners) { + act.runWithData(listener); + } + }); + } + + private void notifyDateChanged () { + notifyListeners(DateChangeListener::onDateChanged); + } + + private void notifyTimeZoneChanged () { + notifyListeners(DateChangeListener::onTimeZoneChanged); + } + + private void notifyTimeChanged () { + notifyListeners(DateChangeListener::onTimeChanged); + } + + private int currentDayOfYear, currentYear; + + private boolean setCurrentDate () { + Calendar c = Calendar.getInstance(); + int dayOfYear = c.get(Calendar.DAY_OF_YEAR); + int year = c.get(Calendar.YEAR); + if (this.currentDayOfYear != dayOfYear || this.currentYear != year) { + this.currentDayOfYear = dayOfYear; + this.currentYear = year; + return true; + } + return false; + } + + public void checkCurrentDate () { + if (setCurrentDate()) { + notifyDateChanged(); + } + } + + private boolean dateWatcherRegistered; + + private void setNeedDateWatcher (boolean needDateWatcher) { + if (this.dateWatcherRegistered != needDateWatcher) { + if (needDateWatcher) { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_DATE_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + filter.addAction(Intent.ACTION_TIME_CHANGED); + try { + UI.getAppContext().registerReceiver(dateChangeReceiver, filter); + this.dateWatcherRegistered = true; + } catch (Throwable t) { + Log.w("Unable to register date change receiver", t); + } + } else { + try { + UI.getAppContext().unregisterReceiver(dateChangeReceiver); + this.dateWatcherRegistered = false; + } catch (Throwable t) { + Log.i("Unable to unregister date change receiver", t); + } + } + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/DisplayInformation.java b/app/src/main/java/org/thunderdog/challegram/telegram/DisplayInformation.java index c749444068..fec0afff2f 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/DisplayInformation.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/DisplayInformation.java @@ -19,9 +19,6 @@ import androidx.annotation.Nullable; import org.drinkless.tdlib.TdApi; -import org.drinkmore.Tracer; -import org.thunderdog.challegram.N; -import org.thunderdog.challegram.U; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.loader.ImageFileLocal; import org.thunderdog.challegram.tool.UI; @@ -63,12 +60,14 @@ private static long buildFlags (@NonNull TdApi.User user) { private String phoneNumber; private String profilePhotoSmallPath, profilePhotoBigPath; private EmojiStatusCache emojiStatusCache; + private int accentColorId; + private TdApi.AccentColor accentColor; DisplayInformation (String prefix) { this.prefix = prefix; } - DisplayInformation (String prefix, TdApi.User user, @Nullable TdApi.Sticker emojiStatusFile, boolean isUpdate) { + DisplayInformation (String prefix, TdApi.User user, @Nullable TdApi.AccentColor accentColor, @Nullable TdApi.Sticker emojiStatusFile, boolean isUpdate) { this.prefix = prefix; this.flags = buildFlags(user); this.userId = user.id; @@ -76,6 +75,8 @@ private static long buildFlags (@NonNull TdApi.User user) { this.lastName = user.lastName; this.usernames = user.usernames; this.phoneNumber = user.phoneNumber; + this.accentColorId = user.accentColorId; + this.accentColor = accentColor; if (user.profilePhoto != null) { this.profilePhotoSmallPath = TD.isFileLoaded(user.profilePhoto.small) ? user.profilePhoto.small.local.path : @@ -118,6 +119,20 @@ public String getLastName () { return lastName; } + public int getAccentColorId () { + return accentColorId; + } + + public TdlibAccentColor getAccentColor () { + if (accentColorId < TdlibAccentColor.BUILT_IN_COLOR_COUNT) { + return new TdlibAccentColor(accentColorId); + } + if (accentColor != null) { + return new TdlibAccentColor(accentColor); + } + return new TdlibAccentColor(TdlibAccentColor.BuiltInId.RED); + } + @Nullable public TdApi.Usernames getUsernames () { return usernames; @@ -228,6 +243,20 @@ private void saveAll () { editor.putLong(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_FLAGS, flags); editor.putString(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_NAME1, firstName); editor.putString(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_NAME2, lastName); + editor.putInt(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_ACCENT_COLOR_ID, accentColorId); + if (accentColor != null) { + if (accentColor.id != accentColorId) + throw new IllegalStateException(accentColor.id + " != " + accentColorId); + editor.putInt(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_ACCENT_BUILT_IN_ACCENT_COLOR_ID, accentColor.builtInAccentColorId); + editor.putIntArray(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_LIGHT_THEME_COLORS, accentColor.lightThemeColors); + editor.putIntArray(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_DARK_THEME_COLORS, accentColor.darkThemeColors); + editor.putInt(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_MIN_CHAT_BOOST_LEVEL, accentColor.minChatBoostLevel); + } else { + editor.remove(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_ACCENT_BUILT_IN_ACCENT_COLOR_ID); + editor.remove(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_LIGHT_THEME_COLORS); + editor.remove(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_DARK_THEME_COLORS); + editor.remove(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_MIN_CHAT_BOOST_LEVEL); + } if (usernames != null) { editor .putString(prefix + Settings.KEY_ACCOUNT_INFO_SUFFIX_USERNAME, usernames.editableUsername); @@ -304,6 +333,40 @@ static DisplayInformation fullRestore (String prefix, long expectedUserId) { case Settings.KEY_ACCOUNT_INFO_SUFFIX_PHOTO_FULL: info.profilePhotoBigPath = toAbsolutePath(entry.asString()); break; + case Settings.KEY_ACCOUNT_INFO_SUFFIX_ACCENT_COLOR_ID: + info.accentColorId = entry.asInt(); + if (info.accentColor != null) { + info.accentColor.id = info.accentColorId; + } + break; + case Settings.KEY_ACCOUNT_INFO_SUFFIX_ACCENT_BUILT_IN_ACCENT_COLOR_ID: + case Settings.KEY_ACCOUNT_INFO_SUFFIX_LIGHT_THEME_COLORS: + case Settings.KEY_ACCOUNT_INFO_SUFFIX_DARK_THEME_COLORS: + case Settings.KEY_ACCOUNT_INFO_SUFFIX_MIN_CHAT_BOOST_LEVEL: + if (info.accentColor == null) { + info.accentColor = new TdApi.AccentColor( + info.accentColorId, + 0, + null, + null, + 0 + ); + } + switch (suffix) { + case Settings.KEY_ACCOUNT_INFO_SUFFIX_ACCENT_BUILT_IN_ACCENT_COLOR_ID: + info.accentColor.builtInAccentColorId = entry.asInt(); + break; + case Settings.KEY_ACCOUNT_INFO_SUFFIX_LIGHT_THEME_COLORS: + info.accentColor.lightThemeColors = entry.asIntArray(); + break; + case Settings.KEY_ACCOUNT_INFO_SUFFIX_DARK_THEME_COLORS: + info.accentColor.darkThemeColors = entry.asIntArray(); + break; + case Settings.KEY_ACCOUNT_INFO_SUFFIX_MIN_CHAT_BOOST_LEVEL: + info.accentColor.minChatBoostLevel = entry.asInt(); + break; + } + break; default: if (suffix.startsWith(Settings.KEY_ACCOUNT_INFO_SUFFIX_EMOJI_STATUS_PREFIX)) { final String subKey = suffix.substring(Settings.KEY_ACCOUNT_INFO_SUFFIX_EMOJI_STATUS_PREFIX.length()); diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/GlobalCountersListener.java b/app/src/main/java/org/thunderdog/challegram/telegram/GlobalCountersListener.java index a1f7a694da..d8114a02c0 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/GlobalCountersListener.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/GlobalCountersListener.java @@ -23,5 +23,7 @@ public interface GlobalCountersListener { @UiThread default void onUnreadCountersChanged (Tdlib tdlib, @NonNull TdApi.ChatList chatList, int count, boolean isMuted) { } @UiThread - void onTotalUnreadCounterChanged (@NonNull TdApi.ChatList chatList, boolean isReset); + default void onTotalUnreadCounterChanged (@NonNull TdApi.ChatList chatList, boolean isReset) { } + @UiThread + default void onBadgeSettingsChanged () { } } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/GlobalMessageListener.java b/app/src/main/java/org/thunderdog/challegram/telegram/GlobalMessageListener.java index 85ed10b9ad..485b36c325 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/GlobalMessageListener.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/GlobalMessageListener.java @@ -23,7 +23,7 @@ public interface GlobalMessageListener { void onMessageSendSucceeded (Tdlib tdlib, TdApi.Message message, long oldMessageId); - void onMessageSendFailed (Tdlib tdlib, TdApi.Message message, long oldMessageId, int errorCode, String errorMessage); + void onMessageSendFailed (Tdlib tdlib, TdApi.Message message, long oldMessageId, TdApi.Error error); void onMessagesDeleted (Tdlib tdlib, long chatId, long[] messageIds); } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/ListManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/ListManager.java index d6b2324fd8..1a643a3e5b 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/ListManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/ListManager.java @@ -28,7 +28,7 @@ import me.vkryl.core.lambda.Destroyable; -public abstract class ListManager implements Destroyable, Iterable { +public abstract class ListManager implements Destroyable, Iterable, TdlibProvider { private static final int STATE_INITIALIZING = 0; private static final int STATE_INITIALIZED = 1; private static final int STATE_FULL = 2; @@ -71,6 +71,12 @@ public ListManager (Tdlib tdlib, int initialLoadCount, int loadCount, boolean ca subscribeToUpdates(); } + @NonNull + @Override + public Tdlib tdlib () { + return tdlib; + } + protected void loadTotalCount (@Nullable Runnable after) { // Implement in the child if the count may desync from the list size } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/LiveLocationManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/LiveLocationManager.java index 7cfc481e05..169c4bc270 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/LiveLocationManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/LiveLocationManager.java @@ -98,7 +98,7 @@ public LiveLocationManager (TdlibManager context) { // this.context = context; this.handler = new UiHandler(this); this.helper = new LocationHelper(UI.getAppContext(), this, false, true); - this.isResumed = UI.getUiState() == UI.STATE_RESUMED; + this.isResumed = UI.getUiState() == UI.State.RESUMED; UI.addStateListener(this); } @@ -364,7 +364,7 @@ void notifyOutputMessageEdited (Tdlib tdlib, TdApi.Message message) { @Override public void onUiStateChanged (int newState) { synchronized (this) { - boolean isResumed = newState == UI.STATE_RESUMED; + boolean isResumed = newState == UI.State.RESUMED; if (this.isResumed != isResumed) { this.isResumed = isResumed; rescheduleLocationWorker(); // changes timeout diff --git a/app/src/main/java/org/thunderdog/challegram/data/MessageListManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/MessageListManager.java similarity index 88% rename from app/src/main/java/org/thunderdog/challegram/data/MessageListManager.java rename to app/src/main/java/org/thunderdog/challegram/telegram/MessageListManager.java index daf79e4b5e..9f4829123b 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/MessageListManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/MessageListManager.java @@ -10,7 +10,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thunderdog.challegram.data; +package org.thunderdog.challegram.telegram; import androidx.annotation.Nullable; import androidx.annotation.UiThread; @@ -18,9 +18,7 @@ import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.Log; -import org.thunderdog.challegram.telegram.ListManager; -import org.thunderdog.challegram.telegram.MessageListener; -import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.data.TD; import java.util.ArrayList; import java.util.Arrays; @@ -32,7 +30,7 @@ import me.vkryl.core.lambda.RunnableInt; import me.vkryl.td.Td; -public class MessageListManager extends ListManager implements MessageListener, Comparator { +public final class MessageListManager extends ListManager implements MessageListener, Comparator { public interface ChangeListener extends ListManager.ListChangeListener { } private final long chatId; @@ -119,19 +117,13 @@ private boolean hasComplexFilter () { private void fetchMessageCount (boolean local, @Nullable RunnableInt callback) { if (!hasComplexFilter() && filter != null) { - tdlib.client().send(new TdApi.GetChatMessageCount(chatId, filter, local), result -> { + tdlib.send(new TdApi.GetChatMessageCount(chatId, filter, local), (chatMessageCount, error) -> { final int count; - switch (result.getConstructor()) { - case TdApi.Count.CONSTRUCTOR: - count = ((TdApi.Count) result).count; - break; - case TdApi.Error.CONSTRUCTOR: - Log.e("GetChatMessageCount: %s, filter:%s, chatId:%s", TD.toErrorString(result), filter, chatId); - count = -1; - break; - default: - Log.unexpectedTdlibResponse(result, TdApi.GetChatMessageCount.class, TdApi.Count.class); - throw new AssertionError(result.toString()); + if (error != null) { + Log.e("GetChatMessageCount: %s, filter:%s, chatId:%s", TD.toErrorString(error), filter, chatId); + count = -1; + } else { + count = chatMessageCount.count; } if (callback != null) { runOnUiThread(() -> @@ -140,7 +132,6 @@ private void fetchMessageCount (boolean local, @Nullable RunnableInt callback) { } }); } else { - TdApi.Function function; if (hasComplexFilter()) { if (local) { if (callback != null) { @@ -148,36 +139,36 @@ private void fetchMessageCount (boolean local, @Nullable RunnableInt callback) { } return; } - function = new TdApi.SearchChatMessages(chatId, query, sender, 0, 0, 1, filter, messageThreadId); - } else { - function = new TdApi.GetChatHistory(chatId, 0, 0, 1, local); - } - tdlib.client().send(function, result -> { - final int count; - switch (result.getConstructor()) { - case TdApi.Messages.CONSTRUCTOR: { - count = ((TdApi.Messages) result).totalCount; - break; + tdlib.send(new TdApi.SearchChatMessages(chatId, query, sender, 0, 0, 1, filter, messageThreadId), (foundChatMessages, error) -> { + final int count; + if (error != null) { + Log.e("SearchChatMessages: %s, chatId: %d", TD.toErrorString(error), chatId); + count = -1; + } else { + count = foundChatMessages.totalCount; } - case TdApi.FoundChatMessages.CONSTRUCTOR: { - count = ((TdApi.FoundChatMessages) result).totalCount; - break; + if (callback != null) { + runOnUiThread(() -> + callback.runWithInt(count) + ); } - case TdApi.Error.CONSTRUCTOR: { - Log.e("%s: %s, chatId: %d", function.getClass().getSimpleName(), TD.toErrorString(result), chatId); + }); + } else { + tdlib.send(new TdApi.GetChatHistory(chatId, 0, 0, 1, local), (messages, error) -> { + final int count; + if (error != null) { + Log.e("GetChatHistory: %s, chatId: %d", TD.toErrorString(error), chatId); count = -1; - break; + } else { + count = messages.totalCount; } - default: - Log.unexpectedTdlibResponse(result, function.getClass(), TdApi.Messages.class); - throw new AssertionError(result.toString()); - } - if (callback != null) { - runOnUiThread(() -> - callback.runWithInt(count) - ); - } - }); + if (callback != null) { + runOnUiThread(() -> + callback.runWithInt(count) + ); + } + }); + } } } @@ -356,7 +347,7 @@ public void onMessageSendSucceeded (TdApi.Message message, long oldMessageId) { } @Override - public void onMessageSendFailed (TdApi.Message message, long oldMessageId, int errorCode, String errorMessage) { + public void onMessageSendFailed (TdApi.Message message, long oldMessageId, TdApi.Error error) { if (message.chatId == chatId) { runOnUiThreadIfReady(() -> replaceMessage(message, oldMessageId, CAUSE_SEND_FAILED) @@ -411,7 +402,7 @@ public void onMessagePinned (long chatId, long messageId, boolean isPinned) { removeMessageAt(index); } } - } else if (filter != null && filter.getConstructor() == TdApi.SearchMessagesFilterPinned.CONSTRUCTOR && isPinned) { + } else if (filter != null && Td.isPinnedFilter(filter) && isPinned) { fetchMessage(messageId, true); } }); diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/MessageListener.java b/app/src/main/java/org/thunderdog/challegram/telegram/MessageListener.java index 7fe4e1658b..faf758f5b3 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/MessageListener.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/MessageListener.java @@ -22,7 +22,7 @@ public interface MessageListener { default void onNewMessage (TdApi.Message message) { } default void onMessageSendAcknowledged (long chatId, long messageId) { } default void onMessageSendSucceeded (TdApi.Message message, long oldMessageId) { } - default void onMessageSendFailed (TdApi.Message message, long oldMessageId, int errorCode, String errorMessage) { } + default void onMessageSendFailed (TdApi.Message message, long oldMessageId, TdApi.Error error) { } default void onMessageContentChanged (long chatId, long messageId, TdApi.MessageContent newContent) { } default void onMessageEdited (long chatId, long messageId, int editDate, @Nullable TdApi.ReplyMarkup replyMarkup) { } default void onMessagePinned (long chatId, long messageId, boolean isPinned) { } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/NotificationSettingsListener.java b/app/src/main/java/org/thunderdog/challegram/telegram/NotificationSettingsListener.java index 8254ee3376..e11000662a 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/NotificationSettingsListener.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/NotificationSettingsListener.java @@ -21,5 +21,6 @@ default void onNotificationSettingsChanged (TdApi.NotificationSettingsScope scop default void onNotificationSettingsChanged (long chatId, TdApi.ChatNotificationSettings settings) { } default void onNotificationChannelChanged (TdApi.NotificationSettingsScope scope) { } default void onNotificationChannelChanged (long chatId) { } - default void onNotificationGlobalSettingsChanged () { } + default void onNotificationGlobalSettingsChanged () { } + default void onArchiveChatListSettingsChanged (TdApi.ArchiveChatListSettings settings) { } } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/RightId.java b/app/src/main/java/org/thunderdog/challegram/telegram/RightId.java index 5d78399cfa..b86e35d8f7 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/RightId.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/RightId.java @@ -41,6 +41,10 @@ RightId.INVITE_USERS, RightId.PIN_MESSAGES, RightId.MANAGE_VIDEO_CHATS, + RightId.POST_STORIES, + RightId.EDIT_STORIES, + RightId.DELETE_STORIES, + RightId.MANAGE_TOPICS, RightId.ADD_NEW_ADMINS, RightId.REMAIN_ANONYMOUS }) @@ -64,6 +68,10 @@ INVITE_USERS = 16, PIN_MESSAGES = 17, MANAGE_VIDEO_CHATS = 18, - ADD_NEW_ADMINS = 19, - REMAIN_ANONYMOUS = 20; + POST_STORIES = 19, + EDIT_STORIES = 20, + DELETE_STORIES = 21, + MANAGE_TOPICS = 22, + ADD_NEW_ADMINS = 23, + REMAIN_ANONYMOUS = 24; } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/SenderListManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/SenderListManager.java new file mode 100644 index 0000000000..f842acda4b --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/SenderListManager.java @@ -0,0 +1,166 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 24/09/2023 + */ +package org.thunderdog.challegram.telegram; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.Client; +import org.drinkless.tdlib.TdApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +import me.vkryl.core.collection.LongSet; +import me.vkryl.core.lambda.RunnableInt; +import me.vkryl.td.ChatId; +import me.vkryl.td.Td; + +public abstract class SenderListManager extends ListManager implements TdlibCache.UserDataChangeListener, TdlibCache.UserStatusChangeListener, TdlibCache.SupergroupDataChangeListener, ChatListener { + public interface ChangeListener extends ListManager.ListChangeListener { } + + private final LongSet senderIdsCheck = new LongSet(); + + public SenderListManager (Tdlib tdlib, int initialLoadCount, int loadCount, @Nullable ChangeListener listener) { + super(tdlib, initialLoadCount, loadCount, false, listener); + } + + @Override + protected abstract TdApi.Function nextLoadFunction (boolean reverse, int itemCount, int loadCount); + + @Override + protected void subscribeToUpdates () { + tdlib.cache().subscribeForGlobalUpdates(this); + tdlib.listeners().subscribeForGlobalUpdates(this); + } + + @Override + protected void unsubscribeFromUpdates () { + tdlib.cache().unsubscribeFromGlobalUpdates(this); + tdlib.listeners().unsubscribeFromGlobalUpdates(this); + } + + @Override + protected Response processResponse (TdApi.Object response, Client.ResultHandler retryHandler, int retryLoadCount, boolean reverse) { + final TdApi.MessageSenders senders = (TdApi.MessageSenders) response; + List sendersList = new ArrayList<>(senders.senders.length); + for (TdApi.MessageSender sender : senders.senders) { + long senderId = Td.getSenderId(sender); + if (senderIdsCheck.add(senderId)) { + sendersList.add(sender); + } + } + return new Response<>(sendersList, senders.totalCount); + } + + // Updates + + private int indexOfItem (long senderId) { + int index = 0; + for (TdApi.MessageSender sender : items) { + if (Td.getSenderId(sender) == senderId) { + return index; + } + index++; + } + return -1; + } + + private void runWithChat (long chatId, RunnableInt act) { + if (senderIdsCheck.has(chatId)) { + runOnUiThreadIfReady(() -> { + int index = indexOfItem(chatId); + if (index != -1) { + // This check is needed, because + // senderIdsCheck is modified on TDLib thread, + // but items list is modified on main thread + act.runWithInt(index); + } + }); + } + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + UpdateReason.USER_CHANGED, + UpdateReason.USER_FULL_CHANGED, + UpdateReason.USER_STATUS_CHANGED, + UpdateReason.CHAT_TITLE_CHANGED, + UpdateReason.CHAT_PHOTO_CHANGED, + UpdateReason.SUPERGROUP_UPDATED, + UpdateReason.SUPERGROUP_FULL_UPDATED + }) + public @interface UpdateReason { + int + USER_CHANGED = 1, + USER_FULL_CHANGED = 2, + USER_STATUS_CHANGED = 3, + CHAT_TITLE_CHANGED = 4, + CHAT_PHOTO_CHANGED = 5, + SUPERGROUP_UPDATED = 6, + SUPERGROUP_FULL_UPDATED = 7; + } + + private void notifyChatChanged (long chatId, @UpdateReason int reason) { + runWithChat(chatId, index -> + notifyItemChanged(index, reason) + ); + } + + private void notifyUserChanged (long userId, @UpdateReason int reason) { + notifyChatChanged(ChatId.fromUserId(userId), reason); + } + + private void notifySupergroupChanged (long supergroupId, @UpdateReason int reason) { + notifyChatChanged(ChatId.fromSupergroupId(supergroupId), reason); + } + + @Override + public void onUserUpdated (TdApi.User user) { + notifyUserChanged(user.id, UpdateReason.USER_CHANGED); + } + + @Override + public void onUserFullUpdated (long userId, TdApi.UserFullInfo userFull) { + notifyUserChanged(userId, UpdateReason.USER_FULL_CHANGED); + } + + @Override + public void onUserStatusChanged (long userId, TdApi.UserStatus status, boolean uiOnly) { + notifyUserChanged(userId, UpdateReason.USER_STATUS_CHANGED); + } + + @Override + public void onChatTitleChanged (long chatId, String title) { + notifyChatChanged(chatId, UpdateReason.CHAT_TITLE_CHANGED); + } + + @Override + public void onChatPhotoChanged (long chatId, @Nullable TdApi.ChatPhotoInfo photo) { + notifyChatChanged(chatId, UpdateReason.CHAT_PHOTO_CHANGED); + } + + @Override + public void onSupergroupUpdated (TdApi.Supergroup supergroup) { + notifySupergroupChanged(supergroup.id, UpdateReason.SUPERGROUP_UPDATED); + } + + @Override + public void onSupergroupFullUpdated (long supergroupId, TdApi.SupergroupFullInfo newSupergroupFull) { + notifySupergroupChanged(supergroupId, UpdateReason.SUPERGROUP_FULL_UPDATED); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/SortedList.java b/app/src/main/java/org/thunderdog/challegram/telegram/SortedList.java new file mode 100644 index 0000000000..1e0b182374 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/SortedList.java @@ -0,0 +1,304 @@ +package org.thunderdog.challegram.telegram; + +import androidx.annotation.AnyThread; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import me.vkryl.core.lambda.Filter; +import me.vkryl.core.lambda.RunnableBool; +import me.vkryl.core.lambda.RunnableData; +import me.vkryl.core.reference.ReferenceList; + +public abstract class SortedList implements Comparator { + public interface ListListener { + void onListChanged (SortedList list); + default void onListLoadStateChanged (SortedList list, @State int state) { + // Do nothing, as it doesn't affect the list itself + } + default void onListAvailabilityChanged (SortedList list, boolean isAvailable) { + // Do nothing, as it doesn't affect the list itself + } + default void onItemRemoved (SortedList list, T oldItem, int oldIndex) { + onListChanged(list); + } + default void onItemAdded (SortedList list, T newItem, int newIndex) { + onListChanged(list); + } + default void onItemMovedAndUpdated (SortedList list, T newItem, int newIndex, T oldItem, int oldIndex) { + onListChanged(list); + } + default void onItemUpdated (SortedList list, T newItem, int atIndex, T oldItem) { + onListChanged(list); + } + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + State.END_NOT_REACHED, + State.LOADING, + State.END_REACHED + }) + public @interface State { + int + END_NOT_REACHED = 0, + LOADING = 1, + END_REACHED = 2; + } + + protected final Tdlib tdlib; + private final ArrayList list = new ArrayList<>(); + private final ReferenceList> listeners = new ReferenceList<>(true); + private final List onLoadMore = new ArrayList<>(); + + private @State int state = State.END_NOT_REACHED; + private boolean isAvailable; + + public SortedList (Tdlib tdlib) { + this.tdlib = tdlib; + } + + @Override + public abstract int compare (T a, T b); + + protected abstract void loadMoreItems (int desiredCount, @NonNull RunnableBool after); + + protected abstract int approximateTotalItemCount (); + + // List API + + protected final void notifyApproximateTotalItemCountChanged () { + checkAvailability(); + } + + public boolean isAvailable () { + return isAvailable; + } + + public boolean canLoad () { + return state == State.END_NOT_REACHED; + } + + public boolean isEndReached () { + return state == State.END_REACHED; + } + + private void checkAvailability () { + assertTdlibThread(); + boolean isAvailable = totalCount() > 0; + if (this.isAvailable != isAvailable) { + this.isAvailable = isAvailable; + for (ListListener listener : listeners) { + listener.onListAvailabilityChanged(this, isAvailable); + } + } + } + + private void setStateUnsafe (@State int state) { + if (this.state != state) { + int oldState = this.state; + this.state = state; + for (ListListener listener : listeners) { + listener.onListLoadStateChanged(this, state); + } + if (state == State.END_REACHED || oldState == State.END_REACHED) { + checkAvailability(); + } + } + } + + public int totalCount () { + return Math.max(count(null), state != State.END_REACHED ? approximateTotalItemCount() : 0); + } + + public int count (@Nullable Filter filter) { + synchronized (list) { + if (filter == null) { + return list.size(); + } + int count = 0; + for (T item : list) { + if (filter.accept(item)) { + count++; + } + } + return count; + } + } + + private List listCopyUnsafe (@Nullable Filter filter) { + List list; + if (filter != null) { + list = new ArrayList<>(this.list.size()); + for (T item : this.list) { + if (filter.accept(item)) { + list.add(item); + } + } + } else { + list = new ArrayList<>(this.list); + } + return list; + } + + private void assertTdlibThread () { + if (!tdlib.inTdlibThread()) + throw new IllegalStateException(); + } + + @TdlibThread + public void addItem (T item) { + assertTdlibThread(); + int index = Collections.binarySearch(this.list, item, this); + if (index >= 0) + throw new IllegalStateException(); + index = (-index) - 1; + list.add(index, item); + for (ListListener listener : listeners) { + listener.onItemAdded(this, item, index); + } + checkAvailability(); + } + + @TdlibThread + public void moveItem (T newItem, T oldItem) { + assertTdlibThread(); + int oldIndex = Collections.binarySearch(this.list, oldItem, this); + if (oldIndex < 0) + throw new IllegalStateException(); + this.list.remove(oldIndex); + int newIndex = Collections.binarySearch(this.list, newItem, this); + if (newIndex >= 0) + throw new IllegalStateException(); + newIndex = (-newIndex) - 1; + list.add(newIndex, newItem); + if (newIndex != oldIndex) { + for (ListListener listener : listeners) { + listener.onItemMovedAndUpdated(this, newItem, newIndex, oldItem, oldIndex); + } + } else { + for (ListListener listener : listeners) { + listener.onItemUpdated(this, newItem, newIndex, oldItem); + } + } + } + + @TdlibThread + public final void removeItem (T oldItem) { + assertTdlibThread(); + int oldIndex = Collections.binarySearch(this.list, oldItem, this); + if (oldIndex < 0) + throw new IllegalStateException(); + this.list.remove(oldIndex); + for (ListListener listener : listeners) { + listener.onItemRemoved(this, oldItem, oldIndex); + } + checkAvailability(); + } + + @AnyThread + public void getList (@Nullable Filter filter, RunnableData> callback) { + if (!tdlib.inTdlibThread()) { + tdlib.runOnTdlibThread(() -> getList(filter, callback)); + return; + } + // No need to sync, as all changes are made on tdlib thread + List list = listCopyUnsafe(filter); + callback.runWithData(list); + } + + public void initializeList (@Nullable Filter filter, @NonNull ListListener listener, @NonNull RunnableData> callback, int initialChunkSize, @Nullable Runnable onLoadInitialChunk) { + getList(filter, list -> { + callback.runWithData(list); + listeners.add(listener); + }); + loadAtLeast(filter, initialChunkSize, onLoadInitialChunk); + } + + public void loadAtLeast (@Nullable Filter filter, int minimumCount, @Nullable Runnable after) { + loadAtLeast(filter, minimumCount, minimumCount, after); + } + + public void loadAtLeast (@Nullable Filter filter, int minimumCount, int desiredCount, @Nullable Runnable after) { + if (!tdlib.inTdlibThread()) { + tdlib.runOnTdlibThread(() -> loadAtLeast(filter, minimumCount, desiredCount, after)); + return; + } + + Runnable act = new Runnable() { + @Override + public void run () { + int count = count(filter); + if (isEndReached() || count >= minimumCount) { + if (after != null) { + after.run(); + } + return; + } + loadMore(desiredCount - count, this); + } + }; + act.run(); + } + + @AnyThread + public void loadMore (int limit, @Nullable Runnable after) { + if (!tdlib.inTdlibThread()) { + tdlib.runOnTdlibThread(() -> loadMore(limit, after)); + return; + } + if (state == State.END_REACHED) { + if (after != null) { + after.run(); + } + return; + } + if (after != null) { + onLoadMore.add(after); + } + if (state == State.LOADING) { + return; + } + setStateUnsafe(State.LOADING); + loadMoreItems(limit, endReached -> { + setStateUnsafe(endReached ? State.END_REACHED : State.END_NOT_REACHED); + if (!onLoadMore.isEmpty()) { + List callbacks = new ArrayList<>(onLoadMore); + onLoadMore.clear(); + for (Runnable runnable : callbacks) { + runnable.run(); + } + } + }); + } + + @AnyThread + public void loadAll (int chunkSize, @Nullable Runnable after) { + if (!tdlib.inTdlibThread()) { + tdlib.runOnTdlibThread(() -> loadAll(chunkSize, after)); + return; + } + Runnable act = new Runnable() { + @Override + public void run () { + if (isEndReached()) { + if (after != null) { + after.run(); + } + return; + } + loadMore(chunkSize, this); + } + }; + act.run(); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/StoryList.java b/app/src/main/java/org/thunderdog/challegram/telegram/StoryList.java new file mode 100644 index 0000000000..0226d425b1 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/StoryList.java @@ -0,0 +1,34 @@ +package org.thunderdog.challegram.telegram; + +import androidx.annotation.NonNull; + +import org.drinkless.tdlib.TdApi; + +import me.vkryl.core.lambda.RunnableBool; + +public final class StoryList extends SortedList { + private final TdApi.StoryList list; + + public StoryList (Tdlib tdlib, TdApi.StoryList list) { + super(tdlib); + this.list = list; + } + + @Override + public int compare (TdApi.ChatActiveStories a, TdApi.ChatActiveStories b) { + return tdlib.storiesComparator().compare(a, b); + } + + @Override + protected void loadMoreItems (int desiredCount, @NonNull RunnableBool after) { + tdlib.client().send(new TdApi.LoadActiveStories(list), result -> { + boolean endReached = result.getConstructor() == TdApi.Error.CONSTRUCTOR; + after.runWithBool(endReached); + }); + } + + @Override + public int approximateTotalItemCount () { + return tdlib.getStoryListChatCount(list); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/StoryListener.java b/app/src/main/java/org/thunderdog/challegram/telegram/StoryListener.java new file mode 100644 index 0000000000..b0d777af56 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/StoryListener.java @@ -0,0 +1,14 @@ +package org.thunderdog.challegram.telegram; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; + +public interface StoryListener { + void onStoryUpdated (@NonNull TdApi.Story story); + void onStoryDeleted (long storySenderChatId, int storyId); + default void onStorySendSucceeded (@NonNull TdApi.Story story, int oldStoryId) { } + default void onStorySendFailed (@NonNull TdApi.Story story, TdApi.Error error, @Nullable TdApi.CanSendStoryResult errorType) { } + default void onStoryStealthModeUpdated (int activeUntilDate, int cooldownUntilDate) { } +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java b/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java index 32b24a6b41..d6073d7139 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java @@ -20,9 +20,11 @@ import android.os.Looper; import android.os.Message; import android.os.SystemClock; +import android.util.SparseIntArray; import android.widget.Toast; import androidx.annotation.AnyThread; +import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -41,10 +43,12 @@ import org.thunderdog.challegram.R; import org.thunderdog.challegram.TDLib; import org.thunderdog.challegram.U; +import org.thunderdog.challegram.component.chat.TdlibSingleUnreadReactionsManager; import org.thunderdog.challegram.component.dialogs.ChatView; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.AvatarPlaceholder; +import org.thunderdog.challegram.data.ContentPreview; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.data.TGReaction; @@ -59,8 +63,10 @@ import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.ui.EditRightsController; import org.thunderdog.challegram.unsorted.Passcode; import org.thunderdog.challegram.unsorted.Settings; +import org.thunderdog.challegram.util.AppInstallationUtil; import org.thunderdog.challegram.util.DrawableProvider; import org.thunderdog.challegram.util.UserProvider; import org.thunderdog.challegram.util.WrapperProvider; @@ -98,7 +104,10 @@ import me.vkryl.core.BitwiseUtils; import me.vkryl.core.FileUtils; import me.vkryl.core.MathUtils; +import me.vkryl.core.ObjectUtils; import me.vkryl.core.StringUtils; +import me.vkryl.core.collection.LongList; +import me.vkryl.core.collection.LongSet; import me.vkryl.core.collection.LongSparseIntArray; import me.vkryl.core.collection.LongSparseLongArray; import me.vkryl.core.lambda.CancellableRunnable; @@ -115,7 +124,7 @@ import me.vkryl.td.Td; import me.vkryl.td.TdConstants; -public class Tdlib implements TdlibProvider, Settings.SettingsChangeListener { +public class Tdlib implements TdlibProvider, Settings.SettingsChangeListener, DateChangeListener { @Override public final int accountId () { return id(); @@ -177,7 +186,7 @@ public final Tdlib tdlib () { TdApi.JsonValue json = (TdApi.JsonValue) object; setApplicationConfig(json, JSON.stringify(json)); } else { - Log.e("getApplicationConfig failed: %s", TD.toErrorString(object)); + Log.i("getApplicationConfig failed: %s", TD.toErrorString(object)); } }; private final Client.ResultHandler messageHandler = object -> { @@ -215,24 +224,15 @@ public final Tdlib tdlib () { Log.e("TDLib Error (silenced): %s", TD.toErrorString(object)); } }; - private final Client.ResultHandler imageLoadHandler = object -> { - switch (object.getConstructor()) { - case TdApi.Ok.CONSTRUCTOR: - break; - case TdApi.File.CONSTRUCTOR: - TdApi.File file = (TdApi.File) object; - if (file.local.isDownloadingCompleted) { - ImageLoader.instance().onLoad(Tdlib.this, file); - } else if (!file.local.isDownloadingActive) { - Log.e(Log.TAG_IMAGE_LOADER, "WARNING: Image load not started"); - } - break; - case TdApi.Error.CONSTRUCTOR: - Log.e(Log.TAG_IMAGE_LOADER, "DownloadFile failed: %s", TD.toErrorString(object)); - break; - default: - Log.unexpectedTdlibResponse(object, TdApi.DownloadFile.class, TdApi.Ok.class, TdApi.Error.class); - break; + private final ResultHandler imageLoadHandler = (file, error) -> { + if (error != null) { + Log.e(Log.TAG_IMAGE_LOADER, "DownloadFile failed: %s", TD.toErrorString(error)); + } else { + if (file.local.isDownloadingCompleted) { + ImageLoader.instance().onLoad(Tdlib.this, file); + } else if (!file.local.isDownloadingActive) { + Log.e(Log.TAG_IMAGE_LOADER, "WARNING: Image load not started"); + } } }; private final Client.ResultHandler profilePhotoHandler = object -> { @@ -430,6 +430,9 @@ public long timeWasted () { private final Object clientLock = new Object(); private final Object dataLock = new Object(); private final HashMap chats = new HashMap<>(); + private final HashMap activeStories = new HashMap<>(); + private final SparseIntArray storyListChatCount = new SparseIntArray(); + private final SparseArrayCompat storyLists = new SparseArrayCompat<>(); private final HashMap forumTopicInfos = new HashMap<>(); private final HashMap chatLists = new HashMap<>(); private final StickerSet @@ -450,6 +453,8 @@ public long timeWasted () { private final TdlibWallpaperManager wallpaperManager; private final TdlibNotificationManager notificationManager; private final TdlibFileGenerationManager fileGenerationManager; + private final TdlibSingleUnreadReactionsManager unreadReactionsManager; + private final TdlibMessageViewer messageViewer; private final HashSet channels = new HashSet<>(); private final LongSparseLongArray accessibleChatTimers = new LongSparseLongArray(); @@ -460,6 +465,12 @@ public long timeWasted () { private int chatFolderMaxCount = 10, folderChosenChatMaxCount = 100; private int addedShareableChatFolderMaxCount = 2, chatFolderInviteLinkMaxCount = 3; private long chatFolderUpdatePeriod = 300; // Seconds + private int activeStoryCountMax = 100, weeklySentStoryCountMax = 700,monthlySentStoryCountMax = 3000; + private boolean canUseTextEntitiesInStoryCaptions; + private int storyCaptionLengthMax = 2048; + private int storySuggestedReactionAreaCountMax = 5; + private int storyViewersExpirationDelay = 86400; + private int storyStealhModeCooldownPeriod = 3600, storyStealthModeFuturePeriod = 1500, storyStealthModePastPeriod = 300; private boolean isPremium, isPremiumAvailable; private @GiftPremiumOption int giftPremiumOptions; private boolean suggestOnlyApiStickers; @@ -470,20 +481,23 @@ public long timeWasted () { private int pinnedChatsMaxCount = 5, pinnedArchivedChatsMaxCount = 100, pinnedForumTopicMaxCount = 5; private int favoriteStickersMaxCount = 5; private double emojiesAnimatedZoom = .75f; - private boolean youtubePipDisabled, qrLoginCamera, dialogFiltersTooltip, dialogFiltersEnabled; + private boolean youtubePipDisabled, qrLoginCamera, dialogFiltersTooltip, dialogFiltersEnabled, forceUrgentInAppUpdate; private String qrLoginCode; private String[] diceEmoji, activeEmojiReactions; private TdApi.ReactionType defaultReactionType; private final Map cachedReactions = new HashMap<>(); private boolean callsEnabled = true, expectBlocking, isLocationVisible; private boolean canIgnoreSensitiveContentRestrictions, ignoreSensitiveContentRestrictions; - private boolean canArchiveAndMuteNewChatsFromUnknownUsers, archiveAndMuteNewChatsFromUnknownUsers; + private boolean canArchiveAndMuteNewChatsFromUnknownUsers; private RtcServer[] rtcServers; private long unixTime; private long unixTimeReceived; private long utcTimeOffset; + private int storyStealthModeActiveUntilDate; + private int storyStealthModeCooldownUntilDate; + private Boolean disableTopChats; private boolean disableSentScheduledMessageNotifications; private long antiSpamBotUserId; @@ -643,6 +657,12 @@ public TdlibCounter getCounter (@NonNull TdApi.ChatList chatList) { Log.v("INITIALIZATION: Tdlib.fileGenerationManager -> %dms", SystemClock.uptimeMillis() - ms); ms = SystemClock.uptimeMillis(); } + this.messageViewer = new TdlibMessageViewer(this); + if (needMeasure) { + Log.v("INITIALIZATION: Tdlib.messageViewer -> %dms", SystemClock.uptimeMillis() - ms); + ms = SystemClock.uptimeMillis(); + } + this.unreadReactionsManager = new TdlibSingleUnreadReactionsManager(this); this.applicationConfigJson = settings().getApplicationConfig(); if (!StringUtils.isEmpty(applicationConfigJson)) { TdApi.JsonValue value = JSON.parse(applicationConfigJson); @@ -1138,13 +1158,19 @@ public TdApi.AuthorizationState authorizationState () { return authorizationState; } - public void signOut () { + public boolean switchToNextAuthorizedAccount () { if (context().preferredAccountId() == accountId) { int nextAccountId = context().findNextAccountId(accountId); if (nextAccountId != TdlibAccount.NO_ID) { context().changePreferredAccountId(nextAccountId, TdlibManager.SWITCH_REASON_UNAUTHORIZED); + return true; } } + return false; + } + + public void signOut () { + switchToNextAuthorizedAccount(); boolean isMulti = context().isMultiUser(); String name = isMulti ? TD.getUserName(account().getFirstName(), account().getLastName()) : null; incrementReferenceCount(REFERENCE_TYPE_JOB); @@ -1330,6 +1356,7 @@ private void updateAuthState (ClientHolder context, TdApi.AuthorizationState new if (newStatus != Status.READY) { startupPerformed = false; } + setNeedTimeZoneListener(newStatus != Status.UNKNOWN); if (prevStatus == Status.UNKNOWN && newStatus != prevStatus) { onInitialized(); } @@ -1354,6 +1381,29 @@ private void updateAuthState (ClientHolder context, TdApi.AuthorizationState new } } + private boolean needTimeZoneListener; + + private void setNeedTimeZoneListener (boolean needTimeZoneListener) { + if (this.needTimeZoneListener != needTimeZoneListener) { + this.needTimeZoneListener = needTimeZoneListener; + if (needTimeZoneListener) { + context.dateManager().addListener(this); + } else { + context.dateManager().removeListener(this); + } + } + } + + @Override + public void onTimeChanged () { + updateUtcTimeOffset(); + } + + @Override + public void onTimeZoneChanged () { + updateUtcTimeOffset(); + } + private static TdApi.FormattedText makeUpdateText (String version, String changeLog) { String text = Lang.getStringSecure(R.string.ChangeLogText, version, changeLog); TdApi.FormattedText formattedText = new TdApi.FormattedText(text, null); @@ -1372,9 +1422,9 @@ private static void makeUpdateText (int major, int agesSinceBirthdate, int month } }*/ TdApi.FormattedText text = makeUpdateText(String.format(Locale.US, "%d.%d.%d.%d", major, agesSinceBirthdate, monthsSinceLastBirthday, buildNo), changeLogUrl); - functions.add(new TdApi.GetWebPagePreview(text)); + functions.add(new TdApi.GetWebPagePreview(text, new TdApi.LinkPreviewOptions(false, changeLogUrl, false, false, false))); functions.add(new TdApi.GetWebPageInstantView(changeLogUrl, false)); - messages.add(new TdApi.InputMessageText(text, false, false)); + messages.add(new TdApi.InputMessageText(text, null, false)); } private boolean isOptimizing; @@ -1418,8 +1468,10 @@ private static boolean checkVersion (int version, int checkVersion, boolean isTe return Status.UNAUTHORIZED; case TdApi.AuthorizationStateReady.CONSTRUCTOR: return Status.READY; + default: + Td.assertAuthorizationState_6e5056de(); + throw Td.unsupported(state); } - throw new UnsupportedOperationException(state.toString()); } public boolean checkChangeLogs (boolean alreadySent, boolean test) { @@ -1482,7 +1534,10 @@ public boolean checkChangeLogs (boolean alreadySent, boolean test) { makeUpdateText(0, 25, 6, APP_RELEASE_VERSION_2023_APRIL, "https://telegra.ph/Telegram-X-04-02", functions, updates, false); } if (checkVersion(prevVersion, APP_RELEASE_VERSION_2023_AUGUST, test)) { - makeUpdateText(0, 25, 10, APP_RELEASE_VERSION_2023_AUGUST, "https://telegra.ph/Telegram-X-08-02", functions, updates, true); + makeUpdateText(0, 25, 10, APP_RELEASE_VERSION_2023_AUGUST, "https://telegra.ph/Telegram-X-08-02", functions, updates, false); + } + if (checkVersion(prevVersion, APP_RELEASE_VERSION_2023_DECEMBER, test)) { + makeUpdateText(0, 25, 10, APP_RELEASE_VERSION_2023_DECEMBER, "https://telegra.ph/Telegram-X-2023-12-31", functions, updates, true); } if (!updates.isEmpty()) { incrementReferenceCount(REFERENCE_TYPE_JOB); // starting task @@ -1507,7 +1562,7 @@ public boolean checkChangeLogs (boolean alreadySent, boolean test) { } }; for (TdApi.InputMessageContent content : updates) { - client().send(new TdApi.AddLocalMessage(chatId, new TdApi.MessageSenderUser(TdConstants.TELEGRAM_ACCOUNT_ID) /*TODO: @tgx_android?*/, 0, true, content), localMessageHandler); + client().send(new TdApi.AddLocalMessage(chatId, new TdApi.MessageSenderUser(TdConstants.TELEGRAM_ACCOUNT_ID) /*TODO: @tgx_android?*/, null, true, content), localMessageHandler); } } }; @@ -1542,6 +1597,7 @@ public boolean checkChangeLogs (boolean alreadySent, boolean test) { private static final int APP_RELEASE_VERSION_2023_MARCH_2 = 1615; // Bugfixes to the previous release. 15 March, 2023: https://t.me/tgx_android/305 private static final int APP_RELEASE_VERSION_2023_APRIL = 1624; // Emoji 15.0, more recent stickers & more + critical TDLIb upgrade. 2 April, 2023: https://telegra.ph/Telegram-X-04-02 private static final int APP_RELEASE_VERSION_2023_AUGUST = 1646; // Translation, Advanced Text Formatting, Emoji Status, tgcalls, reproducible TDLib & more. 3 August, 2023: https://telegra.ph/Telegram-X-08-02 + private static final int APP_RELEASE_VERSION_2023_DECEMBER = 1674; // Custom emoji, select link preview, archive settings, in-app avatar picker, group chat tools, & more. 31st December, 2023 (full roll-out in January 2024): https://telegra.ph/Telegram-X-2023-12-31 // Startup @@ -1622,6 +1678,10 @@ public int id () { return accountId; } + public boolean isProduction () { + return instanceMode == Mode.NORMAL; + } + private boolean isDebugInstance () { return instanceMode == Mode.DEBUG; } @@ -1728,11 +1788,42 @@ public Client client () { // TODO migrate all tdlib.client().send(..) to tdlib.s return clientHolder().client; } - public void send (TdApi.Function function, Client.ResultHandler handler) { - client().send(function, handler); + public interface ResultHandler { + void onResult (T result, @Nullable TdApi.Error error); + + static Client.ResultHandler toTdlibHandler (ResultHandler handler) { + return result -> { + if (result.getConstructor() == TdApi.Error.CONSTRUCTOR) { + handler.onResult(null, (TdApi.Error) result); + } else { + //noinspection unchecked + handler.onResult((T) result, null); + } + }; + } + } + + public void send (TdApi.Function function, ResultHandler handler) { + send(client(), function, handler); + } + + public void sendAll (TdApi.Function[] functions, @NonNull ResultHandler handler, @Nullable Runnable after) { + sendAll(functions, ResultHandler.toTdlibHandler(handler), after); + } + + public static void send (Client client, TdApi.Function function, ResultHandler handler) { + send(client, function, ResultHandler.toTdlibHandler(handler)); + } + + private void send (TdApi.Function function, Client.ResultHandler handler) { + send(client(), function, handler); + } + + private static void send (Client client, TdApi.Function function, Client.ResultHandler handler) { + client.send(function, handler); } - public void sendAll (TdApi.Function[] functions, Client.ResultHandler handler, @Nullable Runnable after) { + public void sendAll (TdApi.Function[] functions, @NonNull Client.ResultHandler handler, @Nullable Runnable after) { if (functions.length == 0) { if (after != null) { after.run(); @@ -1760,7 +1851,7 @@ public void sendAll (TdApi.Function[] functions, Client.ResultHandler handler remaining = null; actualHandler = handler; } - for (TdApi.Function function : functions) { + for (TdApi.Function function : functions) { send(function, actualHandler); } } @@ -1826,10 +1917,12 @@ public void runOnTdlibThread (@NonNull Runnable runnable, double timeoutSeconds, } public void searchContacts (@Nullable String searchQuery, int limit, Client.ResultHandler handler) { + Log.ensureReturnType(TdApi.SearchContacts.class, TdApi.Users.class); client().send(new TdApi.SearchContacts(searchQuery, limit), handler); } public void loadMoreChats (@NonNull TdApi.ChatList chatList, int limit, Client.ResultHandler handler) { + Log.ensureReturnType(TdApi.LoadChats.class, TdApi.Ok.class); client().send(new TdApi.LoadChats(chatList, limit), handler); } @@ -2147,6 +2240,23 @@ public TdApi.Object clientExecute (TdApi.Function function, long timeoutMs) { return clientExecute(function, timeoutMs, true); } + public T clientExecuteT (TdApi.Function function, boolean throwErrors) throws TdlibException { + return clientExecuteT(function, 0, throwErrors); + } + + public T clientExecuteT (TdApi.Function function, long timeoutMs, boolean throwErrors) throws TdlibException { + TdApi.Object result = clientExecute(function, timeoutMs); + if (result instanceof TdApi.Error) { + if (throwErrors) { + throw new TdlibException((TdApi.Error) result); + } + Log.i("clientExecute %s failed: %s", function.getClass().getName(), TD.toErrorString(result)); + return null; + } + //noinspection unchecked + return (T) result; + } + @Nullable private TdApi.Object clientExecute (TdApi.Function function, long timeoutMs, boolean requiresTdlibInitialization) { if (inTdlibThread()) @@ -2236,6 +2346,10 @@ public TdlibFileGenerationManager filegen () { return fileGenerationManager; } + public TdlibMessageViewer messageViewer () { + return messageViewer; + } + public TdlibQuickAckManager qack () { return quickAckManager; } @@ -2297,6 +2411,14 @@ public Client.ResultHandler okHandler () { }; } + public ResultHandler typedOkHandler () { + return (ok, error) -> { + if (error != null) { + UI.showError(error); + } + }; + } + public Client.ResultHandler okHandler (@Nullable Runnable after) { return after != null ? object -> { switch (object.getConstructor()) { @@ -2310,6 +2432,26 @@ public Client.ResultHandler okHandler (@Nullable Runnable after) { } : okHandler(); } + public ResultHandler typedOkHandler (@Nullable Runnable after) { + return after != null ? (ok, error) -> { + if (error != null) { + UI.showError(error); + } else { + tdlib().ui().post(after); + } + } : typedOkHandler(); + } + + public ResultHandler successHandler (@Nullable Runnable after) { + return (data, error) -> { + if (error != null) { + UI.showError(error); + } else { + tdlib().ui().post(after); + } + }; + } + public Client.ResultHandler doneHandler () { return doneHandler; } @@ -2339,7 +2481,7 @@ public Client.ResultHandler messageHandler () { return messageHandler; } - public Client.ResultHandler imageLoadHandler () { + public ResultHandler imageLoadHandler () { return imageLoadHandler; } @@ -2404,7 +2546,7 @@ public void loadChats (long[] chatIds, @Nullable Runnable after) { if (chatIds == null || chatIds.length == 0) return; if (after != null) { - int[] counter = new int[]{chatIds.length}; + int[] counter = new int[] {chatIds.length}; for (long chatId : chatIds) { client().send(new TdApi.GetChat(chatId), result -> { silentHandler.onResult(result); @@ -2433,6 +2575,7 @@ public boolean chatOnline (long chatId) { case TdApi.ChatTypeBasicGroup.CONSTRUCTOR: break; default: + Td.assertChatType_e562ec7d(); throw new UnsupportedOperationException(Long.toString(chatId)); } return false; @@ -2637,6 +2780,16 @@ public void stickerSet (String name, RunnableData callback) { } } + public boolean isAnonymousAdmin (long chatId) { + TdApi.ChatMemberStatus status = chatStatus(chatId); + return status != null && Td.isAnonymous(status); + } + + public boolean isAnonymousAdminNonCreator (long chatId) { + TdApi.ChatMemberStatus status = chatStatus(chatId); + return status != null && Td.isAnonymous(status) && !TD.isCreator(status); + } + public @Nullable TdApi.ChatMemberStatus chatStatus (long chatId) { if (chatId == 0) { return null; @@ -2722,18 +2875,26 @@ public boolean canClearHistory (long chatId) { return chatId != 0 && canClearHistory(chat(chatId)); } - public boolean canClearHistoryForEveryone (long chatId) { - return chatId != 0 && canClearHistoryForEveryone(chat(chatId)); - } - public boolean canClearHistory (TdApi.Chat chat) { return chat != null && chat.lastMessage != null && (chat.canBeDeletedOnlyForSelf || chat.canBeDeletedForAllUsers); } - public boolean canClearHistoryForEveryone (TdApi.Chat chat) { + public boolean canClearHistoryForAllUsers (long chatId) { + return chatId != 0 && canClearHistoryForAllUsers(chat(chatId)); + } + + public boolean canClearHistoryForAllUsers (TdApi.Chat chat) { return chat != null && chat.lastMessage != null && chat.canBeDeletedForAllUsers; } + public boolean canClearHistoryOnlyForSelf (long chatId) { + return chatId != 0 && canClearHistoryOnlyForSelf(chat(chatId)); + } + + public boolean canClearHistoryOnlyForSelf (TdApi.Chat chat) { + return chat != null && chat.lastMessage != null && chat.canBeDeletedOnlyForSelf; + } + public boolean canAddToOtherChat (TdApi.Chat chat) { TdApi.User user = chatUser(chat); if (user == null) { @@ -2748,14 +2909,6 @@ public boolean canAddToOtherChat (TdApi.Chat chat) { return false; } - /*public boolean hasWritePermission (long chatId) { - return chatId != 0 && hasWritePermission(chat(chatId)); - } - - public boolean hasWritePermission (TdApi.Chat chat) { - return getRestrictionStatus(chat, R.id.right_sendMessages) == null; - }*/ - public @Nullable TdApi.SecretChat chatToSecretChat (long chatId) { int secretChatId = ChatId.toSecretChatId(chatId); return secretChatId != 0 ? cache().secretChat(secretChatId) : null; @@ -2787,27 +2940,6 @@ public boolean hasWritePermission (TdApi.Chat chat) { return avatarFile; } - public int chatAvatarColorId (long chatId) { - if (chatId == 0) { - return TD.getAvatarColorId(-1, myUserId()); - } - switch (ChatId.getType(chatId)) { - case TdApi.ChatTypeBasicGroup.CONSTRUCTOR: - return TD.getAvatarColorId(-ChatId.toBasicGroupId(chatId), myUserId()); - case TdApi.ChatTypeSupergroup.CONSTRUCTOR: { - return TD.getAvatarColorId(-ChatId.toSupergroupId(chatId), myUserId()); - } - case TdApi.ChatTypePrivate.CONSTRUCTOR: - return cache().userAvatarColorId(ChatId.toUserId(chatId)); - case TdApi.ChatTypeSecret.CONSTRUCTOR: { - int secretChatId = ChatId.toSecretChatId(chatId); - TdApi.SecretChat secretChat = secretChatId != 0 ? cache().secretChat(secretChatId) : null; - return cache().userAvatarColorId(secretChat != null ? secretChat.userId : 0); - } - } - throw new RuntimeException(); - } - public AvatarPlaceholder chatPlaceholder (TdApi.Chat chat, boolean allowSavedMessages, float radius, @Nullable DrawableProvider provider) { return new AvatarPlaceholder(radius, chatPlaceholderMetadata(chat, allowSavedMessages), provider); } @@ -2823,8 +2955,14 @@ public AvatarPlaceholder.Metadata chatPlaceholderMetadata (long chatId, boolean public AvatarPlaceholder.Metadata chatPlaceholderMetadata (long chatId, @Nullable TdApi.Chat chat, boolean allowSavedMessages) { if (chat != null || chatId == 0) { return chatPlaceholderMetadata(chat, allowSavedMessages); + } else if (allowSavedMessages && isSelfChat(chatId)) { + return new AvatarPlaceholder.Metadata(accentColor(TdlibAccentColor.InternalId.SAVED_MESSAGES)); + } else if (isRepliesChat(chatId)) { + return new AvatarPlaceholder.Metadata(accentColor(TdlibAccentColor.InternalId.REPLIES)); + } else if (isDeletedAccountChat(chatId)) { + return new AvatarPlaceholder.Metadata(accentColor(TdlibAccentColor.InternalId.INACTIVE)); } else { - return new AvatarPlaceholder.Metadata(chatAvatarColorId(chatId)); + return new AvatarPlaceholder.Metadata(chatAccentColor(chatId)); } } @@ -2832,15 +2970,15 @@ public AvatarPlaceholder.Metadata chatPlaceholderMetadata (@Nullable TdApi.Chat if (chat == null) { return null; } - Letters avatarLetters = null; - int avatarColorId; + TdlibAccentColor accentColor; + Letters avatarLetters; int desiredDrawableRes = 0; int extraDrawableRes = 0; if (isUserChat(chat)) { long userId = chatUserId(chat); return cache().userPlaceholderMetadata(userId, cache().user(userId), allowSavedMessages); } else { - avatarColorId = chatAvatarColorId(chat.id); + accentColor = chatAccentColor(chat); avatarLetters = chatLetters(chat); switch (chat.type.getConstructor()) { case TdApi.ChatTypeBasicGroup.CONSTRUCTOR: @@ -2851,7 +2989,7 @@ public AvatarPlaceholder.Metadata chatPlaceholderMetadata (@Nullable TdApi.Chat break; } } - return new AvatarPlaceholder.Metadata(avatarColorId, avatarLetters != null ? avatarLetters.text : null, desiredDrawableRes, extraDrawableRes); + return new AvatarPlaceholder.Metadata(accentColor, avatarLetters, desiredDrawableRes, extraDrawableRes); } public Letters chatLetters (TdApi.Chat chat) { @@ -2886,26 +3024,103 @@ public Letters chatLetters (long chatId) { return TD.getLetters(); } - public int chatAvatarColorId (TdApi.Chat chat) { - if (chat != null) { - switch (chat.type.getConstructor()) { - case TdApi.ChatTypeSupergroup.CONSTRUCTOR: { - return TD.getAvatarColorId(-((TdApi.ChatTypeSupergroup) chat.type).supergroupId, myUserId()); - } - case TdApi.ChatTypeBasicGroup.CONSTRUCTOR: { - return TD.getAvatarColorId(-((TdApi.ChatTypeBasicGroup) chat.type).basicGroupId, myUserId()); - } - case TdApi.ChatTypeSecret.CONSTRUCTOR: - case TdApi.ChatTypePrivate.CONSTRUCTOR: { - long userId = chatUserId(chat); - if (isSelfUserId(userId)) { - return ColorId.avatarSavedMessages; - } - return cache().userAvatarColorId(userId); + public int chatAccentColorId (long chatId) { + if (chatId == 0) { + return TdlibAccentColor.InternalId.INACTIVE; + } + if (isRepliesChat(chatId)) { + return TdlibAccentColor.InternalId.REPLIES; + } + switch (ChatId.getType(chatId)) { + case TdApi.ChatTypeBasicGroup.CONSTRUCTOR: + case TdApi.ChatTypeSupergroup.CONSTRUCTOR: { + TdApi.Chat chat = chat(chatId); + if (chat != null) { + return chat.accentColorId; + } else { + return TdlibAccentColor.InternalId.INACTIVE; } } + case TdApi.ChatTypePrivate.CONSTRUCTOR: + case TdApi.ChatTypeSecret.CONSTRUCTOR: { + long userId = chatUserId(chatId); + return cache().userAccentColorId(userId); + } + default: { + Td.assertChatType_e562ec7d(); + throw new UnsupportedOperationException(Long.toString(chatId)); + } + } + } + + public int chatAccentColorId (@Nullable TdApi.Chat chat) { + if (chat == null) { + return TdlibAccentColor.InternalId.INACTIVE; + } + if (isRepliesChat(chat.id)) { + return TdlibAccentColor.InternalId.REPLIES; + } + switch (chat.type.getConstructor()) { + case TdApi.ChatTypeBasicGroup.CONSTRUCTOR: + case TdApi.ChatTypeSupergroup.CONSTRUCTOR: { + return chat.accentColorId; + } + case TdApi.ChatTypePrivate.CONSTRUCTOR: + case TdApi.ChatTypeSecret.CONSTRUCTOR: { + long userId = chatUserId(chat); + return cache().userAccentColorId(userId); + } + default: { + Td.assertChatType_e562ec7d(); + throw Td.unsupported(chat.type); + } + } + } + + public TdlibAccentColor messageAccentColor (@NonNull TdApi.Message message) { + if (message.forwardInfo != null) { + switch (message.forwardInfo.origin.getConstructor()) { + case TdApi.MessageOriginChat.CONSTRUCTOR: + return chatAccentColor(((TdApi.MessageOriginChat) message.forwardInfo.origin).senderChatId); + case TdApi.MessageOriginUser.CONSTRUCTOR: + return cache().userAccentColor(((TdApi.MessageOriginUser) message.forwardInfo.origin).senderUserId); + case TdApi.MessageOriginChannel.CONSTRUCTOR: + return chatAccentColor(((TdApi.MessageOriginChannel) message.forwardInfo.origin).chatId); + case TdApi.MessageOriginHiddenUser.CONSTRUCTOR: + return null; + default: + Td.assertMessageOrigin_f2224a59(); + throw Td.unsupported(message.forwardInfo.origin); + } + } + return senderAccentColor(message.senderId); + } + + public TdlibAccentColor chatAccentColor (long chatId) { + int accentColorId = chatAccentColorId(chatId); + return accentColor(accentColorId); + } + + public TdlibAccentColor chatAccentColor (@Nullable TdApi.Chat chat) { + int accentColorId = chatAccentColorId(chat); + return accentColor(accentColorId); + } + + public TdlibAccentColor senderAccentColor (TdApi.MessageSender sender) { + switch (sender.getConstructor()) { + case TdApi.MessageSenderUser.CONSTRUCTOR: { + TdApi.MessageSenderUser user = (TdApi.MessageSenderUser) sender; + return cache().userAccentColor(user.userId); + } + case TdApi.MessageSenderChat.CONSTRUCTOR: { + TdApi.MessageSenderChat chat = (TdApi.MessageSenderChat) sender; + return chatAccentColor(chat.chatId); + } + default: { + Td.assertMessageSender_439d4c9c(); + throw Td.unsupported(sender); + } } - return TD.getAvatarColorId(-1, 0); } public String messageAuthor (TdApi.Message message) { @@ -2929,12 +3144,12 @@ public String messageAuthor (TdApi.Message message, boolean allowSignature, bool return null; if (message.forwardInfo != null) { switch (message.forwardInfo.origin.getConstructor()) { - case TdApi.MessageForwardOriginUser.CONSTRUCTOR: { - long userId = ((TdApi.MessageForwardOriginUser) message.forwardInfo.origin).senderUserId; + case TdApi.MessageOriginUser.CONSTRUCTOR: { + long userId = ((TdApi.MessageOriginUser) message.forwardInfo.origin).senderUserId; return shorten ? cache().userFirstName(userId) : cache().userName(userId); } - case TdApi.MessageForwardOriginChannel.CONSTRUCTOR: { - TdApi.MessageForwardOriginChannel info = (TdApi.MessageForwardOriginChannel) message.forwardInfo.origin; + case TdApi.MessageOriginChannel.CONSTRUCTOR: { + TdApi.MessageOriginChannel info = (TdApi.MessageOriginChannel) message.forwardInfo.origin; if (allowSignature && !StringUtils.isEmpty(info.authorSignature)) return info.authorSignature; TdApi.Chat chat = chat(info.chatId); @@ -2942,8 +3157,8 @@ public String messageAuthor (TdApi.Message message, boolean allowSignature, bool return chat.title; break; } - case TdApi.MessageForwardOriginChat.CONSTRUCTOR: { - TdApi.MessageForwardOriginChat info = (TdApi.MessageForwardOriginChat) message.forwardInfo.origin; + case TdApi.MessageOriginChat.CONSTRUCTOR: { + TdApi.MessageOriginChat info = (TdApi.MessageOriginChat) message.forwardInfo.origin; if (allowSignature && !StringUtils.isEmpty(info.authorSignature)) return info.authorSignature; TdApi.Chat chat = chat(info.senderChatId); @@ -2951,9 +3166,12 @@ public String messageAuthor (TdApi.Message message, boolean allowSignature, bool return chat.title; break; } - case TdApi.MessageForwardOriginHiddenUser.CONSTRUCTOR: - case TdApi.MessageForwardOriginMessageImport.CONSTRUCTOR: + case TdApi.MessageOriginHiddenUser.CONSTRUCTOR: break; + default: { + Td.assertMessageOrigin_f2224a59(); + throw Td.unsupported(message.forwardInfo.origin); + } } } if (message.senderId == null) @@ -3064,8 +3282,11 @@ public TdApi.ChatPhoto chatPhoto (long chatId, boolean allowRequest) { TdApi.BasicGroupFullInfo basicGroupFullInfo = basicGroupId != 0 ? cache().basicGroupFull(basicGroupId, allowRequest) : null; return basicGroupFullInfo != null ? basicGroupFullInfo.photo : null; } + default: { + Td.assertChatType_e562ec7d(); + throw new UnsupportedOperationException(Long.toString(chatId)); + } } - throw new UnsupportedOperationException(Long.toString(chatId)); } public TdApi.MessageSender sender (long chatId) { @@ -3081,6 +3302,12 @@ public boolean isSelfSender (TdApi.Message message) { return message != null && (message.isOutgoing || isSelfSender(message.senderId)); } + public boolean senderContactOrCloseFirend (TdApi.MessageSender sender) { + long userId = Td.getSenderUserId(sender); + TdApi.User user = userId != 0 ? cache().user(userId) : null; + return user != null && (user.isContact || user.isCloseFriend); + } + public @Nullable TdApi.User chatUser (long chatId) { long userId = chatUserId(chatId); if (userId != 0) { @@ -3095,24 +3322,24 @@ public boolean chatUserDeleted (TdApi.Chat chat) { return user != null && user.type.getConstructor() == TdApi.UserTypeDeleted.CONSTRUCTOR; } - public boolean chatForum (long chatId) { + public boolean isForum (long chatId) { TdApi.Supergroup supergroup = chatToSupergroup(chatId); return supergroup != null && supergroup.isForum; } - public boolean chatBlocked (TdApi.Chat chat) { - return chat != null && chatBlocked(chat.id); + public @Nullable TdApi.BlockList chatBlockList (TdApi.Chat chat) { + return chat != null ? chatBlockList(chat.id) : null; } - public boolean chatBlocked (long chatId) { + public @Nullable TdApi.BlockList chatBlockList (long chatId) { TdApi.Chat chat = chat(chatId); - return chat != null && chat.isBlocked; + return chat != null ? chat.blockList : null; } - public boolean userBlocked (long userId) { - return chatBlocked(ChatId.fromUserId(userId)); + public boolean chatFullyBlocked (long chatId) { + TdApi.BlockList blockList = chatBlockList(chatId); + return blockList != null && blockList.getConstructor() == TdApi.BlockListMain.CONSTRUCTOR; } - @Nullable public String chatUsername (TdApi.Chat chat) { TdApi.Usernames usernames = chatUsernames(chat); @@ -3137,8 +3364,11 @@ public TdApi.Usernames chatUsernames (TdApi.Chat chat) { long supergroupId = ((TdApi.ChatTypeSupergroup) chat.type).supergroupId; return cache().supergroupUsernames(supergroupId); } + default: { + Td.assertChatType_e562ec7d(); + throw Td.unsupported(chat.type); + } } - throw new UnsupportedOperationException(chat.type.toString()); } @Nullable @@ -3166,8 +3396,11 @@ public TdApi.Usernames chatUsernames (long chatId) { long supergroupId = ChatId.toSupergroupId(chatId); return cache().supergroupUsernames(supergroupId); } + default: { + Td.assertChatType_e562ec7d(); + throw new UnsupportedOperationException(Long.toString(chatId)); + } } - throw new UnsupportedOperationException(Long.toString(chatId)); } @Nullable @@ -3262,11 +3495,29 @@ public boolean chatPinned (TdApi.ChatList chatList, long chatId) { } } - public TdApi.ChatFolderInfo chatFilterInfo (int chatFilterId) { + public int chatFoldersCount () { + synchronized (dataLock) { + return chatFolders.length; + } + } + + public TdApi.ChatFolderInfo[] chatFolders () { + synchronized (dataLock) { + return chatFolders; + } + } + + public int mainChatListPosition () { + synchronized (dataLock) { + return mainChatListPosition; + } + } + + public TdApi.ChatFolderInfo chatFolderInfo (int chatFolderId) { synchronized (dataLock) { if (chatFolders != null) { for (TdApi.ChatFolderInfo filter : chatFolders) { - if (filter.id == chatFilterId) + if (filter.id == chatFolderId) return filter; } } @@ -3274,6 +3525,12 @@ public TdApi.ChatFolderInfo chatFilterInfo (int chatFilterId) { return null; } + public boolean hasFolders () { + synchronized (dataLock) { + return chatFolders != null && chatFolders.length > 0; + } + } + public boolean canArchiveChat (TdApi.ChatList chatList, TdApi.Chat chat) { if (chat == null) return false; @@ -3426,23 +3683,52 @@ public long senderUserId (TdApi.Message msg) { return 0; } if (isSelfChat(msg.chatId)) { - if (msg.forwardInfo != null && msg.forwardInfo.origin.getConstructor() == TdApi.MessageForwardOriginUser.CONSTRUCTOR) - return ((TdApi.MessageForwardOriginUser) msg.forwardInfo.origin).senderUserId; + if (msg.forwardInfo != null && msg.forwardInfo.origin.getConstructor() == TdApi.MessageOriginUser.CONSTRUCTOR) + return ((TdApi.MessageOriginUser) msg.forwardInfo.origin).senderUserId; } return Td.getSenderUserId(msg); } + public String sponsorName (TdApi.MessageSponsor sponsor) { + switch (sponsor.type.getConstructor()) { + case TdApi.MessageSponsorTypeBot.CONSTRUCTOR: { + TdApi.MessageSponsorTypeBot bot = (TdApi.MessageSponsorTypeBot) sponsor.type; + return cache().userName(bot.botUserId); + } + case TdApi.MessageSponsorTypeWebApp.CONSTRUCTOR: { + TdApi.MessageSponsorTypeWebApp webApp = (TdApi.MessageSponsorTypeWebApp) sponsor.type; + return webApp.webAppTitle; + } + case TdApi.MessageSponsorTypePublicChannel.CONSTRUCTOR: { + TdApi.MessageSponsorTypePublicChannel publicChannel = (TdApi.MessageSponsorTypePublicChannel) sponsor.type; + return chatTitle(publicChannel.chatId); + } + case TdApi.MessageSponsorTypePrivateChannel.CONSTRUCTOR: { + TdApi.MessageSponsorTypePrivateChannel privateChannel = (TdApi.MessageSponsorTypePrivateChannel) sponsor.type; + return privateChannel.title; + } + case TdApi.MessageSponsorTypeWebsite.CONSTRUCTOR: { + TdApi.MessageSponsorTypeWebsite website = (TdApi.MessageSponsorTypeWebsite) sponsor.type; + return website.name; + } + default: + Td.assertMessageSponsorType_cdabde01(); + throw Td.unsupported(sponsor.type); + } + } + public String senderName (TdApi.Message msg, boolean allowForward, boolean shorten) { long authorId = Td.getMessageAuthorId(msg, allowForward); - if (authorId == 0 && allowForward && msg.forwardInfo != null) { - switch (msg.forwardInfo.origin.getConstructor()) { - case TdApi.MessageForwardOriginHiddenUser.CONSTRUCTOR: - return ((TdApi.MessageForwardOriginHiddenUser) msg.forwardInfo.origin).senderName; - case TdApi.MessageForwardOriginMessageImport.CONSTRUCTOR: - return ((TdApi.MessageForwardOriginMessageImport) msg.forwardInfo.origin).senderName; - default: + if (authorId == 0 && allowForward) { + if (msg.forwardInfo != null) { + if (msg.forwardInfo.origin.getConstructor() == TdApi.MessageOriginHiddenUser.CONSTRUCTOR) { + return ((TdApi.MessageOriginHiddenUser) msg.forwardInfo.origin).senderName; + } else { authorId = Td.getMessageAuthorId(msg, false); - break; + } + } + if (msg.importInfo != null && authorId == 0) { + return msg.importInfo.senderName; } } if (ChatId.isUserChat(authorId)) { @@ -3506,7 +3792,8 @@ public boolean senderPremium (TdApi.MessageSender sender) { userId = ((TdApi.MessageSenderUser) sender).userId; break; default: - throw new UnsupportedOperationException(sender.toString()); + Td.assertMessageSender_439d4c9c(); + throw Td.unsupported(sender); } TdApi.User user = cache().user(userId); return user != null && user.isPremium; @@ -3784,6 +4071,11 @@ public boolean isSupportChat (TdApi.Chat chat) { return user != null && user.isSupport; } + public boolean isDeletedAccountChat (long chatId) { + TdApi.User user = chatUser(chatId); + return TD.isUserDeleted(user); + } + public boolean chatVerified (TdApi.Chat chat) { if (chat == null) { return false; @@ -3832,6 +4124,10 @@ public boolean chatFake (TdApi.Chat chat) { return false; } + public boolean chatRestricted (long chatId) { + return !StringUtils.isEmpty(chatRestrictionReason(chatId)); + } + public boolean chatRestricted (TdApi.Chat chat) { return !StringUtils.isEmpty(chatRestrictionReason(chat)); } @@ -4262,23 +4558,26 @@ private void closeChatImpl (long chatId, ViewController controller) { } public void onScreenshotTaken (int timeSeconds) { - synchronized (chatOpenMutex) { + /* synchronized (chatOpenMutex) { final int size = openedChatsTimes.size(); for (int i = 0; i < size; i++) { final int openTime = openedChatsTimes.valueAt(i); if (timeSeconds >= openTime) { final long chatId = openedChatsTimes.keyAt(i); TdApi.Chat chat = chat(chatId); - if (/*hasWritePermission(chat) && */(ChatId.isSecret(chatId) || ui().shouldSendScreenshotHint(chat))) { - sendScreenshotMessage(chatId); + if ((ChatId.isSecret(chatId) || ui().shouldSendScreenshotHint(chat))) { + sendScreenshotMessage(chatId, null); } } } - } + }*/ + ui().execute(() -> + messageViewer.onScreenshotTaken(timeSeconds) + ); } - public boolean hasOpenChats () { - return openedChats != null && openedChats.size() > 0; + public boolean hasPotentiallyVisibleMessages () { + return (openedChats != null && openedChats.size() > 0) || messageViewer.hasPotentiallyVisibleMessages(); } // Metadata @@ -4329,49 +4628,49 @@ public String accountName () { // Actions - public void sendScreenshotMessage (long chatId) { - client().send(new TdApi.SendChatScreenshotTakenNotification(chatId), messageHandler()); + public void sendScreenshotMessage (long chatId, long[] messageIds) { + client().send(new TdApi.ViewMessages(chatId, messageIds, new TdApi.MessageSourceScreenshot(), false), messageHandler()); } - public void sendMessage (long chatId, long messageThreadId, long replyToMessageId, TdApi.MessageSendOptions options, TdApi.Animation animation) { + public void sendMessage (long chatId, long messageThreadId, @Nullable TdApi.InputMessageReplyTo replyTo, TdApi.MessageSendOptions options, TdApi.Animation animation) { TdApi.InputMessageContent inputMessageContent = new TdApi.InputMessageAnimation(new TdApi.InputFileId(animation.animation.id), null, null, animation.duration, animation.width, animation.height, null, false); - sendMessage(chatId, messageThreadId, replyToMessageId, options, inputMessageContent); + sendMessage(chatId, messageThreadId, replyTo, options, inputMessageContent); } - public void sendMessage (long chatId, long messageThreadId, long replyToMessageId, TdApi.MessageSendOptions options, TdApi.Audio audio) { + public void sendMessage (long chatId, long messageThreadId, @Nullable TdApi.InputMessageReplyTo replyTo, TdApi.MessageSendOptions options, TdApi.Audio audio) { TdApi.InputMessageContent inputMessageContent = new TdApi.InputMessageAudio(new TdApi.InputFileId(audio.audio.id), null, audio.duration, audio.title, audio.performer, null); - sendMessage(chatId, messageThreadId, replyToMessageId, options, inputMessageContent); + sendMessage(chatId, messageThreadId, replyTo, options, inputMessageContent); } - public void sendMessage (long chatId, long messageThreadId, long replyToMessageId, TdApi.MessageSendOptions options, TdApi.Sticker sticker, @Nullable String emoji) { + public void sendMessage (long chatId, long messageThreadId, @Nullable TdApi.InputMessageReplyTo replyTo, TdApi.MessageSendOptions options, TdApi.Sticker sticker, @Nullable String emoji) { TdApi.InputMessageContent inputMessageContent = new TdApi.InputMessageSticker(new TdApi.InputFileId(sticker.sticker.id), null, 0, 0, emoji); - sendMessage(chatId, messageThreadId, replyToMessageId, options, inputMessageContent); + sendMessage(chatId, messageThreadId, replyTo, options, inputMessageContent); } - public void sendMessage (long chatId, long messageThreadId, long replyToMessageId, TdApi.MessageSendOptions options, TdApi.InputMessageContent inputMessageContent) { - sendMessage(chatId, messageThreadId, replyToMessageId, options, inputMessageContent, null); + public void sendMessage (long chatId, long messageThreadId, @Nullable TdApi.InputMessageReplyTo replyTo, TdApi.MessageSendOptions options, TdApi.InputMessageContent inputMessageContent) { + sendMessage(chatId, messageThreadId, replyTo, options, inputMessageContent, null); } - public void sendMessage (long chatId, long messageThreadId, long replyToMessageId, TdApi.MessageSendOptions options, TdApi.InputMessageContent inputMessageContent, @Nullable RunnableData after) { - client().send(new TdApi.SendMessage(chatId, messageThreadId, replyToMessageId, options, null, inputMessageContent), after != null ? result -> { + public void sendMessage (long chatId, long messageThreadId, @Nullable TdApi.InputMessageReplyTo replyTo, TdApi.MessageSendOptions options, TdApi.InputMessageContent inputMessageContent, @Nullable RunnableData after) { + client().send(new TdApi.SendMessage(chatId, messageThreadId, replyTo, options, null, inputMessageContent), after != null ? result -> { messageHandler.onResult(result); after.runWithData(result instanceof TdApi.Message ? (TdApi.Message) result : null); } : messageHandler()); } public void resendMessages (long chatId, long[] messageIds) { - client().send(new TdApi.ResendMessages(chatId, messageIds), messageHandler()); + client().send(new TdApi.ResendMessages(chatId, messageIds, null), messageHandler()); } private final HashMap pendingMessageTexts = new HashMap<>(); private final HashMap pendingMessageCaptions = new HashMap<>(); public void editMessageText (long chatId, long messageId, TdApi.InputMessageText content, @Nullable TdApi.WebPage webPage) { - if (content.disableWebPagePreview) { + if (content.linkPreviewOptions != null && content.linkPreviewOptions.isDisabled) { webPage = null; } TD.parseEntities(content.text); - TdApi.MessageText messageText = new TdApi.MessageText(content.text, webPage); + TdApi.MessageText messageText = new TdApi.MessageText(content.text, webPage, content.linkPreviewOptions); if (!Emoji.instance().isSingleEmoji(content.text)) { performEdit(chatId, messageId, messageText, new TdApi.EditMessageText(chatId, messageId, null, content), pendingMessageTexts); return; @@ -4379,7 +4678,7 @@ public void editMessageText (long chatId, long messageId, TdApi.InputMessageText long customEmojiId = 0; if (content.text.entities != null) { for (TdApi.TextEntity entity : content.text.entities) { - if (entity.type.getConstructor() == TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR) { + if (Td.isCustomEmoji(entity.type)) { customEmojiId = ((TdApi.TextEntityTypeCustomEmoji) entity.type).customEmojiId; break; } @@ -4427,13 +4726,15 @@ public TdApi.FormattedText getFormattedText (TdApi.Message message) { public TdApi.FormattedText getPendingFormattedText (long chatId, long messageId) { TdApi.MessageContent messageText = getPendingMessageText(chatId, messageId); if (messageText != null) { + //noinspection SwitchIntDef switch (messageText.getConstructor()) { case TdApi.MessageText.CONSTRUCTOR: return ((TdApi.MessageText) messageText).text; case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: return Td.textOrCaption(messageText); } - throw new UnsupportedOperationException(Integer.toString(messageText.getConstructor())); + Td.assertMessageContent_d40af239(); + throw Td.unsupported(messageText); } return getPendingMessageCaption(chatId, messageId); } @@ -4588,11 +4889,11 @@ public void getTesterLevel (@NonNull RunnableInt callback, boolean onlyLocal) { } public void forwardMessage (long chatId, long messageThreadId, long fromChatId, long messageId, TdApi.MessageSendOptions options) { - client().send(new TdApi.ForwardMessages(chatId, messageThreadId, fromChatId, new long[] {messageId}, options, false, false, false), messageHandler()); + client().send(new TdApi.ForwardMessages(chatId, messageThreadId, fromChatId, new long[] {messageId}, options, false, false), messageHandler()); } - public void sendInlineQueryResult (long chatId, long messageThreadId, long replyToMessageId, TdApi.MessageSendOptions options, long queryId, String resultId) { - client().send(new TdApi.SendInlineQueryResultMessage(chatId, messageThreadId, replyToMessageId, options, queryId, resultId, false), messageHandler()); + public void sendInlineQueryResult (long chatId, long messageThreadId, @Nullable TdApi.InputMessageReplyTo replyTo, TdApi.MessageSendOptions options, long queryId, String resultId) { + client().send(new TdApi.SendInlineQueryResultMessage(chatId, messageThreadId, replyTo, options, queryId, resultId, false), messageHandler()); } public void sendBotStartMessage (long botUserId, long chatId, String parameter) { @@ -4603,7 +4904,7 @@ public void setChatMessageAutoDeleteTime (long chatId, int ttl) { client().send(new TdApi.SetChatMessageAutoDeleteTime(chatId, ttl), okHandler()); } - public void getPrimaryChatInviteLink (long chatId, Client.ResultHandler handler) { + public void getPrimaryChatInviteLink (long chatId, Tdlib.ResultHandler handler) { Client.ResultHandler linkHandler = new Client.ResultHandler() { @Override public void onResult (TdApi.Object result) { @@ -4618,15 +4919,16 @@ public void onResult (TdApi.Object result) { break; } case TdApi.ChatInviteLink.CONSTRUCTOR: + handler.onResult((TdApi.ChatInviteLink) result, null); + return; case TdApi.Error.CONSTRUCTOR: - handler.onResult(result); + handler.onResult(null, (TdApi.Error) result); return; default: - Log.unexpectedTdlibResponse(result, TdApi.ReplacePrimaryChatInviteLink.class, TdApi.ChatInviteLink.class); - return; + throw new UnsupportedOperationException(result.toString()); } if (inviteLink != null) { - handler.onResult(inviteLink); + handler.onResult(inviteLink, null); } else { client().send(new TdApi.ReplacePrimaryChatInviteLink(chatId), this); } @@ -4642,7 +4944,7 @@ public void onResult (TdApi.Object result) { break; } default: { - handler.onResult(new TdApi.Error(-1, "Invalid chat type")); + handler.onResult(null, new TdApi.Error(-1, "Invalid chat type")); break; } } @@ -4715,8 +5017,12 @@ public int chatAccessState (TdApi.Chat chat) { return CHAT_ACCESS_OK; } - public void blockSender (TdApi.MessageSender sender, boolean block, Client.ResultHandler handler) { - client().send(new TdApi.ToggleMessageSenderIsBlocked(sender, block), handler); + public void blockSender (TdApi.MessageSender sender, @Nullable TdApi.BlockList blockList, Client.ResultHandler handler) { + client().send(new TdApi.SetMessageSenderBlockList(sender, blockList), handler); + } + + public void unblockSender (TdApi.MessageSender sender, Client.ResultHandler handler) { + blockSender(sender, null, handler); } public void setScopeNotificationSettings (TdApi.NotificationSettingsScope scope, TdApi.ScopeNotificationSettings settings) { @@ -5274,29 +5580,23 @@ public void syncLanguage (@NonNull TdApi.LanguagePackInfo info, @Nullable Runnab } private void syncLanguage (@NonNull String languagePackId, @Nullable RunnableBool callback) { - client().send(new TdApi.SynchronizeLanguagePack(languagePackId), result -> { + send(new TdApi.SynchronizeLanguagePack(languagePackId), (ok, error) -> { boolean success; - switch (result.getConstructor()) { - case TdApi.Ok.CONSTRUCTOR: - Log.v("%s language is successfully synchronized", languagePackId); - success = true; - break; - case TdApi.Error.CONSTRUCTOR: - Log.e("Unable to synchronize languagePackId %s: %s", languagePackId, TD.toErrorString(result)); - success = languagePackId.equals(Lang.getBuiltinLanguagePackId()); - if (!success) { - success = Config.NEED_LANGUAGE_WORKAROUND; + if (error != null) { + Log.e("Unable to synchronize languagePackId %s: %s", languagePackId, TD.toErrorString(error)); + success = languagePackId.equals(Lang.getBuiltinLanguagePackId()); + if (!success) { + success = Config.NEED_LANGUAGE_WORKAROUND; + UI.showError(error); + /*if (success = Config.NEED_LANGUAGE_WORKAROUND) { + UI.showToast("Warning: language not synced. It's temporary issue of current beta version. " + TD.makeErrorString(result), Toast.LENGTH_LONG); + } else { UI.showError(result); - /*if (success = Config.NEED_LANGUAGE_WORKAROUND) { - UI.showToast("Warning: language not synced. It's temporary issue of current beta version. " + TD.makeErrorString(result), Toast.LENGTH_LONG); - } else { - UI.showError(result); - }*/ - } - break; - default: - Log.unexpectedTdlibResponse(result, TdApi.SynchronizeLanguagePack.class, TdApi.Ok.class, TdApi.Error.class); - return; + }*/ + } + } else { + Log.v("%s language is successfully synchronized", languagePackId); + success = true; } if (callback != null) { callback.runWithBool(success); @@ -5462,31 +5762,43 @@ private Map newConnectionParams () { if (deviceToken != null && (state == TdlibManager.TokenState.NONE || state == TdlibManager.TokenState.INITIALIZING)) { state = TdlibManager.TokenState.OK; } + String tokenProvider = TdlibNotificationUtils.getTokenRetriever().getName(); String error = context().getTokenError(); switch (state) { case TdlibManager.TokenState.ERROR: { - params.put("device_token", "FIREBASE_ERROR"); + params.put("device_token", tokenProvider.toUpperCase() + "_ERROR"); if (!StringUtils.isEmpty(error)) { - params.put("firebase_error", error); + params.put(tokenProvider + "_error", error); } break; } case TdlibManager.TokenState.INITIALIZING: { - params.put("device_token", "FIREBASE_INITIALIZING"); + params.put("device_token", tokenProvider.toUpperCase() + "_INITIALIZING"); break; } case TdlibManager.TokenState.OK: { - switch (deviceToken.getConstructor()) { - // TODO more push services - case TdApi.DeviceTokenFirebaseCloudMessaging.CONSTRUCTOR: { - String token = ((TdApi.DeviceTokenFirebaseCloudMessaging) deviceToken).token; - params.put("device_token", token); + String tokenOrEndpoint; + switch (ObjectUtils.requireNonNull(deviceToken).getConstructor()) { + case TdApi.DeviceTokenFirebaseCloudMessaging.CONSTRUCTOR: + tokenOrEndpoint = ((TdApi.DeviceTokenFirebaseCloudMessaging) deviceToken).token; + break; + case TdApi.DeviceTokenHuaweiPush.CONSTRUCTOR: { + tokenOrEndpoint = ((TdApi.DeviceTokenHuaweiPush) deviceToken).token; + final String huaweiTokenPrefix = "huawei://"; + if (tokenOrEndpoint.startsWith(huaweiTokenPrefix)) { + tokenOrEndpoint = huaweiTokenPrefix + tokenOrEndpoint; + } break; } + case TdApi.DeviceTokenSimplePush.CONSTRUCTOR: + tokenOrEndpoint = ((TdApi.DeviceTokenSimplePush) deviceToken).endpoint; + break; default: { - throw new UnsupportedOperationException(deviceToken.toString()); + Td.assertDeviceToken_de4a4f61(); + throw Td.unsupported(deviceToken); } } + params.put("device_token", tokenOrEndpoint); break; } case TdlibManager.TokenState.NONE: @@ -5495,15 +5807,19 @@ private Map newConnectionParams () { throw new IllegalStateException(Integer.toString(state)); } } - long timeZoneOffset = TimeUnit.MILLISECONDS.toSeconds( - TimeZone.getDefault().getRawOffset() + - TimeZone.getDefault().getDSTSavings() - ); + long timeZoneOffset = timeZoneOffset(); params.put("package_id", UI.getAppContext().getPackageName()); - String installerName = U.getInstallerPackageName(); + String installerName = AppInstallationUtil.getInstallerPackageName(); if (!StringUtils.isEmpty(installerName)) { params.put("installer", installerName); } + String initiatorName = AppInstallationUtil.getInitiatorPackageName(); + if (!StringUtils.isEmpty(initiatorName) && !initiatorName.equals(installerName)) { + params.put("initiator", initiatorName); + } + if (BuildConfig.DEBUG) { + params.put("debug", true); + } String fingerprint = U.getApkFingerprint("SHA1", false); if (!StringUtils.isEmpty(fingerprint)) { params.put("data", fingerprint); @@ -5537,6 +5853,22 @@ private Map newConnectionParams () { return params; } + private void updateUtcTimeOffset () { + performOptional(client -> { + long timeZoneOffset = timeZoneOffset(); + if (this.utcTimeOffset != timeZoneOffset) { + client.send(new TdApi.SetOption("utc_time_offset", new TdApi.OptionValueInteger(timeZoneOffset)), silentHandler()); + } + }, null); + } + + public static long timeZoneOffset () { + return TimeUnit.MILLISECONDS.toSeconds( + TimeZone.getDefault().getRawOffset() + + TimeZone.getDefault().getDSTSavings() + ); + } + private void checkConnectionParams (Client client, boolean force) { Map params = newConnectionParams(); String connectionParams = JSON.stringify(JSON.toObject(params)); @@ -5570,7 +5902,7 @@ private void updateParameters (Client client) { updateNotificationParameters(client); client.send(new TdApi.SetOption("storage_max_files_size", new TdApi.OptionValueInteger(Integer.MAX_VALUE)), okHandler); client.send(new TdApi.SetOption("ignore_default_disable_notification", new TdApi.OptionValueBoolean(true)), okHandler); - client.send(new TdApi.SetOption("ignore_platform_restrictions", new TdApi.OptionValueBoolean(U.isAppSideLoaded())), okHandler); + client.send(new TdApi.SetOption("ignore_platform_restrictions", new TdApi.OptionValueBoolean(AppInstallationUtil.isAppSideLoaded())), okHandler); } checkConnectionParams(client, true); @@ -5601,6 +5933,10 @@ private void setApplicationConfig (TdApi.JsonValue config, String json) { } } + public boolean hasUrgentInAppUpdate () { + return forceUrgentInAppUpdate; + } + private void processApplicationConfig (TdApi.JsonValue config) { if (!(config instanceof TdApi.JsonValueObject)) return; @@ -5612,6 +5948,9 @@ private void processApplicationConfig (TdApi.JsonValue config) { case "test": // Nothing to do? break; + case "force_inapp_update": + this.forceUrgentInAppUpdate = member.value instanceof TdApi.JsonValueBoolean && ((TdApi.JsonValueBoolean) member.value).value; + break; case "ios_disable_parallel_channel_reset": case "small_queue_max_active_operations_count": // Number case "large_queue_max_active_operations_count": // Number @@ -5813,33 +6152,22 @@ public void removeProxies (int excludeProxyId) { public void getProxyLink (@NonNull Settings.Proxy proxy, RunnableData callback) { if (proxy.proxy == null) throw new IllegalArgumentException(); - client().send(new TdApi.AddProxy(proxy.proxy.server, proxy.proxy.port, false, proxy.proxy.type), object -> { - switch (object.getConstructor()) { - case TdApi.Proxy.CONSTRUCTOR: { - int tdlibProxyId = ((TdApi.Proxy) object).id; - client().send(new TdApi.GetProxyLink(tdlibProxyId), httpUrl -> { - String url; - switch (httpUrl.getConstructor()) { - case TdApi.HttpUrl.CONSTRUCTOR: - url = ((TdApi.HttpUrl) httpUrl).url; - break; - case TdApi.Error.CONSTRUCTOR: - Log.e("Proxy link unavailable: %s", TD.toErrorString(httpUrl)); - url = null; - break; - default: - Log.unexpectedTdlibResponse(httpUrl, TdApi.GetProxyLink.class, TdApi.HttpUrl.class, TdApi.Error.class); - return; - } - ui().post(() -> callback.runWithData(url)); - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - ui().post(() -> callback.runWithData(null)); - break; - } + send(new TdApi.AddProxy(proxy.proxy.server, proxy.proxy.port, false, proxy.proxy.type), (tdlibProxy, error) -> { + if (error != null) { + UI.showError(error); + ui().post(() -> callback.runWithData(null)); + } else { + int tdlibProxyId = tdlibProxy.id; + send(new TdApi.GetProxyLink(tdlibProxyId), (httpUrl, error1) -> { + String url; + if (error1 != null) { + Log.e("Proxy link unavailable: %s", TD.toErrorString(error1)); + url = null; + } else { + url = httpUrl.url; + } + ui().post(() -> callback.runWithData(url)); + }); } }); } @@ -6312,13 +6640,6 @@ private void setDisableSentScheduledMessageNotificationsImpl (boolean disableSen } } - private void setArchiveAndMuteNewChatsFromUnknownUsersImpl (boolean archiveAndMuteNewChatsFromUnknownUsers) { - if (this.archiveAndMuteNewChatsFromUnknownUsers != archiveAndMuteNewChatsFromUnknownUsers) { - this.archiveAndMuteNewChatsFromUnknownUsers = archiveAndMuteNewChatsFromUnknownUsers; - listeners().updateArchiveAndMuteChatsFromUnknownUsersEnabled(archiveAndMuteNewChatsFromUnknownUsers); - } - } - /*public boolean disablePinnedMessageNotifications () { return disablePinnedMessageNotifications; } @@ -6366,17 +6687,6 @@ public void setIgnoreSensitiveContentRestrictions (boolean ignoreSensitiveConten } } - public boolean autoArchiveEnabled () { - return archiveAndMuteNewChatsFromUnknownUsers; - } - - public void setAutoArchiveEnabled (boolean enabled) { - if (this.archiveAndMuteNewChatsFromUnknownUsers != enabled) { - this.archiveAndMuteNewChatsFromUnknownUsers = enabled; - client().send(new TdApi.SetOption("archive_and_mute_new_chats_from_unknown_users", new TdApi.OptionValueBoolean(enabled)), okHandler()); - } - } - public String uniqueSuffix () { return accountId + "." + authorizationDate; } @@ -6418,7 +6728,7 @@ public double emojiesAnimatedZoom () { } public boolean youtubePipEnabled () { - return !youtubePipDisabled || U.isAppSideLoaded(); + return !youtubePipDisabled || AppInstallationUtil.isAppSideLoaded(); } public RtcServer[] rtcServers () { @@ -6430,7 +6740,7 @@ public boolean autoArchiveAvailable () { } public String tMeUrl () { - return StringUtils.isEmpty(tMeUrl) ? "https://" + TD.getTelegramMeHost() + "/" : tMeUrl; + return StringUtils.isEmpty(tMeUrl) ? "https://" + TdConstants.TME_HOSTS[0] + "/" : tMeUrl; } public String tMeMessageUrl (String username, long messageId) { @@ -6481,6 +6791,17 @@ public String tMeStartUrl (String botUsername, String parameter, boolean inGroup .toString(); } + public String tMeChatUrl (long chatId) { + String username = chatUsername(chatId); + if (!StringUtils.isEmpty(username)) { + return tMeUrl(username); + } + if (ChatId.isSupergroup(chatId)) { + return tMeUrl("c/" + ChatId.toSupergroupId(chatId)); + } + return null; + } + public String tMeBackgroundUrl (String backgroundId) { return tMeUrl("bg/" + backgroundId); } @@ -6493,6 +6814,20 @@ public String tMeLanguageUrl (String languagePackId) { return tMeUrl("setlanguage/" + languagePackId); } + public String tMeStickerSetUrl (@NonNull TdApi.StickerSetInfo stickerSetInfo) { + switch (stickerSetInfo.stickerType.getConstructor()) { + case TdApi.StickerTypeCustomEmoji.CONSTRUCTOR: + return tMeUrl("addemoji/" + stickerSetInfo.name); + case TdApi.StickerTypeMask.CONSTRUCTOR: + case TdApi.StickerTypeRegular.CONSTRUCTOR: + return tMeUrl("addstickers/" + stickerSetInfo.name); + default: { + Td.assertStickerType_cc811bb7(); + throw Td.unsupported(stickerSetInfo.stickerType); + } + } + } + public String tMeHost () { return StringUtils.urlWithoutProtocol(tMeUrl()); } @@ -6665,6 +7000,14 @@ public boolean suggestOnlyApiStickers () { public int maxMessageTextLength () { return maxMessageTextLength; } + + public long chatFolderCountMax () { + return chatFolderMaxCount; + } + + public long chatFolderChosenChatCountMax () { + return folderChosenChatMaxCount; + } public long telegramAntiSpamUserId () { return antiSpamBotUserId; @@ -6849,6 +7192,9 @@ private void resetContextualData () { // chats.clear(); resetChatsData(); activeCalls.clear(); + activeStories.clear(); + storyLists.clear(); + storyStealthModeActiveUntilDate = storyStealthModeCooldownUntilDate = 0; accessibleChatTimers.clear(); chatOnlineMemberCount.clear(); myProfilePhoto = null; @@ -6980,7 +7326,7 @@ private TdApi.Sticker findExplicitDiceEmoji (int value) { matchedLanguageLevel = 2; } else if (diceEmoji.equals(builtinLanguageEmoji)) { matchedLanguageLevel = 1; - } else if (diceEmoji.equals(TD.EMOJI_DICE.textRepresentation)) { + } else if (diceEmoji.equals(ContentPreview.EMOJI_DICE.textRepresentation)) { stickerValue = 1; } else if (diceEmoji.equals(numberEmoji)) { stickerValue = value; @@ -7010,13 +7356,13 @@ private TdApi.Sticker findExplicitDiceEmoji (int value) { explicitDice = animatedDiceExplicit.find(Lang.getBuiltinLanguageEmoji()); if (explicitDice != null) return explicitDice; - explicitDice = animatedDiceExplicit.find(TD.EMOJI_DICE.textRepresentation); + explicitDice = animatedDiceExplicit.find(ContentPreview.EMOJI_DICE.textRepresentation); return explicitDice; } @Nullable public TdApi.DiceStickers findDiceEmoji (String emoji, int value, TdApi.DiceStickers defaultValue) { - if (TD.EMOJI_DICE.textRepresentation.equals(emoji)) { + if (ContentPreview.EMOJI_DICE.textRepresentation.equals(emoji)) { TdApi.Sticker explicitDice = findExplicitDiceEmoji(value); if (explicitDice != null) return new TdApi.DiceStickersRegular(explicitDice); @@ -7141,7 +7487,7 @@ private void updateMessageSendSucceeded (TdApi.UpdateMessageSendSucceeded update } private void updateMessageSendFailed (TdApi.UpdateMessageSendFailed update) { - UI.showError(new TdApi.Error(update.errorCode, update.errorMessage)); + UI.showError(update.error); synchronized (dataLock) { Settings.instance().updateScrollMessageId(accountId, update.message.chatId, update.oldMessageId, update.message.id); } @@ -7235,18 +7581,21 @@ private void updateMessageInteractionInfo (TdApi.UpdateMessageInteractionInfo up @TdlibThread private void updateMessageUnreadReactions (TdApi.UpdateMessageUnreadReactions update) { final boolean counterChanged, availabilityChanged; + final TdApi.Chat chat; + final TdlibChatList[] chatLists; synchronized (dataLock) { - final TdApi.Chat chat = chats.get(update.chatId); + chat = chats.get(update.chatId); if (TdlibUtils.assertChat(update.chatId, chat, update)) { return; } availabilityChanged = (chat.unreadReactionCount > 0) != (update.unreadReactionCount > 0); counterChanged = chat.unreadReactionCount != update.unreadReactionCount; chat.unreadReactionCount = update.unreadReactionCount; + chatLists = counterChanged || availabilityChanged ? chatListsImpl(chat.positions) : null; } - listeners.updateMessageUnreadReactions(update, counterChanged, availabilityChanged); + listeners.updateMessageUnreadReactions(update, counterChanged, availabilityChanged, chat, chatLists); } @TdlibThread @@ -7613,14 +7962,24 @@ private void updateChatAvailableReactions (TdApi.UpdateChatAvailableReactions up listeners.updateChatAvailableReactions(update); } - private TdApi.ChatFolderInfo[] chatFolders; + private int mainChatListPosition; + private TdApi.ChatFolderInfo[] chatFolders = new TdApi.ChatFolderInfo[0]; + private final SparseArrayCompat chatFoldersById = new SparseArrayCompat<>(); @TdlibThread - private void updateChatFilters (TdApi.UpdateChatFolders update) { + private void updateChatFolders (TdApi.UpdateChatFolders update) { synchronized (dataLock) { - this.chatFolders = update.chatFolders; + TdApi.ChatFolderInfo[] chatFolders = update.chatFolders; + this.chatFolders = chatFolders; + this.chatFoldersById.clear(); + if (chatFolders != null) { + for (TdApi.ChatFolderInfo chatFolder : chatFolders) { + this.chatFoldersById.put(chatFolder.id, chatFolder); + } + } + this.mainChatListPosition = update.mainChatListPosition; } - listeners.updateChatFilters(update); + listeners.updateChatFolders(update); } @TdlibThread @@ -7848,6 +8207,18 @@ private void updateForumTopicInfo (TdApi.UpdateForumTopicInfo update) { listeners.updateForumTopicInfo(update); } + @TdlibThread + private void updateChatViewAsTopics (TdApi.UpdateChatViewAsTopics update) { + synchronized (dataLock) { + final TdApi.Chat chat = chats.get(update.chatId); + if (TdlibUtils.assertChat(update.chatId, chat, update)) { + return; + } + chat.viewAsTopics = update.viewAsTopics; + } + listeners.updateChatViewAsTopics(update); + } + @TdlibThread private void updateChatPendingJoinRequests (TdApi.UpdateChatPendingJoinRequests update) { synchronized (dataLock) { @@ -7887,16 +8258,149 @@ private void updateChatIsTranslatable (TdApi.UpdateChatIsTranslatable update) { } @TdlibThread - private void updateChatIsBlocked (TdApi.UpdateChatIsBlocked update) { + private void updateChatIsBlocked (TdApi.UpdateChatBlockList update) { synchronized (dataLock) { final TdApi.Chat chat = chats.get(update.chatId); if (TdlibUtils.assertChat(update.chatId, chat, update)) { return; } - chat.isBlocked = update.isBlocked; + chat.blockList = update.blockList; } - listeners.updateChatIsBlocked(update); + listeners.updateChatBlockList(update); + } + + // Updates: STORIES + + private final Comparator storiesComparator = (o1, o2) -> { + if (o1.order != o2.order) { + return o1.order > o2.order ? -1 : 1; + } + if (o1.chatId != o2.chatId) { + return o1.chatId > o2.chatId ? -1 : 1; + } + return 0; + }; + + public Comparator storiesComparator () { + return storiesComparator; + } + + @NonNull + public StoryList getStoryList (@NonNull TdApi.StoryList list) { + synchronized (dataLock) { + StoryList storyList = storyLists.get(list.getConstructor()); + if (storyList == null) { + storyList = new StoryList(this, list); + storyLists.put(list.getConstructor(), storyList); + } + return storyList; + } + } + + @Nullable + public TdApi.ChatActiveStories getActiveStories (long chatId, boolean allowRequest, @Nullable RunnableData onLoaded) { + synchronized (dataLock) { + TdApi.ChatActiveStories stories = this.activeStories.get(chatId); + if (stories != null) { + return stories; + } + } + if (allowRequest) { + client().send(new TdApi.GetChatActiveStories(chatId), result -> { + switch (result.getConstructor()) { + case TdApi.ChatActiveStories.CONSTRUCTOR: { + TdApi.ChatActiveStories stories = getActiveStories(chatId, false, null); + if (stories == null) { + throw new IllegalStateException(); + } + if (onLoaded != null) { + onLoaded.runWithData(stories); + } + break; + } + case TdApi.Error.CONSTRUCTOR: { + UI.showError(result); + break; + } + } + }); + } + return null; + } + + @TdlibThread + private void updateStoryListChatCount (TdApi.UpdateStoryListChatCount update) { + synchronized (dataLock) { + storyListChatCount.put(update.storyList.getConstructor(), update.chatCount); + } + StoryList storyList = getStoryList(update.storyList); + storyList.notifyApproximateTotalItemCountChanged(); + } + + public int getStoryListChatCount (@NonNull TdApi.StoryList list) { + synchronized (dataLock) { + return storyListChatCount.get(list.getConstructor()); + } + } + + @TdlibThread + private void updateChatActiveStories (TdApi.UpdateChatActiveStories update) { + final TdApi.ChatActiveStories prevActiveStories; + synchronized (dataLock) { + final long chatId = update.activeStories.chatId; + prevActiveStories = activeStories.remove(chatId); + activeStories.put(chatId, update.activeStories); + } + listeners.updateChatActiveStories(update); + boolean wasPresent = prevActiveStories != null && prevActiveStories.stories.length > 0 && prevActiveStories.list != null; + boolean nowPresent = update.activeStories.stories.length > 0 && update.activeStories.list != null; + boolean sameList = wasPresent && nowPresent && Td.equalsTo(prevActiveStories.list, update.activeStories.list); + if (sameList) { + // Moved or just updated within the same list + StoryList storyList = getStoryList(update.activeStories.list); + storyList.moveItem(update.activeStories, prevActiveStories); + } else { + if (wasPresent) { + // Removed from prevActiveStories.list + StoryList storyList = getStoryList(prevActiveStories.list); + storyList.removeItem(prevActiveStories); + } + if (nowPresent) { + // Added to update.activeStories.list + StoryList storyList = getStoryList(update.activeStories.list); + storyList.addItem(update.activeStories); + } + } + } + + @TdlibThread + private void updateStory (TdApi.UpdateStory update) { + listeners.updateStory(update); + } + + @TdlibThread + private void updateStoryDeleted (TdApi.UpdateStoryDeleted update) { + listeners.updateStoryDeleted(update); + } + + @TdlibThread + private void updateStorySendSucceeded (TdApi.UpdateStorySendSucceeded update) { + listeners.updateStorySendSucceeded(update); + } + + @TdlibThread + private void updateStorySendFailed (TdApi.UpdateStorySendFailed update) { + listeners.updateStorySendFailed(update); + } + + @TdlibThread + private void updateStoryStealthMode (TdApi.UpdateStoryStealthMode update) { + synchronized (dataLock) { + this.storyStealthModeActiveUntilDate = update.activeUntilDate; + this.storyStealthModeCooldownUntilDate = update.cooldownUntilDate; + } + listeners.updateStoryStealthMode(update); } // Updates: CHAT STATUS @@ -8065,6 +8569,11 @@ private void updateServiceNotification (TdApi.UpdateServiceNotification update) }); } + @TdlibThread + private void updateUnconfirmedSession (TdApi.UpdateUnconfirmedSession update) { + + } + // Updates: FILES private void updateFile (TdApi.UpdateFile update) { @@ -8143,7 +8652,8 @@ private void updateConnectionState (TdApi.UpdateConnectionState update) { state = ConnectionState.CONNECTED; break; default: - throw new UnsupportedOperationException(update.toString()); + Td.assertConnectionState_963d6b5f(); + throw Td.unsupported(update.state); } if (this.connectionState != state) { @@ -8370,6 +8880,42 @@ private void updateChatBackground (TdApi.UpdateChatBackground update) { listeners.updateChatBackground(update); } + private void updateChat (T update, long chatId, RunnableData chatModifier, RunnableData updateDispatcher) { + final TdApi.Chat chat; + synchronized (dataLock) { + chat = chats.get(chatId); + if (TdlibUtils.assertChat(chatId, chat, update)) { + return; + } + if (chatModifier != null) { + chatModifier.runWithData(chat); + } + } + if (updateDispatcher != null) { + updateDispatcher.runWithData(update); + } + } + + @TdlibThread + private void updateChatAccentColors (TdApi.UpdateChatAccentColors update) { + updateChat(update, update.chatId, chat -> { + chat.accentColorId = update.accentColorId; + chat.backgroundCustomEmojiId = update.backgroundCustomEmojiId; + chat.profileAccentColorId = update.profileAccentColorId; + chat.profileBackgroundCustomEmojiId = update.profileBackgroundCustomEmojiId; + }, + listeners::updateChatAccentColors + ); + } + + @TdlibThread + private void updateChatEmojiStatus (TdApi.UpdateChatEmojiStatus update) { + updateChat(update, update.chatId, chat -> + chat.emojiStatus = update.emojiStatus, + listeners::updateChatEmojiStatus + ); + } + @AnyThread public @Nullable TdApi.ChatTheme chatTheme (String themeName) { synchronized (dataLock) { @@ -8392,7 +8938,7 @@ private boolean setUnreadCounters (@NonNull TdApi.ChatList chatList, int unreadM account().storeCounter(chatList, counter, false); context.incrementBadgeCounters(chatList, unreadMessageCount - oldUnreadCount, unreadUnmutedCount - oldUnreadUnmutedCount, false); - listeners().notifyMessageCountersChanged(chatList, unreadMessageCount, unreadUnmutedCount); + listeners().notifyMessageCountersChanged(chatList, counter, unreadMessageCount, unreadUnmutedCount); return true; } @@ -8403,7 +8949,7 @@ private void dispatchUnreadCounters (@NonNull TdApi.ChatList chatList, int count } @TdlibThread - private boolean setUnreadChatCounters(@NonNull TdApi.ChatList chatList, int totalCount, int unreadChatCount, int unreadUnmutedCount, int markedAsUnreadCount, int markedAsUnreadUnmutedCount) { + private boolean setUnreadChatCounters (@NonNull TdApi.ChatList chatList, int totalCount, int unreadChatCount, int unreadUnmutedCount, int markedAsUnreadCount, int markedAsUnreadUnmutedCount) { TdlibCounter counter = getCounter(chatList); int oldUnreadCount = Math.max(counter.chatCount, 0); @@ -8413,7 +8959,7 @@ private boolean setUnreadChatCounters(@NonNull TdApi.ChatList chatList, int tota if (counter.setChatCounters(totalCount, unreadChatCount, unreadUnmutedCount, markedAsUnreadCount, markedAsUnreadUnmutedCount)) { account().storeCounter(chatList, counter, true); context.incrementBadgeCounters(chatList, unreadChatCount - oldUnreadCount, unreadUnmutedCount - oldUnreadUnmutedCount, true); - listeners().notifyChatCountersChanged(chatList, (totalCount > 0) != (oldTotalChatCount > 0), totalCount, unreadChatCount, unreadUnmutedCount); + listeners().notifyChatCountersChanged(chatList, counter, (totalCount > 0) != (oldTotalChatCount > 0), totalCount, unreadChatCount, unreadUnmutedCount); return true; } @@ -8454,6 +9000,16 @@ public int getUnreadBadgeCount () { return getUnreadBadge().getCount(); } + @TdlibThread + private void updateSpeechRecognitionTrial (TdApi.UpdateSpeechRecognitionTrial update) { + // TODO + } + + @TdlibThread + private void updateDefaultBackground (TdApi.UpdateDefaultBackground update) { + // TODO ? + } + @TdlibThread private void updateOption (ClientHolder context, TdApi.UpdateOption update) { final String name = update.name; @@ -8628,6 +9184,37 @@ private void updateOption (ClientHolder context, TdApi.UpdateOption update) { this.maxBioLength = Td.intValue(update.value); break; + case "active_story_count_max": + this.activeStoryCountMax = Td.intValue(update.value); + break; + case "weekly_sent_story_count_max": + this.weeklySentStoryCountMax = Td.intValue(update.value); + break; + case "monthly_sent_story_count_max": + this.monthlySentStoryCountMax = Td.intValue(update.value); + break; + case "can_use_text_entities_in_story_caption": + this.canUseTextEntitiesInStoryCaptions = Td.boolValue(update.value); + break; + case "story_caption_length_max": + this.storyCaptionLengthMax = Td.intValue(update.value); + break; + case "story_suggested_reaction_area_count_max": + this.storySuggestedReactionAreaCountMax = Td.intValue(update.value); + break; + case "story_viewers_expiration_delay": + this.storyViewersExpirationDelay = Td.intValue(update.value); + break; + case "story_stealth_mode_cooldown_period": + this.storyStealhModeCooldownPeriod = Td.intValue(update.value); + break; + case "story_stealth_mode_future_period": + this.storyStealthModeFuturePeriod = Td.intValue(update.value); + break; + case "story_stealth_mode_past_period": + this.storyStealthModePastPeriod = Td.intValue(update.value); + break; + // Service accounts and chats case "anti_spam_bot_user_id": @@ -8678,9 +9265,6 @@ private void updateOption (ClientHolder context, TdApi.UpdateOption update) { case "disable_sent_scheduled_message_notifications": setDisableSentScheduledMessageNotificationsImpl(Td.boolValue(update.value)); break; - case "archive_and_mute_new_chats_from_unknown_users": - setArchiveAndMuteNewChatsFromUnknownUsersImpl(Td.boolValue(update.value)); - break; // Language @@ -8693,18 +9277,12 @@ private void updateOption (ClientHolder context, TdApi.UpdateOption update) { this.suggestedLanguagePackId = languagePackId; this.suggestedLanguagePackInfo = null; listeners().updateSuggestedLanguageChanged(languagePackId, null); - context.client.send(new TdApi.GetLanguagePackInfo(languagePackId), result -> { - switch (result.getConstructor()) { - case TdApi.LanguagePackInfo.CONSTRUCTOR: - setSuggestedLanguagePackInfo(languagePackId, (TdApi.LanguagePackInfo) result); - break; - case TdApi.Error.CONSTRUCTOR: - Log.e("Failed to fetch suggested language, code: %s %s", languagePackId, TD.toErrorString(result)); - setSuggestedLanguagePackInfo(languagePackId, null); - break; - default: - Log.unexpectedTdlibResponse(result, TdApi.GetLanguagePackInfo.class, TdApi.LanguagePackInfo.class, TdApi.Error.class); - break; + send(context.client, new TdApi.GetLanguagePackInfo(languagePackId), (languagePackInfo, error) -> { + if (error != null) { + Log.e("Failed to fetch suggested language, code: %s %s", languagePackId, TD.toErrorString(error)); + setSuggestedLanguagePackInfo(languagePackId, null); + } else { + setSuggestedLanguagePackInfo(languagePackId, languagePackInfo); } }); } @@ -8740,6 +9318,66 @@ private void updateOption (ClientHolder context, TdApi.UpdateOption update) { } } + // Updates: Accent colors + + private int[] availableAccentColorIds; + private final SparseArrayCompat accentColors = new SparseArrayCompat<>(); + + @TdlibThread + private void updateAccentColors (TdApi.UpdateAccentColors update) { + boolean listChanged; + int updatedColorsCount = 0; + synchronized (accentColors) { + listChanged = !Arrays.equals(this.availableAccentColorIds, update.availableAccentColorIds); + this.availableAccentColorIds = update.availableAccentColorIds; + for (TdApi.AccentColor newColor : update.colors) { + TdlibAccentColor oldColor = accentColors.get(newColor.id); + if (oldColor == null) { + accentColors.put(newColor.id, new TdlibAccentColor(newColor)); + } + if (oldColor == null || oldColor.updateColor(newColor)) { + updatedColorsCount++; + } + } + } + if (listChanged || updatedColorsCount > 0) { + listeners.updateAccentColors(update); + } + } + + @NonNull + public TdlibAccentColor accentColor (int accentColorId) { + synchronized (accentColors) { + TdlibAccentColor color = accentColors.get(accentColorId); + if (color == null) { + color = new TdlibAccentColor(accentColorId); + accentColors.put(accentColorId, color); + } + return color; + } + } + + @NonNull + public TdlibAccentColor accentColorForString (String any) { + return accentColor(MathUtils.pickNumber(TdlibAccentColor.BUILT_IN_COLOR_COUNT, any)); + } + + private int[] availableProfileAccentColorIds; + private final SparseArrayCompat profileAccentColors = new SparseArrayCompat<>(); + + @TdlibThread + private void updateProfileAccentColors (TdApi.UpdateProfileAccentColors update) { + boolean listChanged; + synchronized (profileAccentColors) { + listChanged = Arrays.equals(this.availableProfileAccentColorIds, update.availableAccentColorIds); + this.availableProfileAccentColorIds = update.availableAccentColorIds; + for (TdApi.ProfileAccentColor profileAccentColor : update.colors) { + profileAccentColors.put(profileAccentColor.id, profileAccentColor); + } + } + listeners.updateProfileAccentColors(update, listChanged); + } + // Updates: MEDIA private void updateAnimationSearchParameters (TdApi.UpdateAnimationSearchParameters update) { @@ -9013,6 +9651,36 @@ private void processUpdate (ClientHolder context, TdApi.Update update) { break; } + // Stories + case TdApi.UpdateStoryListChatCount.CONSTRUCTOR: { + updateStoryListChatCount((TdApi.UpdateStoryListChatCount) update); + break; + } + case TdApi.UpdateChatActiveStories.CONSTRUCTOR: { + updateChatActiveStories((TdApi.UpdateChatActiveStories) update); + break; + } + case TdApi.UpdateStory.CONSTRUCTOR: { + updateStory((TdApi.UpdateStory) update); + break; + } + case TdApi.UpdateStoryDeleted.CONSTRUCTOR: { + updateStoryDeleted((TdApi.UpdateStoryDeleted) update); + break; + } + case TdApi.UpdateStorySendSucceeded.CONSTRUCTOR: { + updateStorySendSucceeded((TdApi.UpdateStorySendSucceeded) update); + break; + } + case TdApi.UpdateStorySendFailed.CONSTRUCTOR: { + updateStorySendFailed((TdApi.UpdateStorySendFailed) update); + break; + } + case TdApi.UpdateStoryStealthMode.CONSTRUCTOR: { + updateStoryStealthMode((TdApi.UpdateStoryStealthMode) update); + break; + } + // Voice chats case TdApi.UpdateChatVideoChat.CONSTRUCTOR: { updateChatVideoChat((TdApi.UpdateChatVideoChat) update); @@ -9024,6 +9692,10 @@ private void processUpdate (ClientHolder context, TdApi.Update update) { updateForumTopicInfo((TdApi.UpdateForumTopicInfo) update); break; } + case TdApi.UpdateChatViewAsTopics.CONSTRUCTOR: { + updateChatViewAsTopics((TdApi.UpdateChatViewAsTopics) update); + break; + } // Join requests case TdApi.UpdateChatPendingJoinRequests.CONSTRUCTOR: { @@ -9071,7 +9743,7 @@ private void processUpdate (ClientHolder context, TdApi.Update update) { break; } case TdApi.UpdateChatFolders.CONSTRUCTOR: { - updateChatFilters((TdApi.UpdateChatFolders) update); + updateChatFolders((TdApi.UpdateChatFolders) update); break; } case TdApi.UpdateChatPosition.CONSTRUCTOR: { @@ -9086,8 +9758,8 @@ private void processUpdate (ClientHolder context, TdApi.Update update) { updateChatIsTranslatable((TdApi.UpdateChatIsTranslatable) update); break; } - case TdApi.UpdateChatIsBlocked.CONSTRUCTOR: { - updateChatIsBlocked((TdApi.UpdateChatIsBlocked) update); + case TdApi.UpdateChatBlockList.CONSTRUCTOR: { + updateChatIsBlocked((TdApi.UpdateChatBlockList) update); break; } case TdApi.UpdateChatDefaultDisableNotification.CONSTRUCTOR: { @@ -9256,6 +9928,10 @@ private void processUpdate (ClientHolder context, TdApi.Update update) { updateServiceNotification((TdApi.UpdateServiceNotification) update); break; } + case TdApi.UpdateUnconfirmedSession.CONSTRUCTOR: { + updateUnconfirmedSession((TdApi.UpdateUnconfirmedSession) update); + break; + } // Files case TdApi.UpdateFile.CONSTRUCTOR: { @@ -9296,8 +9972,23 @@ private void processUpdate (ClientHolder context, TdApi.Update update) { updateOption(context, (TdApi.UpdateOption) update); break; } - case TdApi.UpdateSelectedBackground.CONSTRUCTOR: { - // TODO? + case TdApi.UpdateSpeechRecognitionTrial.CONSTRUCTOR: { + updateSpeechRecognitionTrial((TdApi.UpdateSpeechRecognitionTrial) update); + break; + } + case TdApi.UpdateDefaultBackground.CONSTRUCTOR: { + updateDefaultBackground((TdApi.UpdateDefaultBackground) update); + break; + } + + + // Accent colors + case TdApi.UpdateAccentColors.CONSTRUCTOR: { + updateAccentColors((TdApi.UpdateAccentColors) update); + break; + } + case TdApi.UpdateProfileAccentColors.CONSTRUCTOR: { + updateProfileAccentColors((TdApi.UpdateProfileAccentColors) update); break; } @@ -9362,6 +10053,14 @@ private void processUpdate (ClientHolder context, TdApi.Update update) { updateChatBackground((TdApi.UpdateChatBackground) update); break; } + case TdApi.UpdateChatAccentColors.CONSTRUCTOR: { + updateChatAccentColors((TdApi.UpdateChatAccentColors) update); + break; + } + case TdApi.UpdateChatEmojiStatus.CONSTRUCTOR: { + updateChatEmojiStatus((TdApi.UpdateChatEmojiStatus) update); + break; + } // File generation case TdApi.UpdateFileGenerationStart.CONSTRUCTOR: { @@ -9373,7 +10072,7 @@ private void processUpdate (ClientHolder context, TdApi.Update update) { break; } - // Bots + // for bots only. case TdApi.UpdateNewChatJoinRequest.CONSTRUCTOR: case TdApi.UpdateNewCustomEvent.CONSTRUCTOR: case TdApi.UpdateNewCustomQuery.CONSTRUCTOR: @@ -9385,9 +10084,16 @@ private void processUpdate (ClientHolder context, TdApi.Update update) { case TdApi.UpdateNewShippingQuery.CONSTRUCTOR: case TdApi.UpdatePoll.CONSTRUCTOR: case TdApi.UpdatePollAnswer.CONSTRUCTOR: - case TdApi.UpdateChatMember.CONSTRUCTOR: { - Log.unexpectedTdlibResponse(update, null, TdApi.Update.class); - break; + case TdApi.UpdateChatMember.CONSTRUCTOR: + case TdApi.UpdateChatBoost.CONSTRUCTOR: + case TdApi.UpdateMessageReaction.CONSTRUCTOR: + case TdApi.UpdateMessageReactions.CONSTRUCTOR: { + // Must never come from TDLib. If it does, there's a bug on TDLib side. + throw Td.unsupported(update); + } + default: { + Td.assertUpdate_618db8c7(); + throw Td.unsupported(update); } } } @@ -9399,9 +10105,10 @@ void downloadMyUser (@Nullable TdApi.User user) { TdApi.EmojiStatus emojiStatus = user != null && user.isPremium ? user.emojiStatus : null; long newEmojiStatusId = emojiStatus != null ? emojiStatus.customEmojiId : 0; TdlibEmojiManager.Entry emojiEntry = newEmojiStatusId != 0 ? emoji().find(newEmojiStatusId) : null; + TdlibAccentColor accentColor = cache().userAccentColor(user); TdApi.Sticker emojiStatusSticker = emojiEntry != null && !emojiEntry.isNotFound() ? emojiEntry.value : null; - account().storeUserInformation(user, emojiStatusSticker); + account().storeUserInformation(user, accentColor != null ? accentColor.getRemoteAccentColor() : null, emojiStatusSticker); downloadMyProfilePhoto(user); downloadMyUserEmojiStatus(user); } @@ -9551,12 +10258,17 @@ private void onJobRemoved (boolean justFinishedExecution) { case TdApi.AuthorizationStateReady.CONSTRUCTOR: return true; default: - throw new UnsupportedOperationException(state.toString()); + Td.assertAuthorizationState_6e5056de(); + throw Td.unsupported(state); } } return false; }).onAddRemove(this::onJobAdded, this::onJobRemoved), initializationListeners = new ConditionalExecutor(() -> authorizationStatus() != Status.UNKNOWN).onAddRemove(this::onJobAdded, this::onJobRemoved), // Executed once received authorization state + myUserOrUnauthorizedListeners = new ConditionalExecutor(() -> { + int status = authorizationStatus(); + return status != Status.UNKNOWN && (status == Status.UNAUTHORIZED || myUser() != null); + }).onAddRemove(this::onJobAdded, this::onJobRemoved), connectionListeners = new ConditionalExecutor(() -> authorizationStatus() != Status.UNKNOWN && connectionState == ConnectionState.CONNECTED).onAddRemove(this::onJobAdded, this::onJobRemoved), // Executed once connected notificationInitListeners = new ConditionalExecutor(() -> { final int status = authorizationStatus(); @@ -9572,6 +10284,11 @@ public void awaitInitialization (@NonNull Runnable after) { initializationListeners.executeOrPostponeTask(after); } + @AnyThread + public void awaitMyUserOrUnauthorizedState (@NonNull Runnable after) { + myUserOrUnauthorizedListeners.executeOrPostponeTask(after); + } + @AnyThread public void awaitAllReferencesReleased (@NonNull Runnable after) { noReferenceListeners.executeOrPostponeTask(after); @@ -9640,7 +10357,7 @@ public void dispatchNotificationsInitialized () { // Emoji - void fetchAllMessages (long chatId, @Nullable String query, @Nullable TdApi.SearchMessagesFilter filter, @NonNull RunnableData> callback) { + void fetchAllMessages (long chatId, @Nullable String query, @Nullable TdApi.SearchMessagesFilter filter, @NonNull RunnableData> callback) { List messages = new ArrayList<>(); boolean needFilter = !StringUtils.isEmpty(query) || filter != null; TdApi.Function function; @@ -9720,7 +10437,7 @@ public void findUpdateFile (@NonNull RunnableData onDone) { } } clientHolder().updates.findResource(message -> { - if (message != null && message.content.getConstructor() == TdApi.MessageDocument.CONSTRUCTOR) { + if (message != null && Td.isDocument(message.content)) { TdApi.Document document = ((TdApi.MessageDocument) message.content).document; TdApi.FormattedText caption = ((TdApi.MessageDocument) message.content).caption; boolean ok = false; @@ -9947,6 +10664,26 @@ public boolean canRestrictMembers (long chatId) { return false; } + public boolean inSlowMode (long chatId) { + return cache.getSlowModeDelayExpiresIn(ChatId.toSupergroupId(chatId), TimeUnit.SECONDS) > 0; + } + + public boolean canEditSlowMode (long chatId) { + if (canRestrictMembers(chatId)) { + TdApi.Supergroup supergroup = chatToSupergroup(chatId); + if (supergroup != null) { + return !supergroup.isChannel && !supergroup.isBroadcastGroup; + } + return ChatId.isBasicGroup(chatId) && canUpgradeChat(chatId); + } + return false; + } + + public boolean isBroadcastGroup (long chatId) { + TdApi.Supergroup supergroup = chatToSupergroup(chatId); + return supergroup != null && supergroup.isBroadcastGroup; + } + public boolean canDeleteMessages (long chatId) { TdApi.ChatMemberStatus status = chatStatus(chatId); if (status != null) { @@ -10169,6 +10906,9 @@ public RestrictionStatus getRestrictionStatus (TdApi.Chat chat, @RightId int rig return new RestrictionStatus(chat.id, RESTRICTION_STATUS_RESTRICTED, 0); } } + if (!TD.checkRight(chat.permissions, rightId)) { + return new RestrictionStatus(chat.id, RESTRICTION_STATUS_RESTRICTED, 0); + } return null; } case TdApi.ChatTypeSecret.CONSTRUCTOR: { @@ -10190,6 +10930,9 @@ public RestrictionStatus getRestrictionStatus (TdApi.Chat chat, @RightId int rig return new RestrictionStatus(chat.id, RESTRICTION_STATUS_RESTRICTED, 0); } } + if (!TD.checkRight(chat.permissions, rightId)) { + return new RestrictionStatus(chat.id, RESTRICTION_STATUS_RESTRICTED, 0); + } } return new RestrictionStatus(chat.id, RESTRICTION_STATUS_UNAVAILABLE, 0); } @@ -10202,6 +10945,28 @@ public RestrictionStatus getRestrictionStatus (TdApi.Chat chat, @RightId int rig return null; } + public CharSequence getSlowModeRestrictionText (long chatId) { + return getSlowModeRestrictionText(chatId, null); + } + + public CharSequence getSlowModeRestrictionText (long chatId, @Nullable TdApi.MessageSchedulingState schedulingState) { + if (schedulingState != null) { + return null; + } + + final int timeToSend = (int) cache().getSlowModeDelayExpiresIn(ChatId.toSupergroupId(chatId), TimeUnit.SECONDS); + if (timeToSend == 0) { + return null; + } + + final int minutes = timeToSend / 60; + final int seconds = timeToSend % 60; + + return (minutes > 0) ? + Lang.pluralBold(R.string.xSlowModeRestrictionMinutes, minutes): + Lang.pluralBold(R.string.xSlowModeRestrictionSeconds, seconds); + } + public CharSequence getRestrictionText (TdApi.Chat chat, TdApi.Message message) { if (message != null) { switch (message.content.getConstructor()) { @@ -10222,6 +10987,8 @@ public CharSequence getRestrictionText (TdApi.Chat chat, TdApi.Message message) case TdApi.MessageVideo.CONSTRUCTOR: case TdApi.MessageExpiredVideo.CONSTRUCTOR: return getDefaultRestrictionText(chat, RightId.SEND_VIDEOS); + case TdApi.MessageStory.CONSTRUCTOR: + return getStoryRestrictionText(chat); case TdApi.MessageVideoNote.CONSTRUCTOR: return getDefaultRestrictionText(chat, RightId.SEND_VIDEO_NOTES); case TdApi.MessageVoiceNote.CONSTRUCTOR: @@ -10255,6 +11022,7 @@ public CharSequence getRestrictionText (TdApi.Chat chat, TdApi.Message message) case TdApi.MessageChatJoinByRequest.CONSTRUCTOR: case TdApi.MessageChatSetMessageAutoDeleteTime.CONSTRUCTOR: case TdApi.MessageChatSetTheme.CONSTRUCTOR: + case TdApi.MessageChatSetBackground.CONSTRUCTOR: case TdApi.MessageChatShared.CONSTRUCTOR: case TdApi.MessageChatUpgradeFrom.CONSTRUCTOR: case TdApi.MessageChatUpgradeTo.CONSTRUCTOR: @@ -10265,6 +11033,11 @@ public CharSequence getRestrictionText (TdApi.Chat chat, TdApi.Message message) case TdApi.MessageForumTopicIsClosedToggled.CONSTRUCTOR: case TdApi.MessageForumTopicIsHiddenToggled.CONSTRUCTOR: case TdApi.MessageGiftedPremium.CONSTRUCTOR: + case TdApi.MessagePremiumGiftCode.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayCreated.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayCompleted.CONSTRUCTOR: + case TdApi.MessagePremiumGiveawayWinners.CONSTRUCTOR: + case TdApi.MessagePremiumGiveaway.CONSTRUCTOR: case TdApi.MessageInviteVideoChatParticipants.CONSTRUCTOR: case TdApi.MessagePassportDataReceived.CONSTRUCTOR: case TdApi.MessagePassportDataSent.CONSTRUCTOR: @@ -10273,18 +11046,19 @@ public CharSequence getRestrictionText (TdApi.Chat chat, TdApi.Message message) case TdApi.MessageSuggestProfilePhoto.CONSTRUCTOR: case TdApi.MessageSupergroupChatCreate.CONSTRUCTOR: case TdApi.MessageUnsupported.CONSTRUCTOR: - case TdApi.MessageUserShared.CONSTRUCTOR: + case TdApi.MessageUsersShared.CONSTRUCTOR: case TdApi.MessageVideoChatEnded.CONSTRUCTOR: case TdApi.MessageVideoChatScheduled.CONSTRUCTOR: case TdApi.MessageVideoChatStarted.CONSTRUCTOR: case TdApi.MessageWebAppDataReceived.CONSTRUCTOR: case TdApi.MessageWebAppDataSent.CONSTRUCTOR: - case TdApi.MessageWebsiteConnected.CONSTRUCTOR: // None of these messages ever passed to this method, // assuming we want to check RightId.SEND_BASIC_MESSAGES return getBasicMessageRestrictionText(chat); + default: + Td.assertMessageContent_d40af239(); + throw Td.unsupported(message.content); } - throw new UnsupportedOperationException(message.content.toString()); } // Assuming if null is passed, we want to check if we can write text messages return getBasicMessageRestrictionText(chat); @@ -10324,9 +11098,12 @@ public CharSequence getRestrictionText (TdApi.Chat chat, TdApi.InputMessageConte case TdApi.InputMessageText.CONSTRUCTOR: case TdApi.InputMessageVenue.CONSTRUCTOR: case TdApi.InputMessageContact.CONSTRUCTOR: + case TdApi.InputMessageStory.CONSTRUCTOR: return getBasicMessageRestrictionText(chat); + default: + Td.assertInputMessageContent_4e99a3f(); + throw Td.unsupported(content); } - throw new UnsupportedOperationException(content.toString()); } // Assuming if null is passed, we want to check if we can write text messages return getBasicMessageRestrictionText(chat); @@ -10398,7 +11175,7 @@ public CharSequence getDefaultRestrictionText (TdApi.Chat chat, @RightId int rig ); } - public CharSequence getVoiceVideoRestricitonText (TdApi.Chat chat, boolean needVideo) { + public CharSequence getVoiceVideoRestrictionText (TdApi.Chat chat, boolean needVideo) { return getDefaultRestrictionText(chat, needVideo ? RightId.SEND_VIDEO_NOTES : RightId.SEND_VOICE_NOTES); } @@ -10414,6 +11191,37 @@ public CharSequence getGameRestrictionText (TdApi.Chat chat) { return buildRestrictionText(chat, RightId.SEND_OTHER_MESSAGES, R.string.ChatDisabledGames, R.string.ChatRestrictedGames, R.string.ChatRestrictedGamesUntil); } + public CharSequence getStoryRestrictionText (TdApi.Chat chat) { + Tdlib.RestrictionStatus photoStatus = getRestrictionStatus(chat, RightId.SEND_PHOTOS); + Tdlib.RestrictionStatus videoStatus = getRestrictionStatus(chat, RightId.SEND_VIDEOS); + Tdlib.RestrictionStatus status; + @RightId int rightId; + if (photoStatus == null || videoStatus == null) { + if (videoStatus != null) { + rightId = RightId.SEND_VIDEOS; + status = videoStatus; + } else { + rightId = RightId.SEND_PHOTOS; + status = photoStatus; + } + } else if (photoStatus.isGlobal() != videoStatus.isGlobal()) { + if (photoStatus.isGlobal()) { + rightId = RightId.SEND_VIDEOS; + status = videoStatus; + } else { + rightId = RightId.SEND_PHOTOS; + status = photoStatus; + } + } else { + status = videoStatus; + rightId = RightId.SEND_VIDEOS; + } + return buildRestrictionText(status, + chat, rightId, + R.string.ChatDisabledStory, R.string.ChatRestrictedStory, R.string.ChatRestrictedStoryUntil + ); + } + public CharSequence getInlineRestrictionText (TdApi.Chat chat) { return buildRestrictionText(chat, RightId.SEND_OTHER_MESSAGES, R.string.ChatDisabledBots, R.string.ChatRestrictedBots, R.string.ChatRestrictedBotsUntil); } @@ -10424,11 +11232,11 @@ public CharSequence getPollRestrictionText (TdApi.Chat chat) { public CharSequence getDiceRestrictionText (TdApi.Chat chat, String emoji) { int disabledRes, restrictedRes, restrictedUntilRes; - if (TD.EMOJI_DART.textRepresentation.equals(emoji)) { + if (ContentPreview.EMOJI_DART.textRepresentation.equals(emoji)) { disabledRes = R.string.ChatDisabledDart; restrictedRes = R.string.ChatRestrictedDart; restrictedUntilRes = R.string.ChatRestrictedDartUntil; - } else if (TD.EMOJI_DICE.textRepresentation.equals(emoji)) { + } else if (ContentPreview.EMOJI_DICE.textRepresentation.equals(emoji)) { disabledRes = R.string.ChatDisabledDice; restrictedRes = R.string.ChatRestrictedDice; restrictedUntilRes = R.string.ChatRestrictedDiceUntil; @@ -10448,6 +11256,21 @@ public CharSequence buildRestrictionText (TdApi.Chat chat, @RightId int rightId, @StringRes int defaultRes, @StringRes int specificRes, @StringRes int specificUntilRes, @StringRes int defaultUserRes, @StringRes int specificUserRes) { RestrictionStatus status = getRestrictionStatus(chat, rightId); + return buildRestrictionText(status, + chat, rightId, + defaultRes, specificRes, specificUntilRes, + defaultUserRes, specificUserRes + ); + } + + public CharSequence buildRestrictionText (@Nullable RestrictionStatus status, TdApi.Chat chat, @RightId int rightId, @StringRes int defaultRes, @StringRes int specificRes, @StringRes int specificUntilRes) { + return buildRestrictionText(status, chat, rightId, defaultRes, specificRes, specificUntilRes, R.string.UserDisabledMessages, 0); + } + + public CharSequence buildRestrictionText (@Nullable RestrictionStatus status, + TdApi.Chat chat, @RightId int rightId, + @StringRes int defaultRes, @StringRes int specificRes, @StringRes int specificUntilRes, + @StringRes int defaultUserRes, @StringRes int specificUserRes) { if (status != null) { switch (rightId) { case RightId.SEND_BASIC_MESSAGES: @@ -10476,6 +11299,10 @@ public CharSequence buildRestrictionText (TdApi.Chat chat, @RightId int rightId, case RightId.EDIT_MESSAGES: case RightId.INVITE_USERS: case RightId.MANAGE_VIDEO_CHATS: + case RightId.MANAGE_TOPICS: + case RightId.POST_STORIES: + case RightId.EDIT_STORIES: + case RightId.DELETE_STORIES: case RightId.PIN_MESSAGES: case RightId.READ_MESSAGES: case RightId.REMAIN_ANONYMOUS: @@ -10526,6 +11353,22 @@ public boolean canSendBasicMessage (long chatId) { public boolean canSendBasicMessage (TdApi.Chat chat) { return canSendMessage(chat, RightId.SEND_BASIC_MESSAGES); } + + public boolean canSendSendSomeMedia (TdApi.Chat chat) { + return canSendSendSomeMedia(chat, false); + } + + public boolean canSendSendSomeMedia (TdApi.Chat chat, boolean checkGlobal) { + for (int rightId : EditRightsController.SEND_MEDIA_RIGHT_IDS) { + Tdlib.RestrictionStatus restrictionStatus = getRestrictionStatus(chat, rightId); + if (restrictionStatus == null || checkGlobal && !restrictionStatus.isGlobal()) { + return true; + } + } + + return false; + } + public boolean canSendMessage (TdApi.Chat chat, @RightId int kindResId) { switch (kindResId) { case RightId.SEND_BASIC_MESSAGES: @@ -10546,6 +11389,10 @@ public boolean canSendMessage (TdApi.Chat chat, @RightId int kindResId) { case RightId.EDIT_MESSAGES: case RightId.INVITE_USERS: case RightId.MANAGE_VIDEO_CHATS: + case RightId.MANAGE_TOPICS: + case RightId.POST_STORIES: + case RightId.EDIT_STORIES: + case RightId.DELETE_STORIES: case RightId.PIN_MESSAGES: case RightId.READ_MESSAGES: case RightId.REMAIN_ANONYMOUS: @@ -10571,9 +11418,19 @@ public int getSettingSuggestionCount () { } } + public TdlibSingleUnreadReactionsManager singleUnreadReactionsManager () { + return unreadReactionsManager; + } + + @Nullable + public TdApi.UnreadReaction getSingleUnreadReaction (long chatId) { + // If chat has one unread reaction, returns it. May be null + return unreadReactionsManager.getSingleUnreadReaction(chatId); + } + public boolean haveAnySettingsSuggestions () { synchronized (dataLock) { - for (TdApi.SuggestedAction action: suggestedActions) { + for (TdApi.SuggestedAction action : suggestedActions) { if (isSettingSuggestion(action)) return true; } @@ -10646,4 +11503,138 @@ public String getMessageSenderTitle (TdApi.MessageSender sender) { return chatTitle(Td.getSenderId(sender)); } } + + @Nullable + public TdApi.ChatFolderIcon chatFolderIcon (TdApi.ChatFolder chatFolder) { + if (chatFolder.icon != null && !StringUtils.isEmpty(chatFolder.icon.name)) { + return chatFolder.icon; + } + TdApi.ChatFolderIcon result = clientExecuteT(new TdApi.GetChatFolderDefaultIconName(chatFolder), false); + if (result != null && !StringUtils.isEmpty(result.name)) { + return result; + } + return null; + } + + public String chatFolderIconName (TdApi.ChatFolder chatFolder) { + TdApi.ChatFolderIcon icon = chatFolderIcon(chatFolder); + return icon != null ? icon.name : ""; + } + + public @DrawableRes int chatFolderIconDrawable (TdApi.ChatFolder chatFolder, @DrawableRes int defaultIcon) { + return TD.findFolderIcon(chatFolderIcon(chatFolder), defaultIcon); + } + + public void addChatsToChatFolder (TdlibDelegate delegate, int chatFolderId, long[] chatIds) { + if (chatIds.length == 0) { + return; + } + send(new TdApi.GetChatFolder(chatFolderId), (chatFolder, error) -> { + if (error != null) { + UI.showError(chatFolder); + } else { + addChatsToChatFolder(delegate, chatFolderId, chatFolder, chatIds); + } + }); + } + + public void addChatsToChatFolder (TdlibDelegate delegate, int chatFolderId, TdApi.ChatFolder chatFolder, long[] chatIds) { + if (chatIds.length == 0) { + return; + } + LongSet pinnedChatIds = new LongSet(chatFolder.pinnedChatIds); + LongSet includedChatIds = new LongSet(chatFolder.includedChatIds); + for (long chatId : chatIds) { + if (pinnedChatIds.has(chatId) || includedChatIds.has(chatId)) { + continue; + } + includedChatIds.add(chatId); + } + if (includedChatIds.size() == chatFolder.includedChatIds.length) { + return; + } + int chatCount = pinnedChatIds.size() + includedChatIds.size(); + int secretChatCount = 0; + for (long pinnedChatId : pinnedChatIds) { + if (ChatId.isSecret(pinnedChatId)) secretChatCount++; + } + for (long includedChatId : includedChatIds) { + if (ChatId.isSecret(includedChatId)) secretChatCount++; + } + int nonSecretChatCount = chatCount - secretChatCount; + long chosenChatCountMax = tdlib().chatFolderChosenChatCountMax(); + if (secretChatCount > chosenChatCountMax || nonSecretChatCount > chosenChatCountMax) { + if (hasPremium()) { + CharSequence text = Lang.getMarkdownString(delegate, R.string.ChatsInFolderLimitReached, chosenChatCountMax); + UI.showCustomToast(text, Toast.LENGTH_LONG, 0); + } else { + send(new TdApi.GetPremiumLimit(new TdApi.PremiumLimitTypeChatFolderChosenChatCount()), (premiumLimit, error) -> { + CharSequence text; + if (error != null) { + text = Lang.getMarkdownString(delegate, R.string.ChatsInFolderLimitReached, chosenChatCountMax); + } else { + text = Lang.getMarkdownString(delegate, R.string.PremiumRequiredChatsInFolder, premiumLimit.defaultValue, premiumLimit.premiumValue); + } + UI.showCustomToast(text, Toast.LENGTH_LONG, 0); + }); + } + return; + } + chatFolder.includedChatIds = includedChatIds.toArray(); + chatFolder.excludedChatIds = ArrayUtils.removeAll(chatFolder.excludedChatIds, chatIds); + send(new TdApi.EditChatFolder(chatFolderId, chatFolder), (chatFolderInfo, error) -> { + if (error != null) { + UI.showError(error); + } + }); + } + + public void removeChatFromChatFolder (int chatFolderId, long chatId) { + removeChatsFromChatFolder(chatFolderId, new long[] {chatId}); + } + + public void removeChatsFromChatFolder (int chatFolderId, long[] chatIds) { + if (chatIds.length == 0) { + return; + } + send(new TdApi.GetChatFolder(chatFolderId), (chatFolder, error) -> { + if (error != null) { + UI.showError(error); + } else { + removeChatsFromChatFolder(chatFolderId, chatFolder, chatIds); + } + }); + } + + public void removeChatsFromChatFolder (int chatFolderId, TdApi.ChatFolder chatFolder, long[] chatIds) { + if (chatIds.length == 0) { + return; + } + LongList pinnedChatIds = new LongList(chatFolder.pinnedChatIds); + LongSet includedChatIds = new LongSet(chatFolder.includedChatIds); + LongSet excludedChatIds = new LongSet(chatFolder.excludedChatIds); + for (long chatId : chatIds) { + boolean removed = pinnedChatIds.remove(chatId) | includedChatIds.remove(chatId); + if (removed && Config.CHAT_FOLDERS_SMART_CHAT_DELETION_ENABLED) { + TdApi.Chat chat = chat(chatId); + boolean isBotChat = isBotChat(chat); + boolean isUserChat = isUserChat(chat) && !isBotChat; + boolean isContactChat = isUserChat && TD.isContact(chatUser(chat)); + if (!chatFolder.includeContacts && isUserChat && isContactChat) continue; + if (!chatFolder.includeNonContacts && isUserChat && !isContactChat) continue; + if (!chatFolder.includeGroups && TD.isMultiChat(chat)) continue; + if (!chatFolder.includeChannels && isChannelChat(chat)) continue; + if (!chatFolder.includeBots && isBotChat) continue; + } + excludedChatIds.add(chatId); + } + chatFolder.pinnedChatIds = pinnedChatIds.get(); + chatFolder.includedChatIds = includedChatIds.toArray(); + chatFolder.excludedChatIds = excludedChatIds.toArray(); + send(new TdApi.EditChatFolder(chatFolderId, chatFolder), (chatFolderInfo, error) -> { + if (error != null) { + UI.showError(error); + } + }); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibAccentColor.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibAccentColor.java new file mode 100644 index 0000000000..790988f9cb --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibAccentColor.java @@ -0,0 +1,522 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 04/11/2023 + */ +package org.thunderdog.challegram.telegram; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.Log; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import me.vkryl.core.ColorUtils; +import me.vkryl.core.StringUtils; +import me.vkryl.td.Td; + +public final class TdlibAccentColor { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + InternalId.INACTIVE, + InternalId.SAVED_MESSAGES, + InternalId.REPLIES, + InternalId.ARCHIVE, + InternalId.ARCHIVE_PINNED, + InternalId.REGULAR, + InternalId.FILE_REGULAR + }) + public @interface InternalId { + int + INACTIVE = -1, + SAVED_MESSAGES = -2, + REPLIES = -3, + ARCHIVE = -4, + ARCHIVE_PINNED = -5, + REGULAR = -6, + FILE_REGULAR = -7; + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + BuiltInId.RED, + BuiltInId.ORANGE, + BuiltInId.PURPLE_VIOLET, + BuiltInId.GREEN, + BuiltInId.CYAN, + BuiltInId.BLUE, + BuiltInId.PINK + }) + public @interface BuiltInId { + // Accent color identifier; 0 - red, 1 - orange, 2 - purple/violet, 3 - green, 4 - cyan, 5 - blue, 6 - pink. + int + RED = 0, + ORANGE = 1, + PURPLE_VIOLET = 2, + GREEN = 3, + CYAN = 4, + BLUE = 5, + PINK = 6; + } + public static final int BUILT_IN_COLOR_COUNT = BuiltInId.PINK + 1; + + + private final int id; + private @Nullable TdApi.AccentColor accentColor; + + public TdlibAccentColor (int id) { + this.id = id; + } + + public TdlibAccentColor (@NonNull TdApi.AccentColor accentColor) { + this(accentColor.id); + this.accentColor = accentColor; + } + + public int getId () { + return id; + } + + public TdApi.AccentColor getRemoteAccentColor () { + return accentColor; + } + + boolean updateColor (@NonNull TdApi.AccentColor updatedAccentColor) { + if (Td.equalsTo(this.accentColor, updatedAccentColor)) { + return false; + } + if (this.id != updatedAccentColor.id) { + throw new IllegalArgumentException(this.id + " != " + updatedAccentColor.id); + } + if (this.accentColor != null) { + Td.copyTo(updatedAccentColor, this.accentColor); + } else { + this.accentColor = updatedAccentColor; + } + return true; + } + + @Override + public boolean equals (@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof TdlibAccentColor) { + TdlibAccentColor other = (TdlibAccentColor) obj; + return this.id == other.id && Td.equalsTo(this.accentColor, other.accentColor); + } + return false; + } + + // Utilities + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + UseCase.PRIMARY, + UseCase.PRIMARY_BIG, + UseCase.NAME, + UseCase.LINE + }) + private @interface UseCase { + int PRIMARY = 0, PRIMARY_BIG = 1, NAME = 2, LINE = 3; + } + + private static final int[][] builtInColorIds; + + static { + builtInColorIds = new int[][] { + // 0 - red + new int[] {ColorId.avatarRed, ColorId.avatarRed_big, ColorId.nameRed, ColorId.lineRed}, + // 1 - orange + new int[] {ColorId.avatarOrange, ColorId.avatarOrange_big, ColorId.nameOrange, ColorId.lineOrange}, + // 2 - purple/violet + new int[] {ColorId.avatarViolet, ColorId.avatarViolet_big, ColorId.nameViolet, ColorId.lineViolet}, + // 3 - green + new int[] {ColorId.avatarGreen, ColorId.avatarGreen_big, ColorId.nameGreen, ColorId.lineGreen}, + // 4 - cyan + new int[] {ColorId.avatarCyan, ColorId.avatarCyan_big, ColorId.nameCyan, ColorId.lineCyan}, + // 5 - blue + new int[] {ColorId.avatarBlue, ColorId.avatarBlue_big, ColorId.nameBlue, ColorId.lineBlue}, + // 6 - pink. + new int[] {ColorId.avatarPink, ColorId.avatarPink_big, ColorId.namePink, ColorId.linePink}, + }; + } + + private static @ColorId int accentColorIdToAppColorId (int accentColorId, @UseCase int useCase) { + if (Td.isBuiltInColorId(accentColorId)) { + @BuiltInId int builtInColorId = accentColorId; + return builtInColorIds[builtInColorId][useCase]; + } + if (accentColorId < 0) { + @InternalId int internalAccentColorId = accentColorId; + switch (internalAccentColorId) { + case InternalId.FILE_REGULAR: + switch (useCase) { + case UseCase.NAME: + case UseCase.LINE: + break; // unsupported + case UseCase.PRIMARY: + case UseCase.PRIMARY_BIG: + return ColorId.file; + } + case InternalId.REGULAR: + switch (useCase) { + case UseCase.NAME: + return ColorId.messageAuthor; + case UseCase.LINE: + return ColorId.messageVerticalLine; + case UseCase.PRIMARY: + case UseCase.PRIMARY_BIG: + // Unsupported + break; + } + case InternalId.INACTIVE: + switch (useCase) { + case UseCase.NAME: + return ColorId.nameInactive; + case UseCase.PRIMARY: + return ColorId.avatarInactive; + case UseCase.PRIMARY_BIG: + return ColorId.avatarInactive_big; + case UseCase.LINE: + return ColorId.lineInactive; + } + break; + case InternalId.SAVED_MESSAGES: + switch (useCase) { + case UseCase.NAME: + case UseCase.LINE: + break; // Unsupported + case UseCase.PRIMARY: + return ColorId.avatarSavedMessages; + case UseCase.PRIMARY_BIG: + return ColorId.avatarSavedMessages_big; + } + break; + case InternalId.REPLIES: + switch (useCase) { + case UseCase.NAME: + return ColorId.messageAuthor; + case UseCase.LINE: + return ColorId.messageVerticalLine; + case UseCase.PRIMARY: + return ColorId.avatarReplies; + case UseCase.PRIMARY_BIG: + return ColorId.avatarReplies_big; + } + break; + case InternalId.ARCHIVE: + switch (useCase) { + case UseCase.NAME: + case UseCase.LINE: + break; // unsupported + case UseCase.PRIMARY: + case UseCase.PRIMARY_BIG: + return ColorId.avatarArchive; + } + break; + case InternalId.ARCHIVE_PINNED: + switch (useCase) { + case UseCase.NAME: + case UseCase.LINE: + break; // unsupported + case UseCase.PRIMARY: + case UseCase.PRIMARY_BIG: + return ColorId.avatarArchivePinned; + } + break; + } + return accentColorIdToAppColorId(BuiltInId.BLUE, useCase); + } + return accentColorIdToAppColorId(InternalId.INACTIVE, useCase); + } + + // Get color + + private boolean isBuiltInOrInternalTheme () { + return id < 0 || Td.isBuiltInColorId(id); + } + + private int getTargetBuiltInAccentColorId () { + if (accentColor != null) { + return accentColor.builtInAccentColorId; + } + if (isBuiltInOrInternalTheme()) { + return id; + } + return InternalId.INACTIVE; + } + + private long getComplexColor (@UseCase int useCase, boolean forceBuiltInColor) { + boolean isId; + int color; + if (!forceBuiltInColor && accentColor != null) { + // 2-3 colors are used only for stripe + isId = false; + color = ColorUtils.fromToArgb( + ColorUtils.color(255, accentColor.lightThemeColors[0]), + ColorUtils.color(255, accentColor.darkThemeColors[0]), + Theme.getDarkFactor() + ); + } else { + int accentColorId = getTargetBuiltInAccentColorId(); + isId = true; + color = accentColorIdToAppColorId(accentColorId, useCase); + } + return Theme.newComplexColor(isId, color); + } + + @ColorInt + private int getColor (@UseCase int useCase, boolean forceBuiltInColor) { + long complexColor = getComplexColor(useCase, forceBuiltInColor); + return Theme.toColorInt(complexColor); + } + + public static String getTextRepresentation (@BuiltInId int accentColorId) { + switch (accentColorId) { + case BuiltInId.RED: + return Lang.getString(R.string.AccentColorRed); + case BuiltInId.ORANGE: + return Lang.getString(R.string.AccentColorOrange); + case BuiltInId.PURPLE_VIOLET: + return Lang.getString(R.string.AccentColorPurple); + case BuiltInId.GREEN: + return Lang.getString(R.string.AccentColorGreen); + case BuiltInId.CYAN: + return Lang.getString(R.string.AccentColorCyan); + case BuiltInId.BLUE: + return Lang.getString(R.string.AccentColorBlue); + case BuiltInId.PINK: + return Lang.getString(R.string.AccentColorPink); + } + throw new IllegalArgumentException(Integer.toString(accentColorId)); + } + + public String getTextRepresentation () { + @BuiltInId int accentColorId = getTargetBuiltInAccentColorId(); + if (accentColor != null) { + int[] colors = Theme.isDark() ? accentColor.darkThemeColors : accentColor.lightThemeColors; + if (colors.length > 1) { + // TODO what to display? + } + } + return getTextRepresentation(accentColorId); + } + + @ColorInt + public int getPrimaryColor () { + return getColor(UseCase.PRIMARY, false); + } + + @ColorInt + public int getVerticalLineColor () { + return getColor(UseCase.LINE, true); + } + + public long getPrimaryComplexColor () { + return getComplexColor(UseCase.PRIMARY, false); + } + + @ColorInt + public int getPrimaryContentColor () { + return Theme.toColorInt(getPrimaryContentComplexColor()); + } + + public long getPrimaryContentComplexColor () { + return Theme.newComplexColor(true, ColorId.avatar_content); + } + + public int getPrimaryBigColor () { + return getColor(UseCase.PRIMARY_BIG, false); + } + + public int getNameColor () { + return getColor(UseCase.NAME, true); + } + + public long getNameComplexColor () { + return getComplexColor(UseCase.NAME, true); + } + + // Utils + + public static TdlibAccentColor defaultAccentColorForUserId (Tdlib tdlib, long possiblyFakeUserId) { + int accentColorId = defaultAccentColorIdForUserId(possiblyFakeUserId); + return tdlib.accentColor(accentColorId); + } + + public static int defaultAccentColorIdForUserId (long userId) { + if (userId >= 0 && userId < BUILT_IN_COLOR_COUNT) { + return (int) userId; + } + try { + String str = String.valueOf(userId); + if (str.length() > 15) { + str = str.substring(0, 15); + } + java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(str.getBytes(StringUtils.UTF_8)); + int b = digest[(int) Math.abs(userId % 16)]; + if (b < 0) { + b += 256; + } + return Math.abs(b) % BUILT_IN_COLOR_COUNT; + } catch (Throwable t) { + Log.e("Cannot calculate default user color", t); + } + return (int) Math.abs(userId % BUILT_IN_COLOR_COUNT); + } + + @ColorId + + public static int getFileColorId (TdApi.Document doc, boolean isOutBubble) { + return getFileColorId(doc.fileName, doc.mimeType, isOutBubble); + } + + @ColorId + public static int getFileColorId (String fileName, @Nullable String mimeType, boolean isOutBubble) { + String mime = mimeType != null ? mimeType.toLowerCase() : null; + int i = fileName.lastIndexOf('.'); + String ext = i != -1 ? fileName.substring(i + 1).toLowerCase() : ""; + + // Android APKs + if ("application/vnd.android.package-archive".equals(mime) || "apk".equals(ext)) { + return ColorId.fileGreen; + } + + if ( + "7z".equals(ext) || "application/x-7z-compressed".equals(mime) || + "zip".equals(ext) || "application/zip".equals(mime) || + "rar".equals(ext) || "application/x-rar-compressed".equals(mime) + ) { + return ColorId.fileYellow; + } + + if ( + "pdf".equals(ext) || "application/pdf".equals(mime) + ) { + return ColorId.fileRed; + } + + return isOutBubble ? ColorId.bubbleOut_file : ColorId.file; + } + + /*public static int getColorIdForString (String string) { + switch (Math.abs(string.hashCode()) % 3) { + case 0: return ColorId.fileYellow; + case 1: return ColorId.fileRed; + case 2: return ColorId.fileGreen; + } + return ColorId.file; + } + + public static int getColorIdForName (String name) { + return color_ids[MathUtils.pickNumber(color_ids.length, name)]; + } + + public static int getNameColorId (@ColorId int avatarColorId) { + switch (avatarColorId) { + case ColorId.avatarRed: + return ColorId.nameRed; + case ColorId.avatarOrange: + return ColorId.nameOrange; + case ColorId.avatarYellow: + return ColorId.nameYellow; + case ColorId.avatarGreen: + return ColorId.nameGreen; + case ColorId.avatarCyan: + return ColorId.nameCyan; + case ColorId.avatarBlue: + return ColorId.nameBlue; + case ColorId.avatarViolet: + return ColorId.nameViolet; + case ColorId.avatarPink: + return ColorId.namePink; + case ColorId.avatarSavedMessages: + return ColorId.messageAuthor; + case ColorId.avatarInactive: + return ColorId.nameInactive; + } + return ColorId.messageAuthor; + } + + public static int calculateTdlibAccentColorId (long userId) { + int index = calculateLegacyColorIndex(0, userId); + // Accent color identifier; 0 - red, 1 - orange, 2 - purple/violet, 3 - green, 4 - cyan, 5 - blue, 6 - pink. + switch (index) { + case 0: // ColorId.avatarRed + case 1: // ColorId.avatarOrange + case 3: // ColorId.avatarGreen + case 4: // ColorId.avatarCyan + case 5: // ColorId.avatarBlue + return index; + case 6: // ColorId.avatarViolet + return 2; // purple/violet + case 2: // ColorId.avatarYellow + return 1; // orange, as yellow was removed + case 7: // ColorId.avatarPink + return 0; + default: // unreachable + throw new IllegalStateException(Integer.toString(index)); + } + } + */ + + /*private static final int[] color_ids = { + ColorId.avatarRed *//* red 0 *//*, + ColorId.avatarOrange *//* orange 1 *//*, + ColorId.avatarYellow *//* yellow 2 *//*, + ColorId.avatarGreen *//* green 3 *//*, + ColorId.avatarCyan *//* cyan 4 *//*, + ColorId.avatarBlue *//* blue 5 *//*, + ColorId.avatarViolet *//* violet 6 *//*, + ColorId.avatarPink *//* pink 7 *//* + };*/ + + /*private static int calculateLegacyColorIndex (long selfUserId, long id) { + if (id >= 0 && id < color_ids.length) { + return (int) id; + } + try { + String str; + if (id >= 0 && selfUserId != 0) { + str = String.format(Locale.US, "%d%d", id, selfUserId); + } else { + str = String.format(Locale.US, "%d", id); + } + if (str.length() > 15) { + str = str.substring(0, 15); + } + java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(str.getBytes(StringUtils.UTF_8)); + int b = digest[(int) Math.abs(id % 16)]; + if (b < 0) { + b += 256; + } + return Math.abs(b) % color_ids.length; + } catch (Throwable t) { + Log.e("Cannot calculate user color", t); + } + + return (int) Math.abs(id % color_ids.length); + }*/ +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibAccount.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibAccount.java index fba51c5d76..d818ca6579 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibAccount.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibAccount.java @@ -31,7 +31,6 @@ import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.ImageFileLocal; -import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.unsorted.Settings; @@ -51,7 +50,8 @@ public class TdlibAccount implements Comparable, TdlibProvider { public static final int VERSION_1 = 1; public static final int VERSION_2 = 2; - public static final int VERSION = VERSION_2; + public static final int VERSION_3 = 3; + public static final int VERSION = VERSION_3; private static final int FLAG_UNAUTHORIZED = 1; private static final int FLAG_DEBUG = 1 << 1; @@ -114,9 +114,9 @@ public boolean isSameAs (TdlibAccount o) { @Override public final int compareTo (@NonNull TdlibAccount o) { - if (this.order != o.order) { - int x = this.order != -1 ? this.order : Integer.MAX_VALUE; - int y = o.order != -1 ? o.order : Integer.MAX_VALUE; + int x = this.order != -1 ? this.order : Integer.MAX_VALUE; + int y = o.order != -1 ? o.order : Integer.MAX_VALUE; + if (x != y) { return Integer.compare(x, y); } if (this.modificationTime != o.modificationTime) { @@ -126,15 +126,19 @@ public final int compareTo (@NonNull TdlibAccount o) { } private void restore (RandomAccessFile r, int version, boolean allowIntegrityChecks) throws IOException { - this.flags = r.readByte(); - this.knownUserId = version == VERSION_2 ? r.readLong() : r.readInt(); + if (version >= VERSION_3) { + this.flags = r.readInt(); + } else { + this.flags = r.read(); + } + this.knownUserId = version >= VERSION_2 ? r.readLong() : r.readInt(); this.modificationTime = r.readLong(); this.order = r.readInt(); boolean integrityCheckFailed = false; if (allowIntegrityChecks) { if (BitwiseUtils.hasFlag(flags, FLAG_SERVICE | FLAG_DEBUG) && !Settings.instance().allowSpecialTdlibInstanceMode(id)) { - int flags = this.flags & ~FLAG_DEBUG; - flags &= ~FLAG_SERVICE; + int flags = BitwiseUtils.setFlag(this.flags, FLAG_SERVICE, false); + flags = BitwiseUtils.setFlag(flags, FLAG_DEBUG, false); this.flags = flags; integrityCheckFailed = true; } @@ -142,10 +146,10 @@ private void restore (RandomAccessFile r, int version, boolean allowIntegrityChe Log.i(Log.TAG_ACCOUNTS, "restored accountId:%d flags:%d userId:%d time:%d order:%d integrity_check_failed:%b", id, flags, knownUserId, modificationTime, order, integrityCheckFailed); } - static final int SIZE_PER_ENTRY = 1 /*flags*/ + 8 /*knownUserId*/ + 8 /*modification_time*/ + 4 /*order*/; + static final int SIZE_PER_ENTRY = 4 /*flags*/ + 8 /*knownUserId*/ + 8 /*modification_time*/ + 4 /*order*/; void save (RandomAccessFile r) throws IOException { - r.write(flags); + r.writeInt(flags); r.writeLong(knownUserId); r.writeLong(modificationTime); r.writeInt(order); @@ -153,7 +157,7 @@ void save (RandomAccessFile r) throws IOException { int saveOrder (RandomAccessFile r, final int position) throws IOException { int skipSize = - 1 /*flags*/ + 4 /*flags*/ + 8 /*knownUserId*/ + 8 /*modificationTime*/; r.seek(position + skipSize); @@ -163,7 +167,7 @@ int saveOrder (RandomAccessFile r, final int position) throws IOException { int saveFlags (RandomAccessFile r, final int position) throws IOException { r.seek(position); - r.write(flags); + r.writeInt(flags); return position + SIZE_PER_ENTRY; } @@ -471,12 +475,12 @@ boolean comparePhoneNumber (String phoneNumber) { private ImageFile avatarSmallFile, avatarBigFile; private DisplayInformation displayInformation; - void storeUserInformation (@Nullable TdApi.User user, @Nullable TdApi.Sticker emojiStatus) { + void storeUserInformation (@Nullable TdApi.User user, @Nullable TdApi.AccentColor accentColor, @Nullable TdApi.Sticker emojiStatus) { avatarSmallFile = avatarBigFile = null; if (user != null && user.id == knownUserId) { String prefix = Settings.accountInfoPrefix(id); boolean isUpdate = Settings.instance().getLong(prefix, 0) == user.id; - displayInformation = new DisplayInformation(prefix, user, emojiStatus, isUpdate); + displayInformation = new DisplayInformation(prefix, user, accentColor, emojiStatus, isUpdate); } else { deleteDisplayInformation(); counters.clear(); @@ -585,10 +589,14 @@ public AvatarPlaceholder.Metadata getAvatarPlaceholderMetadata () { if (user != null) return tdlib.cache().userPlaceholderMetadata(user, false); DisplayInformation info = getDisplayInformation(); - if (info != null) - return new AvatarPlaceholder.Metadata(TD.getAvatarColorId(knownUserId, knownUserId), TD.getLetters(info.getFirstName(), info.getLastName())); - if (knownUserId != 0) - return new AvatarPlaceholder.Metadata(TD.getAvatarColorId(knownUserId, knownUserId)); + if (info != null) { + TdlibAccentColor accentColor = info.getAccentColor(); + return new AvatarPlaceholder.Metadata(accentColor, TD.getLetters(info.getFirstName(), info.getLastName())); + } + if (knownUserId != 0) { + int accentColorId = TdlibAccentColor.defaultAccentColorIdForUserId(knownUserId); + return new AvatarPlaceholder.Metadata(new TdlibAccentColor(accentColorId)); + } return null; } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibBadgeCounter.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibBadgeCounter.java index e297ba73b8..be106190df 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibBadgeCounter.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibBadgeCounter.java @@ -39,9 +39,15 @@ boolean reset (TdlibAccount account) { } void add (TdlibBadgeCounter counter) { - this.count += counter.count; - if (!counter.isMuted) - this.isMuted = false; + if (counter.count > 0) { + if (this.count == 0) { + this.isMuted = counter.isMuted; + } + this.count += counter.count; + if (!counter.isMuted) { + this.isMuted = false; + } + } } boolean reset (TdlibCounter counter, @Nullable TdlibCounter archiveCounter) { diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibCache.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibCache.java index cb1955fcbc..1b44817e2e 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibCache.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibCache.java @@ -38,9 +38,9 @@ import org.thunderdog.challegram.data.AvatarPlaceholder; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.loader.ImageFile; -import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.util.AppInstallationUtil; import org.thunderdog.challegram.util.DrawableProvider; import org.thunderdog.challegram.util.text.Letters; import org.thunderdog.challegram.voip.annotation.CallState; @@ -55,6 +55,7 @@ import me.vkryl.core.ArrayUtils; import me.vkryl.core.collection.LongSparseIntArray; +import me.vkryl.core.collection.LongSparseLongArray; import me.vkryl.core.lambda.CancellableRunnable; import me.vkryl.core.lambda.RunnableData; import me.vkryl.core.reference.ReferenceIntMap; @@ -125,6 +126,7 @@ public interface ChatMemberStatusChangeListener { private final HashMap supergroups = new HashMap<>(); private final HashMap supergroupsFulls = new HashMap<>(); + private final LongSparseLongArray supergroupsFullsLastUpdateTime = new LongSparseLongArray(); private final ReferenceList supergroupsGlobalListeners = new ReferenceList<>(); private final ReferenceLongMap supergroupListeners = new ReferenceLongMap<>(); @@ -143,7 +145,8 @@ public interface ChatMemberStatusChangeListener { private final ArrayList outputLocations = new ArrayList<>(); private boolean loadingMyUser; - private final Client.ResultHandler meHandler, dataHandler; + private final Tdlib.ResultHandler meHandler; + private final Client.ResultHandler dataHandler; private final LongSparseIntArray pendingStatusRefresh = new LongSparseIntArray(); private final Handler onlineHandler; @@ -206,20 +209,11 @@ private void onUserStatusUpdate (long userId, int wasOnline, @Nullable TdApi.Use TdlibCache (Tdlib tdlib) { this.tdlib = tdlib; - this.meHandler = object -> { - switch (object.getConstructor()) { - case TdApi.User.CONSTRUCTOR: { - loadingMyUser = false; - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.GetMe.class, TdApi.User.class, TdApi.Error.class); - break; - } + this.meHandler = (user, error) -> { + if (error != null) { + UI.showError(error); + } else { + loadingMyUser = false; } }; this.dataHandler = object -> { @@ -253,8 +247,7 @@ private void onUserStatusUpdate (long userId, int wasOnline, @Nullable TdApi.Use break; } default: { - Log.unexpectedTdlibResponse(object, TdApi.GetUserFullInfo.class, TdApi.UserFullInfo.class, TdApi.BasicGroupFullInfo.class, TdApi.SupergroupFullInfo.class, TdApi.Error.class, TdApi.User.class); - break; + throw new UnsupportedOperationException(object.toString()); } } }; @@ -264,12 +257,12 @@ private void onUserStatusUpdate (long userId, int wasOnline, @Nullable TdApi.Use tdlib.listeners().addCleanupListener(this); UI.addStateListener(this); - this.refreshUiPaused = UI.getUiState() != UI.STATE_RESUMED; + this.refreshUiPaused = UI.getUiState() != UI.State.RESUMED; } @Override public void onUiStateChanged (int newState) { - setPauseStatusRefreshers(newState != UI.STATE_RESUMED); + setPauseStatusRefreshers(newState != UI.State.RESUMED); } public void getInviteText (@Nullable final RunnableData callback) { @@ -281,10 +274,17 @@ public void getInviteText (@Nullable final RunnableData callback) { }); } - public void getDownloadUrl (@Nullable final RunnableData callback) { + private @NonNull AppInstallationUtil.DownloadUrl toDownloadUrl (@Nullable TdApi.HttpUrl url) { + if (url != null && tdlib.hasUrgentInAppUpdate()) { + return new AppInstallationUtil.DownloadUrl(AppInstallationUtil.InstallerId.UNKNOWN, url.url); + } + return AppInstallationUtil.getDownloadUrl(url != null ? url.url : null); + } + + public void getDownloadUrl (@Nullable final RunnableData callback) { if (downloadUrl != null) { if (callback != null) { - callback.runWithData(downloadUrl); + callback.runWithData(toDownloadUrl(downloadUrl)); } return; } @@ -292,32 +292,22 @@ public void getDownloadUrl (@Nullable final RunnableData callback @Override public void act () { if (callback != null) { - callback.runWithData(new TdApi.HttpUrl(BuildConfig.DOWNLOAD_URL)); + callback.runWithData(toDownloadUrl(null)); } cancel(); } }; - tdlib.client().send(new TdApi.GetApplicationDownloadLink(), object -> { - switch (object.getConstructor()) { - case TdApi.HttpUrl.CONSTRUCTOR: { - TdApi.HttpUrl httpUrl = (TdApi.HttpUrl) object; - if (Strings.isValidLink(httpUrl.url)) { - tdlib.ui().post(() -> { - downloadUrl = httpUrl; - if (callback != null && fallback.isPending()) { - callback.runWithData(httpUrl); - fallback.cancel(); - } - }); - } else { - tdlib.ui().post(fallback); + tdlib.send(new TdApi.GetApplicationDownloadLink(), (httpUrl, error) -> { + if (error != null || !Strings.isValidLink(httpUrl.url)) { + tdlib.ui().post(fallback); + } else { + tdlib.ui().post(() -> { + downloadUrl = httpUrl; + if (callback != null && fallback.isPending()) { + callback.runWithData(toDownloadUrl(httpUrl)); + fallback.cancel(); } - break; - } - case TdApi.Error.CONSTRUCTOR: { - tdlib.ui().post(fallback); - break; - } + }); } }); if (tdlib.context().watchDog().isOnline()) { @@ -436,7 +426,7 @@ void onUpdateMyUserId (long userId) { tdlib.downloadMyUser(myUser); } else if (!loadingMyUser) { loadingMyUser = true; - tdlib.client().send(new TdApi.GetMe(), meHandler); + tdlib.send(new TdApi.GetMe(), meHandler); } } else { notifyMyUserListeners(myUserListeners.iterator(), null); @@ -450,27 +440,32 @@ void onUpdateMyUserId (long userId) { @TdlibThread void onUpdateUser (TdApi.UpdateUser update) { - boolean statusChanged; - boolean isMe; - boolean hadUser; + final boolean statusChanged; + final boolean hadUser; + final boolean isContactChanged; + final boolean isContact; TdApi.User newUser = update.user; synchronized (dataLock) { TdApi.User oldUser = users.get(newUser.id); - if (hadUser = oldUser != null) { + hadUser = oldUser != null; + isContact = newUser.isContact; + if (hadUser) { statusChanged = !Td.equalsTo(oldUser.status, newUser.status); + isContactChanged = oldUser.isContact != newUser.isContact; Td.copyTo(newUser, oldUser); synchronized (onlineMutex) { oldUser.status = newUser.status; } newUser = oldUser; } else { - statusChanged = false; + statusChanged = isContactChanged = false; users.put(newUser.id, newUser); } } notifyUserListeners(newUser); - if (isMe = (newUser.id == myUserId)) { + boolean isMe = (newUser.id == myUserId); + if (isMe) { notifyMyUserListeners(myUserListeners.iterator(), newUser); tdlib.downloadMyUser(newUser); tdlib.context().onUpdateAccountProfile(tdlib.id(), newUser, true); @@ -490,6 +485,10 @@ void onUpdateUser (TdApi.UpdateUser update) { TdlibNotificationChannelGroup.updateGroup(newUser); } tdlib.context().onUpdateAccountProfile(tdlib.id(), newUser, !hadUser); + } else { + if (isContactChanged) { + tdlib.contacts().notifyContactStatusChanged(newUser.id, isContact); + } } } @@ -731,45 +730,45 @@ public void onUpdateCallSettings (int callId, CallSettings settings) { // Listeners - public void subscribeToAnyUpdates (Object any) { - if (any instanceof UserDataChangeListener) { - __putGlobalUserDataListener((UserDataChangeListener) any); + public void subscribeForGlobalUpdates (Object globalListener) { + if (globalListener instanceof UserDataChangeListener) { + __putGlobalUserDataListener((UserDataChangeListener) globalListener); } - if (any instanceof UserStatusChangeListener) { - __putGlobalStatusListener((UserStatusChangeListener) any); + if (globalListener instanceof UserStatusChangeListener) { + __putGlobalStatusListener((UserStatusChangeListener) globalListener); } - if (any instanceof BasicGroupDataChangeListener) { - putGlobalBasicGroupListener((BasicGroupDataChangeListener) any); + if (globalListener instanceof BasicGroupDataChangeListener) { + putGlobalBasicGroupListener((BasicGroupDataChangeListener) globalListener); } - if (any instanceof SupergroupDataChangeListener) { - putGlobalSupergroupListener((SupergroupDataChangeListener) any); + if (globalListener instanceof SupergroupDataChangeListener) { + putGlobalSupergroupListener((SupergroupDataChangeListener) globalListener); } - if (any instanceof SecretChatDataChangeListener) { - putGlobalSecretChatListener((SecretChatDataChangeListener) any); + if (globalListener instanceof SecretChatDataChangeListener) { + putGlobalSecretChatListener((SecretChatDataChangeListener) globalListener); } - if (any instanceof CallStateChangeListener) { - putGlobalCallListener((CallStateChangeListener) any); + if (globalListener instanceof CallStateChangeListener) { + putGlobalCallListener((CallStateChangeListener) globalListener); } } - public void unsubscribeFromAnyUpdates (Object any) { - if (any instanceof UserDataChangeListener) { - __deleteGlobalUserDataListener((UserDataChangeListener) any); + public void unsubscribeFromGlobalUpdates (Object globalListener) { + if (globalListener instanceof UserDataChangeListener) { + __deleteGlobalUserDataListener((UserDataChangeListener) globalListener); } - if (any instanceof UserStatusChangeListener) { - __deleteGlobalStatusListener((UserStatusChangeListener) any); + if (globalListener instanceof UserStatusChangeListener) { + __deleteGlobalStatusListener((UserStatusChangeListener) globalListener); } - if (any instanceof BasicGroupDataChangeListener) { - deleteGlobalBasicGroupListener((BasicGroupDataChangeListener) any); + if (globalListener instanceof BasicGroupDataChangeListener) { + deleteGlobalBasicGroupListener((BasicGroupDataChangeListener) globalListener); } - if (any instanceof SupergroupDataChangeListener) { - deleteGlobalSupergroupListener((SupergroupDataChangeListener) any); + if (globalListener instanceof SupergroupDataChangeListener) { + deleteGlobalSupergroupListener((SupergroupDataChangeListener) globalListener); } - if (any instanceof SecretChatDataChangeListener) { - deleteGlobalSecretChatListener((SecretChatDataChangeListener) any); + if (globalListener instanceof SecretChatDataChangeListener) { + deleteGlobalSecretChatListener((SecretChatDataChangeListener) globalListener); } - if (any instanceof CallStateChangeListener) { - deleteGlobalCallListener((CallStateChangeListener) any); + if (globalListener instanceof CallStateChangeListener) { + deleteGlobalCallListener((CallStateChangeListener) globalListener); } } @@ -939,12 +938,24 @@ public boolean userGeneral (long userId) { return userId != 0 && TD.isGeneralUser(user(userId)); } - public int userAvatarColorId (long userId) { - return userAvatarColorId(userId != 0 ? user(userId) : null); + public int userAccentColorId (long userId) { + return userAccentColorId(userId != 0 ? user(userId) : null); + } + + public int userAccentColorId (TdApi.User user) { + if (user != null) { + return user.accentColorId; + } else { + return TdlibAccentColor.InternalId.INACTIVE; + } + } + + public TdlibAccentColor userAccentColor (long userId) { + return tdlib.chatAccentColor(ChatId.fromUserId(userId)); } - public int userAvatarColorId (TdApi.User user) { - return TD.getAvatarColorId(user == null || TD.isUserDeleted(user) ? -1 : user.id, myUserId); + public TdlibAccentColor userAccentColor (TdApi.User user) { + return tdlib.chatAccentColor(user != null ? ChatId.fromUserId(user.id) : 0); } public Letters userLetters (TdApi.User user) { @@ -957,34 +968,38 @@ public Letters userLetters (long userId) { } public AvatarPlaceholder.Metadata selfPlaceholderMetadata () { - return new AvatarPlaceholder.Metadata(ColorId.avatarSavedMessages, (String) null, R.drawable.baseline_bookmark_24, 0); + return new AvatarPlaceholder.Metadata(tdlib.accentColor(TdlibAccentColor.InternalId.SAVED_MESSAGES), null, R.drawable.baseline_bookmark_24, 0); + } + + public AvatarPlaceholder.Metadata repliesPlaceholderMetadata () { + return new AvatarPlaceholder.Metadata(tdlib.accentColor(TdlibAccentColor.InternalId.REPLIES), null, R.drawable.baseline_reply_24, R.drawable.baseline_reply_56); + } + + public AvatarPlaceholder.Metadata deletedPlaceholderMetadata () { + return new AvatarPlaceholder.Metadata(tdlib.accentColor(TdlibAccentColor.InternalId.INACTIVE), null, R.drawable.baseline_ghost_24, R.drawable.baseline_ghost_56); } public AvatarPlaceholder.Metadata userPlaceholderMetadata (@Nullable TdApi.User user, boolean allowSavedMessages) { if (user == null) { return null; } - Letters avatarLetters = null; - int avatarColorId; - int desiredDrawableRes = 0; - int extraDrawableRes = 0; if (allowSavedMessages && tdlib.isSelfUserId(user.id)) { - avatarColorId = ColorId.avatarSavedMessages; - desiredDrawableRes = R.drawable.baseline_bookmark_24; - } else { - if (tdlib.isRepliesChat(ChatId.fromUserId(user.id))) { - desiredDrawableRes = R.drawable.baseline_reply_24; - avatarColorId = ColorId.avatarReplies; - } else { - avatarLetters = userLetters(user); - avatarColorId = userAvatarColorId(user); - } - extraDrawableRes = /*tdlib.isSelfUserId(user.id) ? R.drawable.baseline_add_a_photo_56 :*/ - tdlib.isRepliesChat(ChatId.fromUserId(user.id)) ? R.drawable.baseline_reply_56 : - TD.isBot(user) ? R.drawable.deproko_baseline_bots_56 : - R.drawable.baseline_person_56; + return selfPlaceholderMetadata(); + } + long chatId = ChatId.fromUserId(user.id); + if (tdlib.isRepliesChat(chatId)) { + return repliesPlaceholderMetadata(); + } + if (userDeleted(user.id)) { + return deletedPlaceholderMetadata(); } - return new AvatarPlaceholder.Metadata(avatarColorId, avatarLetters != null ? avatarLetters.text : null, desiredDrawableRes, extraDrawableRes); + TdlibAccentColor accentColor = userAccentColor(user); + Letters avatarLetters = userLetters(user); + int extraDrawableRes = /*tdlib.isSelfUserId(user.id) ? R.drawable.baseline_add_a_photo_56 :*/ + /*tdlib.isRepliesChat(ChatId.fromUserId(user.id)) ? R.drawable.baseline_reply_56 :*/ + TD.isBot(user) ? R.drawable.deproko_baseline_bots_56 : + R.drawable.baseline_person_56; + return new AvatarPlaceholder.Metadata(accentColor, avatarLetters, 0, extraDrawableRes); } public AvatarPlaceholder userPlaceholder (long userId, boolean allowSavedMessages, float radius, @Nullable DrawableProvider provider) { @@ -1015,7 +1030,7 @@ public AvatarPlaceholder.Metadata userPlaceholderMetadata (long userId, @Nullabl if (user != null || userId == 0) { return userPlaceholderMetadata(user, allowSavedMessages); } else { - return new AvatarPlaceholder.Metadata(TD.getAvatarColorId(userId, tdlib.myUserId())); + return new AvatarPlaceholder.Metadata(userAccentColor(userId)); } } @@ -1273,6 +1288,19 @@ public TdApi.SupergroupFullInfo supergroupFull (long supergroupId, boolean allow return result; } + public long getSlowModeDelayExpiresIn (long supergroupId, TimeUnit timeUnit) { + synchronized (dataLock) { + final long lastUpdateTime = supergroupsFullsLastUpdateTime.get(supergroupId, 0); + final TdApi.SupergroupFullInfo supergroupFullInfo = supergroupsFulls.get(supergroupId); + if (supergroupFullInfo != null) { + final long delayExpiresInMillis = TimeUnit.SECONDS.toMillis((long) supergroupFullInfo.slowModeDelayExpiresIn); + return timeUnit.convert(Math.max(0, delayExpiresInMillis - (SystemClock.uptimeMillis() - lastUpdateTime)), TimeUnit.MILLISECONDS); + } + } + + return 0; + } + public void supergroupFull (long supergroupId, RunnableData callback) { if (supergroupId == 0) { if (callback != null) { @@ -1385,7 +1413,7 @@ void onScheduledRemove (TdApi.Message outputLocation) { } void addOutputLocationMessage (TdApi.Message message) { - if (message.sendingState != null || !message.canBeEdited || !message.isOutgoing || message.content.getConstructor() != TdApi.MessageLocation.CONSTRUCTOR) { + if (message.sendingState != null || !message.canBeEdited || !message.isOutgoing || !Td.isLocation(message.content)) { return; } TdApi.MessageLocation location = (TdApi.MessageLocation) message.content; @@ -1551,7 +1579,7 @@ public void onLiveLocationBroadcast (@Nullable TdApi.Location location, int head case TdApi.Message.CONSTRUCTOR: { TdApi.Message resultMessage = (TdApi.Message) object; message.editDate = resultMessage.editDate; - if (resultMessage.content.getConstructor() == TdApi.MessageLocation.CONSTRUCTOR) { + if (Td.isLocation(resultMessage.content)) { TdApi.MessageLocation in = (TdApi.MessageLocation) resultMessage.content; TdApi.MessageLocation out = (TdApi.MessageLocation) message.content; out.expiresIn = in.livePeriod; @@ -1885,6 +1913,7 @@ private int putSupergroup (TdApi.Supergroup supergroup) { private boolean putSupergroupFull (long supergroupId, TdApi.SupergroupFullInfo supergroupFull) { supergroupsFulls.put(supergroupId, supergroupFull); + supergroupsFullsLastUpdateTime.put(supergroupId, SystemClock.uptimeMillis()); return true; } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibChatList.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibChatList.java index 64890dbfae..e776d2eabd 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibChatList.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibChatList.java @@ -394,11 +394,12 @@ void onUpdateChatPosition (TdApi.Chat chat, Tdlib.ChatChange changeInfo) { // Internal private void addChatToList (Entry entry, Tdlib.ChatChange changeInfo) { - int atIndex = Collections.binarySearch(this.list, entry, this); - if (atIndex >= 0) - throw new IllegalStateException(); - atIndex = atIndex * -1 - 1; + int atIndex; synchronized (list) { + atIndex = Collections.binarySearch(this.list, entry, this); + if (atIndex >= 0) + throw new IllegalStateException(); + atIndex = atIndex * -1 - 1; list.add(atIndex, entry); } for (RunnableData perChatCallback : perChatCallbacks) { diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibChatListSlice.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibChatListSlice.java index 0890a1af06..fe345f87ff 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibChatListSlice.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibChatListSlice.java @@ -78,6 +78,14 @@ private int loadedCount () { } } + public TdlibChatList sourceList () { + return sourceList; + } + + public TdApi.ChatList chatList () { + return sourceList.chatList(); + } + public boolean needProgressPlaceholder () { return displayCount == 0 && !sourceList.isEndReached(); } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibContactManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibContactManager.java index b4d050f098..6321e8538b 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibContactManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibContactManager.java @@ -45,6 +45,7 @@ import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.unsorted.Settings; +import org.thunderdog.challegram.util.RateLimiter; import org.thunderdog.challegram.util.text.Letters; import java.lang.ref.Reference; @@ -119,6 +120,9 @@ private String key (String key) { TdlibContactManager (Tdlib tdlib) { this.tdlib = tdlib; + this.checkLimiter = new RateLimiter(() -> { + tdlib.searchContacts(null, 5, newHandler()); + }, 200L, null); this.handler = new ContactHandler(this); tdlib.listeners().addCleanupListener(this); } @@ -401,8 +405,15 @@ private void setUnregisteredContactsImpl (final ArrayList c } } + private final RateLimiter checkLimiter; + private void checkRegisteredCount () { - tdlib.searchContacts(null, 5, newHandler()); + checkLimiter.run(); + } + + @TdlibThread + void notifyContactStatusChanged (long userId, boolean isContact) { + checkRegisteredCount(); } private Client.ResultHandler newHandler () { diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibEntitySpan.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibEntitySpan.java new file mode 100644 index 0000000000..6a280b3be8 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibEntitySpan.java @@ -0,0 +1,22 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 05/11/2023 + */ +package org.thunderdog.challegram.telegram; + +import org.drinkless.tdlib.TdApi; + +public interface TdlibEntitySpan { + void setTextEntityType (TdApi.TextEntityType type); + TdApi.TextEntityType getTextEntityType (); +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibException.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibException.java new file mode 100644 index 0000000000..404ee3dec5 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibException.java @@ -0,0 +1,39 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 17/07/2023 + */ +package org.thunderdog.challegram.telegram; + +import androidx.annotation.NonNull; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.data.TD; + +public class TdlibException extends RuntimeException { + private final TdApi.Error error; + + public TdlibException (@NonNull TdApi.Error error) { + super(TD.toErrorString(error)); + this.error = error; + } + + public TdApi.Error getError () { + return error; + } + + @Override + @NonNull + public String toString () { + return TD.toErrorString(error); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibFilesManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibFilesManager.java index 41ca4f916d..5aeabd7ce2 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibFilesManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibFilesManager.java @@ -609,20 +609,6 @@ public void removeGlobalListener (@NonNull FileListener listener) { // Update handlers - public static boolean isDownloadableContentType (@NonNull TdApi.MessageContent content) { - switch (content.getConstructor()) { - case TdApi.MessageAnimation.CONSTRUCTOR: - case TdApi.MessageAudio.CONSTRUCTOR: - case TdApi.MessageVideo.CONSTRUCTOR: - case TdApi.MessagePhoto.CONSTRUCTOR: - case TdApi.MessageVoiceNote.CONSTRUCTOR: - case TdApi.MessageDocument.CONSTRUCTOR: { - return true; - } - } - return false; - } - public void onFileUpdate (TdApi.UpdateFile update) { // pendingOperations.get() synchronized (this) { diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibListeners.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibListeners.java index ce9c335e21..a2540a06ee 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibListeners.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibListeners.java @@ -40,12 +40,13 @@ public class TdlibListeners { final ReferenceList messageListeners; final ReferenceList messageEditListeners; final ReferenceList chatListeners; + final ReferenceList chatFoldersListeners; final ReferenceMap chatListListeners; + final ReferenceList storyListeners; final ReferenceList settingsListeners; final ReferenceList stickersListeners; final ReferenceList animationsListeners; final ReferenceList connectionListeners; - final ReferenceList dayListeners; final ReferenceList authorizationListeners; final ReferenceList componentDelegates; final ReferenceList optionListeners; @@ -64,6 +65,7 @@ public class TdlibListeners { final ReferenceLongMap messageChatListeners; final ReferenceLongMap messageEditChatListeners; final ReferenceLongMap specificChatListeners; + final ReferenceMap specificStoryListeners; final ReferenceMap specificForumTopicListeners; final ReferenceLongMap chatSettingsListeners; final ReferenceIntMap fileUpdateListeners; @@ -79,12 +81,13 @@ public TdlibListeners (Tdlib tdlib) { this.messageListeners = new ReferenceList<>(); this.messageEditListeners = new ReferenceList<>(); this.chatListeners = new ReferenceList<>(); + this.storyListeners = new ReferenceList<>(); this.chatListListeners = new ReferenceMap<>(true); + this.chatFoldersListeners = new ReferenceList<>(true); this.settingsListeners = new ReferenceList<>(true); this.stickersListeners = new ReferenceList<>(true); this.animationsListeners = new ReferenceList<>(); this.connectionListeners = new ReferenceList<>(true); - this.dayListeners = new ReferenceList<>(); this.authorizationListeners = new ReferenceList<>(true); this.componentDelegates = new ReferenceList<>(true); this.optionListeners = new ReferenceList<>(true); @@ -105,6 +108,7 @@ public TdlibListeners (Tdlib tdlib) { this.messageChatListeners = new ReferenceLongMap<>(); this.messageEditChatListeners = new ReferenceLongMap<>(); this.specificChatListeners = new ReferenceLongMap<>(); + this.specificStoryListeners = new ReferenceMap<>(); this.specificForumTopicListeners = new ReferenceMap<>(true); this.chatSettingsListeners = new ReferenceLongMap<>(true); this.fileUpdateListeners = new ReferenceIntMap<>(); @@ -130,109 +134,103 @@ public void unsubscribeFromUpdates (TdApi.Message message) { } @AnyThread - public void subscribeForAnyUpdates (Object any) { + public void subscribeForGlobalUpdates (Object globalListener) { synchronized (this) { - if (any instanceof MessageListener) { - messageListeners.add((MessageListener) any); + if (globalListener instanceof MessageListener) { + messageListeners.add((MessageListener) globalListener); } - if (any instanceof MessageEditListener) { - messageEditListeners.add((MessageEditListener) any); + if (globalListener instanceof MessageEditListener) { + messageEditListeners.add((MessageEditListener) globalListener); } - if (any instanceof ChatListener) { - chatListeners.add((ChatListener) any); + if (globalListener instanceof ChatListener) { + chatListeners.add((ChatListener) globalListener); } - if (any instanceof NotificationSettingsListener) { - settingsListeners.add((NotificationSettingsListener) any); + if (globalListener instanceof NotificationSettingsListener) { + settingsListeners.add((NotificationSettingsListener) globalListener); } - if (any instanceof StickersListener) { - stickersListeners.add((StickersListener) any); + if (globalListener instanceof StickersListener) { + stickersListeners.add((StickersListener) globalListener); } - if (any instanceof AnimationsListener) { - animationsListeners.add((AnimationsListener) any); + if (globalListener instanceof AnimationsListener) { + animationsListeners.add((AnimationsListener) globalListener); } - if (any instanceof ConnectionListener) { - connectionListeners.add((ConnectionListener) any); + if (globalListener instanceof ConnectionListener) { + connectionListeners.add((ConnectionListener) globalListener); } - if (any instanceof DayChangeListener) { - dayListeners.add((DayChangeListener) any); + if (globalListener instanceof TdlibOptionListener) { + optionListeners.add((TdlibOptionListener) globalListener); } - if (any instanceof TdlibOptionListener) { - optionListeners.add((TdlibOptionListener) any); + if (globalListener instanceof CounterChangeListener) { + totalCountersListeners.add((CounterChangeListener) globalListener); } - if (any instanceof CounterChangeListener) { - totalCountersListeners.add((CounterChangeListener) any); + if (globalListener instanceof ChatsNearbyListener) { + chatsNearbyListeners.add((ChatsNearbyListener) globalListener); } - if (any instanceof ChatsNearbyListener) { - chatsNearbyListeners.add((ChatsNearbyListener) any); + if (globalListener instanceof AnimatedEmojiListener) { + animatedEmojiListeners.add((AnimatedEmojiListener) globalListener); } - if (any instanceof AnimatedEmojiListener) { - animatedEmojiListeners.add((AnimatedEmojiListener) any); + if (globalListener instanceof PrivacySettingsListener) { + privacySettingsListeners.add((PrivacySettingsListener) globalListener); } - if (any instanceof PrivacySettingsListener) { - privacySettingsListeners.add((PrivacySettingsListener) any); + if (globalListener instanceof PrivateCallListener) { + privateCallListeners.add((PrivateCallListener) globalListener); } - if (any instanceof PrivateCallListener) { - privateCallListeners.add((PrivateCallListener) any); + if (globalListener instanceof GroupCallListener) { + groupCallListeners.add((GroupCallListener) globalListener); } - if (any instanceof GroupCallListener) { - groupCallListeners.add((GroupCallListener) any); - } - if (any instanceof SessionListener) { - sessionListeners.add((SessionListener) any); + if (globalListener instanceof SessionListener) { + sessionListeners.add((SessionListener) globalListener); } } } @AnyThread - public void unsubscribeFromAnyUpdates (Object any) { + public void unsubscribeFromGlobalUpdates (Object globalListener) { synchronized (this) { - if (any instanceof MessageListener) { - messageListeners.remove((MessageListener) any); - } - if (any instanceof MessageEditListener) { - messageEditListeners.remove((MessageEditListener) any); + if (globalListener instanceof MessageListener) { + messageListeners.remove((MessageListener) globalListener); } - if (any instanceof ChatListener) { - chatListeners.remove((ChatListener) any); + if (globalListener instanceof MessageEditListener) { + messageEditListeners.remove((MessageEditListener) globalListener); } - if (any instanceof NotificationSettingsListener) { - settingsListeners.remove((NotificationSettingsListener) any); + if (globalListener instanceof ChatListener) { + chatListeners.remove((ChatListener) globalListener); } - if (any instanceof StickersListener) { - stickersListeners.remove((StickersListener) any); + if (globalListener instanceof NotificationSettingsListener) { + settingsListeners.remove((NotificationSettingsListener) globalListener); } - if (any instanceof StickersListener) { - animationsListeners.remove((AnimationsListener) any); + if (globalListener instanceof StickersListener) { + stickersListeners.remove((StickersListener) globalListener); } - if (any instanceof ConnectionListener) { - connectionListeners.remove((ConnectionListener) any); + if (globalListener instanceof StickersListener) { + animationsListeners.remove((AnimationsListener) globalListener); } - if (any instanceof DayChangeListener) { - dayListeners.remove((DayChangeListener) any); + if (globalListener instanceof ConnectionListener) { + connectionListeners.remove((ConnectionListener) globalListener); } - if (any instanceof TdlibOptionListener) { - optionListeners.remove((TdlibOptionListener) any); + if (globalListener instanceof TdlibOptionListener) { + optionListeners.remove((TdlibOptionListener) globalListener); } - if (any instanceof CounterChangeListener) { - totalCountersListeners.remove((CounterChangeListener) any); + if (globalListener instanceof CounterChangeListener) { + totalCountersListeners.remove((CounterChangeListener) globalListener); } - if (any instanceof ChatsNearbyListener) { - chatsNearbyListeners.remove((ChatsNearbyListener) any); + if (globalListener instanceof ChatsNearbyListener) { + chatsNearbyListeners.remove((ChatsNearbyListener) globalListener); } - if (any instanceof AnimatedEmojiListener) { - animatedEmojiListeners.remove((AnimatedEmojiListener) any); + if (globalListener instanceof AnimatedEmojiListener) { + animatedEmojiListeners.remove((AnimatedEmojiListener) globalListener); } - if (any instanceof PrivacySettingsListener) { - privacySettingsListeners.remove((PrivacySettingsListener) any); + if (globalListener instanceof PrivacySettingsListener) { + privacySettingsListeners.remove((PrivacySettingsListener) globalListener); } - if (any instanceof PrivateCallListener) { - privateCallListeners.remove((PrivateCallListener) any); + if (globalListener instanceof PrivateCallListener) { + privateCallListeners.remove((PrivateCallListener) globalListener); } - if (any instanceof GroupCallListener) { - groupCallListeners.remove((GroupCallListener) any); + if (globalListener instanceof GroupCallListener) { + groupCallListeners.remove((GroupCallListener) globalListener); } - if (any instanceof SessionListener) { - sessionListeners.remove((SessionListener) any); + if (globalListener instanceof SessionListener) { + sessionListeners.remove((SessionListener) globalListener); } } } @@ -422,6 +420,16 @@ public void unsubscribeFromChatListUpdates (@NonNull TdApi.ChatList chatList, Ch chatListListeners.remove(TD.makeChatListKey(chatList), listener); } + @AnyThread + public void subscribeToChatFoldersUpdates (ChatFoldersListener listener) { + chatFoldersListeners.add(listener); + } + + @AnyThread + public void unsubscribeFromChatFoldersUpdates (ChatFoldersListener listener) { + chatFoldersListeners.remove(listener); + } + @AnyThread public void addReactionLoadListener (String reactionKey, ReactionLoadListener listener) { reactionLoadListeners.add(reactionKey, listener); @@ -539,6 +547,22 @@ void updateAuthorizationCodeReceived (String code) { } } + // Generic updates template + + private static void runUpdate (@Nullable Iterator list, RunnableData act) { + if (list != null) { + while (list.hasNext()) { + T next = list.next(); + act.runWithData(next); + } + } + } + + private void runChatUpdate (long chatId, RunnableData act) { + runUpdate(chatListeners.iterator(), act); + runUpdate(specificChatListeners.iterator(chatId), act); + } + // updateNewMessage private static void updateNewMessage (TdApi.UpdateNewMessage update, @Nullable Iterator list) { @@ -585,7 +609,7 @@ void updateMessageSendSucceeded (TdApi.UpdateMessageSendSucceeded update) { private static void updateMessageSendFailed (TdApi.UpdateMessageSendFailed update, @Nullable Iterator list) { if (list != null) { while (list.hasNext()) { - list.next().onMessageSendFailed(update.message, update.oldMessageId, update.errorCode, update.errorMessage); + list.next().onMessageSendFailed(update.message, update.oldMessageId, update.error); } } } @@ -835,7 +859,7 @@ private static void updateMessageUnreadReactions (TdApi.UpdateMessageUnreadReact } } - void updateMessageUnreadReactions (TdApi.UpdateMessageUnreadReactions update, boolean counterChanged, boolean availabilityChanged) { + void updateMessageUnreadReactions (TdApi.UpdateMessageUnreadReactions update, boolean counterChanged, boolean availabilityChanged, TdApi.Chat chat, TdlibChatList[] chatLists) { List messages = pendingMessages.get(update.chatId + "_" + update.messageId); if (messages != null) { for (TdApi.Message message : messages) { @@ -848,6 +872,17 @@ void updateMessageUnreadReactions (TdApi.UpdateMessageUnreadReactions update, bo updateChatUnreadReactionCount(update.chatId, update.unreadReactionCount, availabilityChanged, chatListeners.iterator()); updateChatUnreadReactionCount(update.chatId, update.unreadReactionCount, availabilityChanged, specificChatListeners.iterator(update.chatId)); } + if (counterChanged) { + updateChatUnreadReactionCount(update.chatId, update.unreadReactionCount, availabilityChanged, chatListeners.iterator()); + updateChatUnreadReactionCount(update.chatId, update.unreadReactionCount, availabilityChanged, specificChatListeners.iterator(update.chatId)); + if (chatLists != null) { + for (TdlibChatList chatList : chatLists) { + iterateChatListListeners(chatList, listener -> + listener.onChatListItemChanged(chatList, chat, availabilityChanged ? ChatListListener.ItemChangeType.UNREAD_AVAILABILITY_CHANGED : ChatListListener.ItemChangeType.READ_INBOX) + ); + } + } + } } // updateDeleteMessages @@ -1156,10 +1191,12 @@ void updateChatDraftMessage (TdApi.UpdateChatDraftMessage update, List list) { + private static void updateChatActiveStories (TdApi.ChatActiveStories activeStories, @Nullable Iterator list) { if (list != null) { while (list.hasNext()) { - list.next().onChatVideoChatChanged(chatId, voiceChat); + list.next().onChatActiveStoriesChanged(activeStories); } } } - void updateChatVideoChat (TdApi.UpdateChatVideoChat update) { - updateChatVideoChat(update.chatId, update.videoChat, chatListeners.iterator()); - updateChatVideoChat(update.chatId, update.videoChat, specificChatListeners.iterator(update.chatId)); + void updateChatActiveStories (TdApi.UpdateChatActiveStories update) { + updateChatActiveStories(update.activeStories, chatListeners.iterator()); + updateChatActiveStories(update.activeStories, specificChatListeners.iterator(update.activeStories.chatId)); } - // updateForumTopicInfo + // updateStory - void updateForumTopicInfo (TdApi.UpdateForumTopicInfo update) { - updateForumTopicInfo(update.chatId, update.info, chatListeners.iterator()); - updateForumTopicInfo(update.chatId, update.info, specificChatListeners.iterator(update.chatId)); - updateForumTopicInfo(update.chatId, update.info, specificForumTopicListeners.iterator(update.chatId + "_" + update.info.messageThreadId)); + private static String uniqueStoryKey (TdApi.Story story) { + return uniqueStoryKey(story.senderChatId, story.id); + } + + private static String uniqueStoryKey (long storySenderChatId, int storyId) { + return storySenderChatId + "_" + storyId; } - private static void updateForumTopicInfo (long chatId, TdApi.ForumTopicInfo info, @Nullable Iterator list) { + private static void updateStory (TdApi.Story story, @Nullable Iterator list) { if (list != null) { while (list.hasNext()) { - list.next().onForumTopicInfoChanged(chatId, info); + list.next().onStoryUpdated(story); } } } - // updateChatPendingJoinRequests + void updateStory (TdApi.UpdateStory update) { + updateStory(update.story, storyListeners.iterator()); + updateStory(update.story, specificStoryListeners.iterator(uniqueStoryKey(update.story))); + } - private static void updateChatPendingJoinRequests (long chatId, TdApi.ChatJoinRequestsInfo pendingJoinRequests, @Nullable Iterator list) { + // updateStoryDeleted + + private static void updateStoryDeleted (long storySenderChatId, int storyId, @Nullable Iterator list) { if (list != null) { while (list.hasNext()) { - list.next().onChatPendingJoinRequestsChanged(chatId, pendingJoinRequests); + list.next().onStoryDeleted(storySenderChatId, storyId); } } } - void updateChatPendingJoinRequests (TdApi.UpdateChatPendingJoinRequests update) { - updateChatPendingJoinRequests(update.chatId, update.pendingJoinRequests, chatListeners.iterator()); - updateChatPendingJoinRequests(update.chatId, update.pendingJoinRequests, specificChatListeners.iterator(update.chatId)); + void updateStoryDeleted (TdApi.UpdateStoryDeleted update) { + updateStoryDeleted(update.storySenderChatId, update.storyId, storyListeners.iterator()); + updateStoryDeleted(update.storySenderChatId, update.storyId, specificStoryListeners.iterator(uniqueStoryKey(update.storySenderChatId, update.storyId))); } - // updateUsersNearby + // updateStorySendSucceeded - private static void updateUsersNearby (TdApi.ChatNearby[] usersNearby, @Nullable Iterator list) { + private static void updateStorySendSucceeded (TdApi.Story story, int oldStoryId, @Nullable Iterator list) { if (list != null) { while (list.hasNext()) { - list.next().onUsersNearbyUpdated(usersNearby); + list.next().onStorySendSucceeded(story, oldStoryId); } } } - void updateUsersNearby (TdApi.UpdateUsersNearby update) { - updateUsersNearby(update.usersNearby, chatsNearbyListeners.iterator()); + void updateStorySendSucceeded (TdApi.UpdateStorySendSucceeded update) { + updateStorySendSucceeded(update.story, update.oldStoryId, storyListeners.iterator()); + updateStorySendSucceeded(update.story, update.oldStoryId, specificStoryListeners.iterator(uniqueStoryKey(update.story.senderChatId, update.oldStoryId))); } - // updateChatIsMarkedAsUnread + // updateStorySendFailed - private static void updateChatIsMarkedAsUnread (long chatId, boolean isMarkedAsUnread, @Nullable Iterator list) { + private static void updateStorySendFailed (TdApi.Story story, TdApi.Error error, @Nullable TdApi.CanSendStoryResult errorType, @Nullable Iterator list) { if (list != null) { while (list.hasNext()) { - list.next().onChatMarkedAsUnread(chatId, isMarkedAsUnread); + list.next().onStorySendFailed(story, error, errorType); } } } - void updateChatIsMarkedAsUnread (TdApi.UpdateChatIsMarkedAsUnread update) { - updateChatIsMarkedAsUnread(update.chatId, update.isMarkedAsUnread, chatListeners.iterator()); - updateChatIsMarkedAsUnread(update.chatId, update.isMarkedAsUnread, specificChatListeners.iterator(update.chatId)); + void updateStorySendFailed (TdApi.UpdateStorySendFailed update) { + updateStorySendFailed(update.story, update.error, update.errorType, storyListeners.iterator()); + updateStorySendFailed(update.story, update.error, update.errorType, specificStoryListeners.iterator(uniqueStoryKey(update.story))); } - // updateChatBackground + // updateStoryStealthMode - private static void updateChatBackground (long chatId, @Nullable TdApi.ChatBackground background, @Nullable Iterator list) { + private static void updateStoryStealthMode (int activeUntilDate, int cooldownUntilDate, @Nullable Iterator list) { if (list != null) { while (list.hasNext()) { - list.next().onChatBackgroundChanged(chatId, background); + list.next().onStoryStealthModeUpdated(activeUntilDate, cooldownUntilDate); } } } - void updateChatBackground (TdApi.UpdateChatBackground update) { - updateChatBackground(update.chatId, update.background, chatListeners.iterator()); - updateChatBackground(update.chatId, update.background, specificChatListeners.iterator(update.chatId)); + void updateStoryStealthMode (TdApi.UpdateStoryStealthMode update) { + updateStoryStealthMode(update.activeUntilDate, update.cooldownUntilDate, storyListeners.iterator()); + updateStoryStealthMode(update.activeUntilDate, update.cooldownUntilDate, specificStoryListeners.combinedIterator()); } - // updateChatIsTranslatable + // updateChatVoiceChat + + void updateChatVideoChat (TdApi.UpdateChatVideoChat update) { + runChatUpdate(update.chatId, listener -> { + listener.onChatVideoChatChanged(update.chatId, update.videoChat); + }); + } + + // updateForumTopicInfo + + void updateForumTopicInfo (TdApi.UpdateForumTopicInfo update) { + runChatUpdate(update.chatId, listener -> { + listener.onForumTopicInfoChanged(update.chatId, update.info); + }); + runUpdate(specificForumTopicListeners.iterator(update.chatId + "_" + update.info.messageThreadId), listener -> { + listener.onForumTopicInfoChanged(update.chatId, update.info); + }); + } + + // updateChatViewAsTopics + + void updateChatViewAsTopics (TdApi.UpdateChatViewAsTopics update) { + runChatUpdate(update.chatId, listener -> { + listener.onChatViewAsTopics(update.chatId, update.viewAsTopics); + }); + } + + // updateChatPendingJoinRequests + + void updateChatPendingJoinRequests (TdApi.UpdateChatPendingJoinRequests update) { + runChatUpdate(update.chatId, listener -> { + listener.onChatPendingJoinRequestsChanged(update.chatId, update.pendingJoinRequests); + }); + } - private static void updateChatIsTranslatable (long chatId, boolean isTranslatable, @Nullable Iterator list) { + // updateUsersNearby + + private static void updateUsersNearby (TdApi.ChatNearby[] usersNearby, @Nullable Iterator list) { if (list != null) { while (list.hasNext()) { - list.next().onChatIsTranslatableChanged(chatId, isTranslatable); + list.next().onUsersNearbyUpdated(usersNearby); } } } + void updateUsersNearby (TdApi.UpdateUsersNearby update) { + updateUsersNearby(update.usersNearby, chatsNearbyListeners.iterator()); + } + + // updateChatIsMarkedAsUnread + + void updateChatIsMarkedAsUnread (TdApi.UpdateChatIsMarkedAsUnread update) { + runChatUpdate(update.chatId, listener -> { + listener.onChatMarkedAsUnread(update.chatId, update.isMarkedAsUnread); + }); + } + + // updateChatBackground + + void updateChatBackground (TdApi.UpdateChatBackground update) { + runChatUpdate(update.chatId, listener -> + listener.onChatBackgroundChanged(update.chatId, update.background) + ); + } + + // updateChatAccentColors + + void updateChatAccentColors (TdApi.UpdateChatAccentColors update) { + runChatUpdate(update.chatId, listener -> + listener.onChatAccentColorsChanged(update.chatId, + update.accentColorId, + update.backgroundCustomEmojiId, + update.profileAccentColorId, + update.profileBackgroundCustomEmojiId + ) + ); + } + + // updateChatEmojiStatus + + void updateChatEmojiStatus (TdApi.UpdateChatEmojiStatus update) { + runChatUpdate(update.chatId, listener -> + listener.onChatEmojiStatusChanged(update.chatId, + update.emojiStatus + ) + ); + } + + // updateChatIsTranslatable + void updateChatIsTranslatable (TdApi.UpdateChatIsTranslatable update) { - updateChatIsTranslatable(update.chatId, update.isTranslatable, chatListeners.iterator()); - updateChatIsTranslatable(update.chatId, update.isTranslatable, specificChatListeners.iterator(update.chatId)); + runChatUpdate(update.chatId, listener -> { + listener.onChatIsTranslatableChanged(update.chatId, update.isTranslatable); + }); } // updateChatIsBlocked - private static void updateChatIsBlocked (long chatId, boolean isBlocked, @Nullable Iterator list) { + private static void updateChatBlockList (long chatId, @Nullable TdApi.BlockList blockList, @Nullable Iterator list) { if (list != null) { while (list.hasNext()) { - list.next().onChatBlocked(chatId, isBlocked); + list.next().onChatBlockListChanged(chatId, blockList); } } } - void updateChatIsBlocked (TdApi.UpdateChatIsBlocked update) { - updateChatIsBlocked(update.chatId, update.isBlocked, chatListeners.iterator()); - updateChatIsBlocked(update.chatId, update.isBlocked, specificChatListeners.iterator(update.chatId)); + void updateChatBlockList (TdApi.UpdateChatBlockList update) { + runChatUpdate(update.chatId, listener -> { + listener.onChatBlockListChanged(update.chatId, update.blockList); + }); } // updateChatClientDataChanged - private static void updateChatClientDataChanged (long chatId, String clientData, @Nullable Iterator list) { - if (list != null) { - while (list.hasNext()) { - list.next().onChatClientDataChanged(chatId, clientData); - } - } - } - void updateChatClientDataChanged (long chatId, String newClientData) { - updateChatClientDataChanged(chatId, newClientData, chatListeners.iterator()); - updateChatClientDataChanged(chatId, newClientData, specificChatListeners.iterator(chatId)); + runChatUpdate(chatId, listener -> { + listener.onChatClientDataChanged(chatId, newClientData); + }); } // updateNotificationSettings @@ -1466,16 +1586,23 @@ public void updateNotificationGlobalSettings () { } @AnyThread - public void notifyChatCountersChanged (TdApi.ChatList chatList, boolean availabilityChanged, int totalCount, int unreadCount, int unreadUnmutedCount) { + public void notifyArchiveChatListSettingsChanged (TdApi.ArchiveChatListSettings archiveChatListSettings) { + for (NotificationSettingsListener listener : settingsListeners) { + listener.onArchiveChatListSettingsChanged(archiveChatListSettings); + } + } + + @AnyThread + public void notifyChatCountersChanged (TdApi.ChatList chatList, TdlibCounter counter, boolean availabilityChanged, int totalCount, int unreadCount, int unreadUnmutedCount) { for (CounterChangeListener listener : totalCountersListeners) { - listener.onChatCounterChanged(chatList, availabilityChanged, totalCount, unreadCount, unreadUnmutedCount); + listener.onChatCounterChanged(chatList, counter, availabilityChanged, totalCount, unreadCount, unreadUnmutedCount); } } @AnyThread - public void notifyMessageCountersChanged (TdApi.ChatList chatList, int unreadCount, int unreadUnmutedCount) { + public void notifyMessageCountersChanged (TdApi.ChatList chatList, TdlibCounter counter, int unreadCount, int unreadUnmutedCount) { for (CounterChangeListener listener : totalCountersListeners) { - listener.onMessageCounterChanged(chatList, unreadCount, unreadUnmutedCount); + listener.onMessageCounterChanged(chatList, counter, unreadCount, unreadUnmutedCount); } } @@ -1705,6 +1832,18 @@ void updateContactRegisteredNotificationsDisabled (boolean areDisabled) { } } + void updateAccentColors (TdApi.UpdateAccentColors update) { + for (TdlibOptionListener listener : optionListeners) { + listener.onAccentColorsChanged(); + } + } + + void updateProfileAccentColors (TdApi.UpdateProfileAccentColors update, boolean listChanged) { + for (TdlibOptionListener listener : optionListeners) { + listener.onProfileAccentColorsChanged(listChanged); + } + } + void updateSuggestedActions (TdApi.UpdateSuggestedActions update) { for (TdlibOptionListener listener : optionListeners) { listener.onSuggestedActionsChanged(update.addedActions, update.removedActions); diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibListenersGlobal.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibListenersGlobal.java index cbcec34c59..fe2b29364f 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibListenersGlobal.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibListenersGlobal.java @@ -96,7 +96,7 @@ void notifyUpdateMessageSendSucceeded (Tdlib tdlib, TdApi.UpdateMessageSendSucce void notifyUpdateMessageSendFailed (Tdlib tdlib, TdApi.UpdateMessageSendFailed update) { for (GlobalMessageListener listener : messageListeners) { - listener.onMessageSendFailed(tdlib, update.message, update.oldMessageId, update.errorCode, update.errorMessage); + listener.onMessageSendFailed(tdlib, update.message, update.oldMessageId, update.error); } } @@ -120,6 +120,12 @@ void notifyTotalCounterChanged (@NonNull TdApi.ChatList chatList, boolean isRese } } + void notifyBadgeSettingsChanged () { + for (GlobalCountersListener listener : countersListeners) { + listener.onBadgeSettingsChanged(); + } + } + // Self user /*public void addSelfUserListener (GlobalSelfUserListener listener) { diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibManager.java index 7d48dee810..19efd3c124 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibManager.java @@ -197,7 +197,7 @@ public static void performExternalReply (Context context, CharSequence text, Tdl if (StringUtils.isEmpty(text)) return; performSyncTask(context, extras.accountId, "reply", (tdlib, onDone) -> { - tdlib.sendMessage(extras.chatId, extras.messageThreadId, extras.needReply ? extras.messageIds[extras.messageIds.length - 1] : 0, Td.newSendOptions(), new TdApi.InputMessageText(new TdApi.FormattedText(text.toString(), null), false, false), sendingMessage -> { + tdlib.sendMessage(extras.chatId, extras.messageThreadId, extras.needReply ? new TdApi.InputMessageReplyToMessage(extras.forceExternalReply ? extras.chatId : 0, extras.messageIds[extras.messageIds.length - 1], null) : null, Td.newSendOptions(), new TdApi.InputMessageText(new TdApi.FormattedText(text.toString(), null), null, false), sendingMessage -> { if (sendingMessage == null) { UI.showToast(R.string.NotificationReplyFailed, Toast.LENGTH_SHORT); if (onDone != null) { @@ -315,6 +315,8 @@ public void onProxyAdded (Settings.Proxy proxy, boolean isCurrent) { } private @Nullable String tdlibCommitHash, tdlibVersion; + private final DateManager dateManager = new DateManager(this); + private TdlibManager (int firstInstanceId, boolean forceService) { Client.setLogMessageHandler(0, (verbosityLevel, errorMessage) -> { if (verbosityLevel == 0) { @@ -346,6 +348,10 @@ private TdlibManager (int firstInstanceId, boolean forceService) { saveCrashes(); } + public DateManager dateManager () { + return dateManager; + } + void setTdlibCommitHash (@NonNull String commitHash) { // called by children Tdlib instances this.tdlibCommitHash = commitHash; @@ -390,7 +396,7 @@ private int findAccountIdByClient (Client client) { @Override @UiThread public void onUiStateChanged (int newState) { - boolean hasUi = newState != UI.STATE_DESTROYED && newState != UI.STATE_UNKNOWN; + boolean hasUi = newState != UI.State.DESTROYED && newState != UI.State.UNKNOWN; if (this.hasUi != hasUi) { this.hasUi = hasUi; for (TdlibAccount account : accounts) { @@ -422,8 +428,10 @@ public String languageDatabasePath () { } public static void setTestLabConfig () { - Client.execute(new TdApi.SetLogVerbosityLevel(5)); - Client.execute(new TdApi.SetLogStream(new TdApi.LogStreamDefault())); + try { + Client.execute(new TdApi.SetLogVerbosityLevel(5)); + Client.execute(new TdApi.SetLogStream(new TdApi.LogStreamDefault())); + } catch (Client.ExecutionException ignored) { } Log.setLogLevel(Log.LEVEL_VERBOSE); } @@ -579,11 +587,14 @@ public TdlibBadgeCounter getTotalUnreadBadgeCounter () { return getTotalUnreadBadgeCounter(TdlibAccount.NO_ID); } - public void resetBadge () { + public void resetBadge (boolean settingsChanged) { synchronized (counterLock) { updateBadgeInternal(true, false); dispatchUnreadCount(true); } + if (settingsChanged) { + global.notifyBadgeSettingsChanged(); + } } private boolean logged; @@ -678,7 +689,14 @@ private void handleUiMessage (Message msg) { @Override public Iterator iterator () { List accounts = new ArrayList<>(this.accounts); - Collections.sort(accounts, (a, b) -> (a == currentAccount) != (b == currentAccount) ? Boolean.compare(b == currentAccount, a == currentAccount) : a.compareTo(b)); + Collections.sort(accounts, (a, b) -> { + boolean aCurrent = a == currentAccount; + boolean bCurrent = b == currentAccount; + if (aCurrent != bCurrent) { + return Boolean.compare(bCurrent, aCurrent); + } + return a.compareTo(b); + }); return new FilteredIterator<>(accounts.iterator(), account -> !account.isUnauthorized() && account.tdlibInstanceMode() != Tdlib.Mode.SERVICE); } @@ -1035,10 +1053,6 @@ public static AccountConfig readAccountConfig (@Nullable TdlibManager context, R return new AccountConfig(currentAccount, accounts, preferredAccountId); } - private int binlogSize () { - return binlogSize(accounts.size()); - } - public static int binlogSize (int accountsNum) { return BINLOG_PREFIX_SIZE + accountsNum * TdlibAccount.SIZE_PER_ENTRY; } @@ -1047,7 +1061,7 @@ private int writeAccountConfig (RandomAccessFile r, int mode, int accountId) thr int saveCount = 0; final int accountNum = accounts.size(); - final int binlogSize = binlogSize(); + final int binlogSize = binlogSize(accountNum); final long currentLen = r.length(); final boolean canOptimize; @@ -1328,7 +1342,7 @@ public TdlibAccount account (int accountId) { return accounts.get(accountId); } - public int accountIdForUserId (int userId, int startIndex) { + public int accountIdForUserId (long userId, int startIndex) { for (int i = startIndex; i < accounts.size(); i++) { if (accounts.get(i).getKnownUserId() == userId) { return i; @@ -1815,16 +1829,11 @@ public void saveCrashes () { pendingRequests.incrementAndGet(); } TDLib.Tag.td_init("Reporting crash %d: %s", crash.id, saveFunction); - tdlib.send(saveFunction, result -> { - switch (result.getConstructor()) { - case TdApi.Ok.CONSTRUCTOR: { - Settings.instance().markCrashAsSaved(crash); - break; - } - case TdApi.Error.CONSTRUCTOR: { - TDLib.Tag.td_init("Can't report crash %d: %s", crash.id, TD.toErrorString(result)); - break; - } + tdlib.send(saveFunction, (ok, error) -> { + if (error != null) { + TDLib.Tag.td_init("Can't report crash %d: %s", crash.id, TD.toErrorString(error)); + } else { + Settings.instance().markCrashAsSaved(crash); } synchronized (pendingRequests) { pendingRequests.decrementAndGet(); @@ -1865,7 +1874,7 @@ private boolean checkAliveAccount (TdlibAccount account) { activeAccounts.remove(position); } global().notifyAccountAddedOrRemoved(account, position, needAdd); - resetBadge(); + resetBadge(false); increaseModCount(account); return true; } @@ -2243,8 +2252,10 @@ public static long deleteAllLogFiles () { private static long deleteLogFiles (int mode) { if (UI.TEST_MODE != UI.TEST_MODE_AUTO) { - Client.execute(new TdApi.SetLogVerbosityLevel(0)); - Client.execute(new TdApi.SetLogStream(new TdApi.LogStreamEmpty())); + try { + Client.execute(new TdApi.SetLogVerbosityLevel(0)); + Client.execute(new TdApi.SetLogStream(new TdApi.LogStreamEmpty())); + } catch (Client.ExecutionException ignored) { } } long removedSize; @@ -2453,21 +2464,25 @@ public void runOnUiThread (Runnable runnable) { public static TdApi.LanguagePackStringValue getString (String languageDatabasePath, String key, @NonNull String languagePackId) { if (StringUtils.isEmpty(key)) return null; - final TdApi.Object result = Client.execute(new TdApi.GetLanguagePackString(languageDatabasePath, BuildConfig.LANGUAGE_PACK, languagePackId, key)); - if (result == null) + final TdApi.LanguagePackStringValue value; + try { + value = Client.execute(new TdApi.GetLanguagePackString(languageDatabasePath, BuildConfig.LANGUAGE_PACK, languagePackId, key)); + } catch (Client.ExecutionException error) { + if (error.error.code != 404) { + Log.e("getString %s error:%s, languagePackId:%s", key, TD.toErrorString(error.error), languagePackId); + } return null; - switch (result.getConstructor()) { + } + switch (value.getConstructor()) { case TdApi.LanguagePackStringValueOrdinary.CONSTRUCTOR: case TdApi.LanguagePackStringValuePluralized.CONSTRUCTOR: - return (TdApi.LanguagePackStringValue) result; + return value; case TdApi.LanguagePackStringValueDeleted.CONSTRUCTOR: return null; - case TdApi.Error.CONSTRUCTOR: - if (((TdApi.Error) result).code != 404) - Log.e("getString %s error:%s, languagePackId:%s", key, TD.toErrorString(result), languagePackId); - return null; + default: + Td.assertLanguagePackStringValue_11536986(); + throw Td.unsupported(value); } - return null; } private TdApi.LanguagePackStringValue getString (String key, @NonNull String languagePackId) { diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibMessageViewer.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibMessageViewer.java new file mode 100644 index 0000000000..8a16801ff8 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibMessageViewer.java @@ -0,0 +1,975 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 10/10/2023 + */ +package org.thunderdog.challegram.telegram; + +import android.widget.Toast; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.collection.LongSparseArray; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.BaseActivity; +import org.thunderdog.challegram.BuildConfig; +import org.thunderdog.challegram.Log; +import org.thunderdog.challegram.TDLib; +import org.thunderdog.challegram.config.Config; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.tool.UI; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import me.vkryl.core.BitwiseUtils; +import me.vkryl.core.collection.LongSet; +import me.vkryl.core.lambda.Destroyable; +import me.vkryl.core.lambda.FutureBool; +import me.vkryl.core.lambda.RunnableBool; +import me.vkryl.core.reference.ReferenceList; +import me.vkryl.td.ChatId; +import me.vkryl.td.Td; + +public class TdlibMessageViewer { + private static final long TRACK_MESSAGE_TIMEOUT_MS = 1000; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + Flags.NO_SENSITIVE_SCREENSHOT_NOTIFICATION, + Flags.NO_SCREENSHOT_NOTIFICATION, + Flags.REFRESH_INTERACTION_INFO + }, flag = true) + public @interface Flags { + int + NO_SENSITIVE_SCREENSHOT_NOTIFICATION = 1, // No sensitive content is visible & no need to send MessageSourceScreenshot + NO_SCREENSHOT_NOTIFICATION = 1 << 1, // Completely ignore screenshots of the given message + REFRESH_INTERACTION_INFO = 1 << 2; // Periodically refresh interaction info based on message date + } + + public static class VisibilityState { + public long openTimeMs, hideTimeMs; + + public VisibilityState (boolean isVisible) { + // TODO consider switching to tdlib.currentTimeMillis()? + if (isVisible) { + this.openTimeMs = System.currentTimeMillis(); + } else { + this.hideTimeMs = System.currentTimeMillis(); + } + } + + public boolean isVisible () { + return hideTimeMs == 0; + } + + public boolean isHidden () { + return hideTimeMs != 0; + } + + public boolean markAsHidden () { + if (isVisible()) { + this.hideTimeMs = System.currentTimeMillis(); + return true; + } + return false; + } + + public boolean markAsVisible () { + if (isHidden()) { + this.openTimeMs = System.currentTimeMillis(); + this.hideTimeMs = 0; + return true; + } + return false; + } + } + + public static class VisibleMessage { + public final long chatId; + public final TdApi.Message message; + public final TdApi.SponsoredMessage sponsoredMessage; + public final VisibilityState visibility = new VisibilityState(true); + public @Flags long flags; + public long viewId; + public boolean isRecentlyViewed; + + public VisibleMessage (@NonNull TdApi.Message message, @Flags long flags, long viewId) { + this.chatId = message.chatId; + this.message = message; + this.sponsoredMessage = null; + this.flags = flags; + this.viewId = viewId; + } + + public VisibleMessage (long chatId, @NonNull TdApi.SponsoredMessage sponsoredMessage, long flags, long viewId) { + this.chatId = chatId; + this.message = null; + this.sponsoredMessage = sponsoredMessage; + this.flags = flags; + this.viewId = viewId; + } + + public long getChatId () { + return chatId; + } + + public long getMessageId () { + return sponsoredMessage != null ? sponsoredMessage.messageId : message != null ? message.id : 0; + } + + public int getMessageDate () { + if (message == null) + throw new IllegalStateException(); + return message.date; + } + + public boolean isSponsored () { + return sponsoredMessage != null; + } + + public boolean needRefreshInteractionInfo () { + if (message != null && message.sendingState == null) { + return BitwiseUtils.hasFlag(flags, Flags.REFRESH_INTERACTION_INFO); + } + return false; + } + + public boolean needRestrictScreenshots () { + return message != null && !message.canBeSaved; + } + + public boolean needScreenshotNotification () { + if (message == null || needRestrictScreenshots()) { + return false; + } + if (BitwiseUtils.hasFlag(flags, Flags.NO_SCREENSHOT_NOTIFICATION)) { + return false; + } + if (BitwiseUtils.hasFlag(flags, Flags.NO_SENSITIVE_SCREENSHOT_NOTIFICATION) && TD.isScreenshotSensitive(message)) { + return false; + } + return true; + } + } + + private static class ChatState { + LongSet visibleMessageIds = new LongSet(); + LongSet visibleProtectedMessageIds = new LongSet(); + LongSet refreshMessageIds = new LongSet(); + boolean haveRecentlyViewedMessages; + boolean awaitingUiResumeForRefresh, awaitingIgnoreLocksForRefresh; + int maxRefreshDate; + } + + public static class VisibleChat implements Destroyable { + private final Viewport viewport; + public final long chatId; + public final LongSparseArray visibleMessages = new LongSparseArray<>(); + private final ChatState state = new ChatState(); + private final VisibilityState visibility = new VisibilityState(false); + + public VisibleChat (Viewport viewport, long chatId) { + this.viewport = viewport; + this.chatId = chatId; + } + + private void assertChat (long chatId) { + if (this.chatId != chatId) { + throw new IllegalArgumentException(); + } + } + + public long[] getMessageIds (boolean onlyRecentlyViewed, boolean updateRecentState) { + if (onlyRecentlyViewed) { + LongSet messageIdsSet = new LongSet(); + for (int index = 0; index < visibleMessages.size(); index++) { + VisibleMessage visibleMessage = visibleMessages.valueAt(index); + if (visibleMessage.isRecentlyViewed) { + messageIdsSet.add(visibleMessage.getMessageId()); + if (updateRecentState) { + visibleMessage.isRecentlyViewed = false; + } + } + } + if (updateRecentState) { + state.haveRecentlyViewedMessages = false; + } + return messageIdsSet.toArray(); + } else { + return state.visibleMessageIds.toArray(); + } + } + + public @Nullable VisibleMessage find (long messageId) { + return visibleMessages.get(messageId); + } + + /** + * @return True, if flags were changed or message wasn't previously visible + */ + public boolean addVisibleMessage (TdApi.Object rawMessage, @Flags long flags, long viewId, boolean forceMarkAsRecent) { + VisibleMessage visibleMessage; + boolean isSponsored = rawMessage.getConstructor() == TdApi.SponsoredMessage.CONSTRUCTOR; + if (isSponsored) { + TdApi.SponsoredMessage sponsoredMessage = (TdApi.SponsoredMessage) rawMessage; + visibleMessage = find(sponsoredMessage.messageId); + } else { + TdApi.Message message = (TdApi.Message) rawMessage; + assertChat(message.chatId); + visibleMessage = find(message.id); + } + if (visibleMessage != null) { + visibleMessage.viewId = viewId; + if (visibleMessage.flags != flags || visibleMessage.visibility.isHidden() || forceMarkAsRecent) { + long oldFlags = visibleMessage.flags; + visibleMessage.flags = flags; + visibleMessage.isRecentlyViewed = true; + if (visibleMessage.visibility.markAsVisible()) { + trackMessage(visibleMessage, true); + } else if (oldFlags != flags) { + handleFlagsChange(visibleMessage, oldFlags); + } + return true; + } + return false; + } + if (isSponsored) { + visibleMessage = new VisibleMessage(chatId, (TdApi.SponsoredMessage) rawMessage, flags, viewId); + } else { + visibleMessage = new VisibleMessage((TdApi.Message) rawMessage, flags, viewId); + } + visibleMessage.isRecentlyViewed = true; + trackMessage(visibleMessage, true); + return true; + } + + @Nullable + public VisibleMessage removeVisibleMessage (long chatId, long messageId) { + assertChat(chatId); + VisibleMessage visibleMessage = find(messageId); + if (visibleMessage != null && visibleMessage.visibility.markAsHidden()) { + trackMessage(visibleMessage, false, messageId); + return visibleMessage; + } + return null; + } + + public boolean removeOtherVisibleMessages (LongSet messageIds) { + int hiddenMessageCount = 0; + for (int index = visibleMessages.size() - 1; index >= 0; index--) { + VisibleMessage visibleMessage = visibleMessages.valueAt(index); + if ((messageIds == null || !messageIds.has(visibleMessage.getMessageId())) && visibleMessage.visibility.markAsHidden()) { + trackMessage(visibleMessage, false); + hiddenMessageCount++; + } + } + return hiddenMessageCount > 0; + } + + public boolean removeOtherVisibleMessagesByViewId (long viewId) { + int hiddenMessageCount = 0; + for (int index = visibleMessages.size() - 1; index >= 0; index--) { + VisibleMessage visibleMessage = visibleMessages.valueAt(index); + if (visibleMessage.viewId != viewId && visibleMessage.visibility.markAsHidden()) { + trackMessage(visibleMessage, false); + hiddenMessageCount++; + } + } + return hiddenMessageCount > 0; + } + + public boolean clear () { + return removeOtherVisibleMessages(null); + } + + public boolean isEmpty () { + return state.visibleMessageIds.isEmpty(); + } + + private void trackMessage (VisibleMessage visibleMessage, boolean isVisible) { + final Long messageId = visibleMessage.getMessageId(); + trackMessage(visibleMessage, isVisible, messageId); + } + + private void trackMessage (VisibleMessage visibleMessage, boolean isVisible, Long messageId) { + if (isVisible) { + if (!state.visibleMessageIds.add(messageId)) + throw new IllegalStateException(); + if (visibleMessage.needRestrictScreenshots()) { + if (!state.visibleProtectedMessageIds.add(messageId)) + throw new IllegalStateException(); + } + visibleMessages.put(messageId, visibleMessage); + if (visibleMessage.needRefreshInteractionInfo() && state.refreshMessageIds.add(messageId)) { + checkRefreshInteractionInfo(); + } + } else { + if (!state.visibleMessageIds.remove(messageId)) + throw new IllegalStateException(); + state.visibleProtectedMessageIds.remove(messageId); + visibleMessages.remove(messageId); + viewport.trackRecentlyViewedMessage(this, visibleMessage); + if (state.refreshMessageIds.remove(messageId)) { + checkRefreshInteractionInfo(); + } + } + } + + private void handleFlagsChange (VisibleMessage visibleMessage, long oldFlags) { + if (BitwiseUtils.flagChanged(visibleMessage.flags, oldFlags, Flags.REFRESH_INTERACTION_INFO)) { + boolean needRefreshInteractionInfo = visibleMessage.needRefreshInteractionInfo(); + boolean needCheck; + if (needRefreshInteractionInfo) { + needCheck = state.refreshMessageIds.add(visibleMessage.getMessageId()); + } else { + needCheck = state.refreshMessageIds.remove(visibleMessage.getMessageId()); + } + if (needCheck) { + checkRefreshInteractionInfo(); + } + } + } + + private void checkRefreshInteractionInfo () { + boolean needRefreshInteractionInfo = !viewport.isDestroyed() && state.refreshMessageIds.size() > 0; + long maxMessageId = needRefreshInteractionInfo ? state.refreshMessageIds.max() : 0; + int maxDate; + if (needRefreshInteractionInfo) { + VisibleMessage visibleMessage = find(maxMessageId); + if (visibleMessage == null) + throw new IllegalStateException(); + maxDate = visibleMessage.getMessageDate(); + } else { + maxDate = 0; + } + boolean forceRefresh = cancelPostponedRefresh(); + if (state.maxRefreshDate != maxDate || (maxDate != 0 && forceRefresh)) { + cancelRefresh(); + state.maxRefreshDate = maxDate; + if (maxDate != 0) { + long refreshTimeout = Math.max(1500L, + timeTillNextRefresh(viewport.context.tdlib.currentTimeMillis() - TimeUnit.SECONDS.toMillis(maxDate)) + ); + if (forceRefresh) { + refreshTimeout = Math.min(refreshTimeout, 3000L); + } + scheduleRefresh(refreshTimeout); + } + } + } + + private final Runnable refreshAct = () -> refreshInteractionInfo(true); + + private void cancelRefresh () { + if (state.maxRefreshDate != 0) { + viewport.context.tdlib.ui().removeCallbacks(refreshAct); + state.maxRefreshDate = 0; + } + } + + private void scheduleRefresh (long timeoutMs) { + if (timeoutMs > 0) { + viewport.context.tdlib.ui().postDelayed(refreshAct, timeoutMs); + } else { + refreshInteractionInfo(true); + } + } + + protected void notifyViewportLockValueChanged () { + if (state.awaitingIgnoreLocksForRefresh && !viewport.needIgnore()) { + refreshInteractionInfo(false); + } + } + + private final BaseActivity.SimpleStateListener activityListener = (activity, newState, prevState) -> { + if (newState == UI.State.RESUMED && state.awaitingUiResumeForRefresh) { + refreshInteractionInfo(false); + } + }; + + private boolean cancelPostponedRefresh () { + boolean result = state.awaitingIgnoreLocksForRefresh || state.awaitingUiResumeForRefresh; + state.awaitingIgnoreLocksForRefresh = false; + if (state.awaitingUiResumeForRefresh) { + if (viewport.activity != null) { + viewport.activity.removeSimpleStateListener(activityListener); + } + state.awaitingUiResumeForRefresh = false; + } + return result; + } + + private void refreshInteractionInfo (boolean byTimeout) { + cancelPostponedRefresh(); + if (!byTimeout) { + cancelRefresh(); + } else { + state.maxRefreshDate = 0; + } + if (!state.refreshMessageIds.isEmpty() && !viewport.isDestroyed()) { + if (viewport.needIgnore()) { + state.awaitingIgnoreLocksForRefresh = true; + if (Config.DEBUG_VIEW_MESSAGES) { + UI.showToast("Scheduling views refresh until lock changes", Toast.LENGTH_SHORT); + } + return; + } + if (viewport.activity != null && viewport.activity.getActivityState() != UI.State.RESUMED) { + state.awaitingUiResumeForRefresh = true; + viewport.activity.addSimpleStateListener(activityListener); + if (Config.DEBUG_VIEW_MESSAGES) { + UI.showToast("Scheduling views refresh until activity resume", Toast.LENGTH_SHORT); + } + return; + } + if (Config.DEBUG_VIEW_MESSAGES) { + UI.showToast("refresh views for " + state.refreshMessageIds.size() + " message(s), byTimeout: " + byTimeout, Toast.LENGTH_SHORT); + } + viewport.refreshMessageInteractionInfo(chatId, state.refreshMessageIds.toArray(), success -> + viewport.runOnUiThreadOptional(this::checkRefreshInteractionInfo) + ); + } + } + + private static long timeTillNextRefresh (long millis) { + long seconds = TimeUnit.MILLISECONDS.toSeconds(millis); + if (seconds < 15) { + return Math.abs(millis) % 3000; // once per 3 seconds for the first 15 seconds + } + if (seconds < 60) { + return millis % 5000; // once per 5 seconds for 15-60 seconds + } + long minutes = TimeUnit.MILLISECONDS.toMinutes(millis); + if (minutes < 30) { + return millis % 15000; // once per 15 seconds for 1-30 minutes + } + if (minutes < 60) { + return millis % 30000; // once per 30 seconds for 30-60 minutes + } + return millis % 60000; // once per minute + } + + @Override + public void performDestroy () { + cancelRefresh(); + if (state.awaitingUiResumeForRefresh && viewport.activity != null) { + viewport.activity.removeSimpleStateListener(activityListener); + } + } + } + + private static class ViewportState { + LongSet visibleChatIds = new LongSet(); + LongSet visibleProtectedChatIds = new LongSet(); + boolean needRestrictScreenshots; + boolean isDestroyed; + } + + public static class Viewport implements Destroyable { + private final TdlibMessageViewer context; + + public final TdApi.MessageSource messageSource; + private final BaseActivity activity; + public final List visibleChats = new ArrayList<>(); + private final ViewportState state = new ViewportState(); + private final List ignoreLocks = new ArrayList<>(); + private final List destroyListeners = new ArrayList<>(); + private final ChatListener chatListener = new ChatListener() { + @Override + public void onChatHasProtectedContentChanged (long chatId, boolean hasProtectedContent) { + context.tdlib.ui().post(() -> { + if (state.visibleChatIds.has(chatId) && state.visibleProtectedChatIds.add(chatId)) { + context.checkNeedRestrictScreenshots(); + } + }); + } + }; + private final MessageListener messageListener = new MessageListener() { + @Override + public void onMessageSendSucceeded (TdApi.Message message, long oldMessageId) { + runOnUiThreadOptional(() -> + replaceMessage(message, oldMessageId, null) + ); + } + + @Override + public void onMessageSendFailed (TdApi.Message message, long oldMessageId, TdApi.Error error) { + runOnUiThreadOptional(() -> + replaceMessage(message, oldMessageId, error) + ); + } + }; + + private void runOnUiThreadOptional (Runnable act) { + context.tdlib.ui().post(() -> { + if (!isDestroyed()) { + act.run(); + } + }); + } + + private final ViewController.AttachListener attachListener = (context, navigation, isAttached) -> { + notifyLockValueChanged(); + }; + + public Viewport (TdlibMessageViewer context, TdApi.MessageSource messageSource, @Nullable BaseActivity activity) { + this.context = context; + this.messageSource = messageSource; + this.activity = activity; + } + + public void addIgnoreLock (FutureBool act) { + this.ignoreLocks.add(act); + } + + public void addOnDestroyListener (Runnable act) { + destroyListeners.add(act); + } + + public void notifyLockValueChanged () { + context.checkNeedRestrictScreenshots(); + for (VisibleChat visibleChat : visibleChats) { + visibleChat.notifyViewportLockValueChanged(); + } + } + + private boolean needIgnore () { + if (!ignoreLocks.isEmpty()) { + for (FutureBool ignoreLock : ignoreLocks) { + if (ignoreLock.getBoolValue()) { + return true; + } + } + } + return false; + } + + public int indexOf (long chatId) { + int index = 0; + for (VisibleChat visibleChat : visibleChats) { + if (visibleChat.chatId == chatId) { + return index; + } + index++; + } + return -1; + } + + public @Nullable VisibleChat find (long chatId) { + int index = indexOf(chatId); + return index != -1 ? visibleChats.get(index) : null; + } + + private boolean replaceMessage (TdApi.Message message, long oldMessageId, @Nullable TdApi.Error error) { + if (isDestroyed()) { + return false; + } + VisibleChat visibleChat = find(message.chatId); + if (visibleChat == null) { + return false; + } + VisibleMessage visibleMessage = visibleChat.removeVisibleMessage(message.chatId, oldMessageId); + if (visibleMessage != null) { + return addVisibleMessage(message, visibleMessage.flags, visibleMessage.viewId, false); + } + return false; + } + + public boolean addVisibleMessage (TdApi.Message message, @Flags long flags, long viewId, boolean forceMarkAsRecent) { + return addVisibleMessageImpl(message.chatId, message, flags, viewId, forceMarkAsRecent); + } + + public boolean addVisibleMessage (long chatId, TdApi.SponsoredMessage sponsoredMessage, @Flags long flags, long viewId, boolean forceMarkAsRecent) { + return addVisibleMessageImpl(chatId, sponsoredMessage, flags, viewId, forceMarkAsRecent); + } + + private boolean addVisibleMessageImpl (long chatId, TdApi.Object rawMessage, @Flags long flags, long viewId, boolean forceMarkAsRecent) { + if (isDestroyed()) { + return false; + } + VisibleChat visibleChat = find(chatId); + if (visibleChat == null) { + visibleChat = new VisibleChat(this, chatId); + } + if (visibleChat.addVisibleMessage(rawMessage, flags, viewId, forceMarkAsRecent)) { + if (visibleChat.visibility.markAsVisible()) { + trackChat(visibleChat, true); + } + visibleChat.state.haveRecentlyViewedMessages = true; + updateState(); + return true; + } + return false; + } + + public boolean removeVisibleMessage (long chatId, long messageId) { + VisibleChat visibleChat = find(chatId); + if (visibleChat != null && visibleChat.removeVisibleMessage(chatId, messageId) != null) { + if (visibleChat.isEmpty() && visibleChat.visibility.markAsHidden()) { + trackChat(visibleChat, false); + } + updateState(); + return true; + } + return false; + } + + public boolean clear () { + return removeOtherVisibleChats(null); + } + + @Override + public void performDestroy () { + clear(); + if (!state.isDestroyed) { + state.isDestroyed = true; + for (Runnable runnable : destroyListeners) { + runnable.run(); + } + } + context.viewports.remove(this); + } + + public boolean removeOtherVisibleChats (@Nullable LongSet visibleChatIds) { + int clearedChatsCount = 0; + for (int i = visibleChats.size() - 1; i >= 0; i--) { + VisibleChat visibleChat = visibleChats.get(i); + if ((visibleChatIds == null || !visibleChatIds.has(visibleChat.chatId)) && visibleChat.clear()) { + if (visibleChat.isEmpty() && visibleChat.visibility.markAsHidden()) { + trackChat(visibleChat, false); + } + clearedChatsCount++; + } + } + if (clearedChatsCount > 0) { + updateState(); + return true; + } + return false; + } + + public boolean removeOtherVisibleMessages (long chatId, LongSet messageIds) { + VisibleChat visibleChat = find(chatId); + if (visibleChat != null && visibleChat.removeOtherVisibleMessages(messageIds)) { + if (visibleChat.isEmpty() && visibleChat.visibility.markAsHidden()) { + trackChat(visibleChat, false); + } + updateState(); + return true; + } + return false; + } + + public boolean removeOtherVisibleMessagesByViewId (long viewId) { + int updatedChatsCount = 0; + for (int index = visibleChats.size() - 1; index >= 0; index--) { + VisibleChat visibleChat = visibleChats.get(index); + if (visibleChat.removeOtherVisibleMessagesByViewId(viewId)) { + if (visibleChat.isEmpty() && visibleChat.visibility.markAsHidden()) { + trackChat(visibleChat, false); + } + updatedChatsCount++; + } + } + if (updatedChatsCount > 0) { + updateState(); + return true; + } + return false; + } + + private boolean checkNeedRestrictScreenshots () { + if (!state.visibleProtectedChatIds.isEmpty()) { + return true; + } + for (VisibleChat visibleChat : visibleChats) { + if (!visibleChat.state.visibleProtectedMessageIds.isEmpty()) { + return true; + } + } + return false; + } + + public boolean needRestrictScreenshots () { + return state.needRestrictScreenshots && !needIgnore(); + } + + private void updateState () { + boolean needRestrictScreenshots = checkNeedRestrictScreenshots(); + if (state.needRestrictScreenshots != needRestrictScreenshots) { + state.needRestrictScreenshots = needRestrictScreenshots; + context.onViewportNeedRestrictScreenshotsChanged(this, needRestrictScreenshots); + } + } + + private void trackChat (VisibleChat visibleChat, boolean isVisible) { + if (isVisible) { + if (!state.visibleChatIds.add(visibleChat.chatId)) + throw new IllegalStateException(); + boolean hasProtectedContent = ChatId.isSecret(visibleChat.chatId); + if (!hasProtectedContent) { + TdApi.Chat chat = context.tdlib.chat(visibleChat.chatId); + // Allowing screenshots for `chat == null` isn't a problem here, + // as that chat content would be protected by other components anyway. + hasProtectedContent = chat != null && chat.hasProtectedContent; + } + if (hasProtectedContent) { + if (!state.visibleProtectedChatIds.add(visibleChat.chatId)) + throw new IllegalStateException(); + } + visibleChats.add(visibleChat); + context.tdlib.listeners().subscribeToChatUpdates(visibleChat.chatId, chatListener); + context.tdlib.listeners().subscribeToMessageUpdates(visibleChat.chatId, messageListener); + } else { + if (!state.visibleChatIds.remove(visibleChat.chatId)) + throw new IllegalStateException(); + state.visibleProtectedChatIds.remove(visibleChat.chatId); + visibleChats.remove(visibleChat); + visibleChat.performDestroy(); + context.tdlib.listeners().unsubscribeFromChatUpdates(visibleChat.chatId, chatListener); + context.tdlib.listeners().unsubscribeFromMessageUpdates(visibleChat.chatId, messageListener); + } + } + + private void trackRecentlyViewedMessage (VisibleChat visibleChat, VisibleMessage visibleMessage) { + if (!isDestroyed()) { + context.trackRecentlyViewedMessage(this, visibleChat, visibleMessage); + } + } + + private void viewMessagesImpl (long chatId, long[] messageIds, TdApi.MessageSource messageSource, boolean forceRead, @Nullable RunnableBool after) { + if (messageIds.length > 0) { + context.tdlib.send(new TdApi.ViewMessages(chatId, messageIds, messageSource, forceRead), (ok, error) -> { + if (after != null) { + after.runWithBool(error == null); + } + if (error != null) { + TDLib.w("Unable to view %d messages in chat %d, source: %s, error: %s", messageIds.length, chatId, messageSource, TD.toErrorString(error)); + } else if (BuildConfig.DEBUG) { + Log.i("Viewed %d messages in chat %d, source: %s", messageIds.length, chatId, messageSource); + } + }); + } + } + + public void refreshMessageInteractionInfo (long chatId, long[] messageIds, @Nullable RunnableBool after) { + if (isDestroyed()) { + return; + } + TdApi.MessageSource messageSource; + switch (this.messageSource.getConstructor()) { + case TdApi.MessageSourceHistoryPreview.CONSTRUCTOR: + case TdApi.MessageSourceSearch.CONSTRUCTOR: + case TdApi.MessageSourceChatList.CONSTRUCTOR: + case TdApi.MessageSourceChatEventLog.CONSTRUCTOR: + messageSource = this.messageSource; + break; + default: + Td.assertMessageSource_eeb3e95(); + messageSource = new TdApi.MessageSourceHistoryPreview(); + break; + } + viewMessagesImpl(chatId, messageIds, messageSource, false, after); + } + + public boolean isDestroyed () { + return state.isDestroyed; + } + + public boolean haveRecentlyViewedMessages () { + if (isDestroyed()) { + return false; + } + for (VisibleChat visibleChat : visibleChats) { + if (visibleChat.state.haveRecentlyViewedMessages) { + return true; + } + } + return false; + } + + public void viewMessages (boolean onlyRecent, boolean forceRead, @Nullable RunnableBool after) { + if (isDestroyed()) { + return; + } + for (VisibleChat visibleChat : visibleChats) { + if (onlyRecent && !visibleChat.state.haveRecentlyViewedMessages) { + continue; + } + final long chatId = visibleChat.chatId; + final long[] messageIds = visibleChat.getMessageIds(true, true); + viewMessagesImpl(chatId, messageIds, messageSource, forceRead, after); + } + } + } + + public interface Listener { + void onNeedRestrictScreenshots (TdlibMessageViewer manager, boolean needRestrictScreenshots); + } + + private final Tdlib tdlib; + private final Set restrictScreenshotsReasons = new HashSet<>(); + private final ReferenceList listeners = new ReferenceList<>(true); + + public TdlibMessageViewer (Tdlib tdlib) { + this.tdlib = tdlib; + } + + public void addListener (Listener listener) { + listeners.add(listener); + } + + public void removeListener (Listener listener) { + listeners.remove(listener); + } + + private final List viewports = new ArrayList<>(); + + public Viewport createViewport (TdApi.MessageSource source, @Nullable ViewController target) { + Viewport viewport = new Viewport(this, source, target != null ? target.context() : null); + if (target != null) { + viewport.addIgnoreLock(() -> + !target.getAttachState() + ); + target.addDisallowScreenshotReason(viewport::needRestrictScreenshots); + target.addAttachStateListener(viewport.attachListener); + target.addDestroyListener(() -> target.removeAttachStateListener(viewport.attachListener)); + } + viewports.add(viewport); + return viewport; + } + + private boolean needRestrictScreenshots; + + private void checkNeedRestrictScreenshots () { + boolean needRestrictScreenshots = false; + for (Viewport viewport : restrictScreenshotsReasons) { + if (viewport.needRestrictScreenshots()) { + needRestrictScreenshots = true; + break; + } + } + if (this.needRestrictScreenshots != needRestrictScreenshots) { + this.needRestrictScreenshots = needRestrictScreenshots; + if (BuildConfig.DEBUG) { + UI.showToast("update restrictScreenshots to " + needRestrictScreenshots, Toast.LENGTH_SHORT); + } + for (Listener listener : listeners) { + listener.onNeedRestrictScreenshots(this, needRestrictScreenshots); + } + } + } + + public boolean needRestrictScreenshots () { + return needRestrictScreenshots; + } + + private void onViewportNeedRestrictScreenshotsChanged (@NonNull Viewport viewport, boolean needRestrictScreenshots) { + if (needRestrictScreenshots) { + restrictScreenshotsReasons.add(viewport); + } else { + restrictScreenshotsReasons.remove(viewport); + } + checkNeedRestrictScreenshots(); + } + + private final Set recentlyViewedMessages = new HashSet<>(); + + private void trackRecentlyViewedMessage (Viewport viewport, VisibleChat recentlyViewedChat, VisibleMessage recentlyViewedMessage) { + if (viewport.state.needRestrictScreenshots || !recentlyViewedMessage.needScreenshotNotification()) { + return; + } + long expiresAtMs = recentlyViewedMessage.visibility.hideTimeMs + TRACK_MESSAGE_TIMEOUT_MS; + long timeoutMs = expiresAtMs - System.currentTimeMillis(); + if (timeoutMs <= 0) { + return; + } + recentlyViewedMessages.add(recentlyViewedMessage); + tdlib.ui().postDelayed(() -> recentlyViewedMessages.remove(recentlyViewedMessage), timeoutMs); + } + + public boolean hasPotentiallyVisibleMessages () { + if (!recentlyViewedMessages.isEmpty()) { + return true; + } + for (Viewport viewport : viewports) { + if (viewport.state.visibleChatIds.size() > viewport.state.visibleProtectedChatIds.size()) { + return true; + } + if (!viewport.state.needRestrictScreenshots) { + return true; + } + } + return false; + } + + private @Nullable LongSparseArray screenshotMessages = null; + + private void addScreenshotMessage (long chatId, long messageId) { + if (screenshotMessages == null) { + screenshotMessages = new LongSparseArray<>(); + } + LongSet messageIds = screenshotMessages.get(chatId); + if (messageIds == null) { + messageIds = new LongSet(); + screenshotMessages.put(chatId, messageIds); + } + messageIds.add(messageId); + } + + public void onScreenshotTaken (int timeSeconds) { + if (screenshotMessages != null) { + screenshotMessages.clear(); + } + long timeMs = TimeUnit.SECONDS.toMillis(timeSeconds); + for (VisibleMessage recentlyViewedMessage : recentlyViewedMessages) { + if (recentlyViewedMessage.visibility.openTimeMs <= timeMs) { + addScreenshotMessage(recentlyViewedMessage.getChatId(), recentlyViewedMessage.getMessageId()); + } + } + for (Viewport viewport : viewports) { + for (VisibleChat visibleChat : viewport.visibleChats) { + if (visibleChat.visibility.openTimeMs > timeMs) + continue; + for (int index = 0; index < visibleChat.visibleMessages.size(); index++) { + VisibleMessage visibleMessage = visibleChat.visibleMessages.valueAt(index); + if (visibleMessage.visibility.openTimeMs > timeMs) + continue; + if (visibleMessage.needScreenshotNotification()) { + addScreenshotMessage(visibleMessage.getChatId(), visibleMessage.getMessageId()); + } + } + } + } + if (screenshotMessages != null && !screenshotMessages.isEmpty()) { + for (int i = 0; i < screenshotMessages.size(); i++) { + long chatId = screenshotMessages.keyAt(i); + long[] messageIds = screenshotMessages.valueAt(i).toArray(); + tdlib.send(new TdApi.ViewMessages(chatId, messageIds, new TdApi.MessageSourceScreenshot(), false), (ok, error) -> { + if (error != null) { + TDLib.w("Error notifying about screenshot of %d messages in chat %d: %s", messageIds.length, chatId, TD.toErrorString(error)); + } + }); + } + screenshotMessages.clear(); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotification.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotification.java index 7e297a83f8..c45e8eca2d 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotification.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotification.java @@ -33,6 +33,7 @@ import org.thunderdog.challegram.TDLib; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.ContentPreview; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.ImageReader; @@ -140,8 +141,11 @@ public boolean isScheduled () { case TdApi.NotificationTypeNewCall.CONSTRUCTOR: case TdApi.NotificationTypeNewSecretChat.CONSTRUCTOR: return false; + default: { + Td.assertNotificationType_dd6d967f(); + throw Td.unsupported(notification.type); + } } - throw new UnsupportedOperationException(notification.type.toString()); } public boolean isVisuallySilent () { // Display bell icon @@ -164,7 +168,7 @@ public boolean canMergeWith (@Nullable TdlibNotification n) { public boolean isPinnedMessage () { switch (notification.type.getConstructor()) { case TdApi.NotificationTypeNewMessage.CONSTRUCTOR: - return ((TdApi.NotificationTypeNewMessage) notification.type).message.content.getConstructor() == TdApi.MessagePinMessage.CONSTRUCTOR; + return Td.isPinned(((TdApi.NotificationTypeNewMessage) notification.type).message.content); case TdApi.NotificationTypeNewPushMessage.CONSTRUCTOR: return Td.isPinned(((TdApi.NotificationTypeNewPushMessage) notification.type).content); case TdApi.NotificationTypeNewCall.CONSTRUCTOR: @@ -178,7 +182,7 @@ public boolean needContentPreview () { switch (notification.type.getConstructor()) { case TdApi.NotificationTypeNewMessage.CONSTRUCTOR: { TdApi.Message message = ((TdApi.NotificationTypeNewMessage) notification.type).message; - return !TD.isSecret(message) && ((TdApi.NotificationTypeNewMessage) notification.type).message.selfDestructTime == 0; + return !Td.isSecret(message.content) && ((TdApi.NotificationTypeNewMessage) notification.type).message.selfDestructType == null; } case TdApi.NotificationTypeNewPushMessage.CONSTRUCTOR: { TdApi.PushMessageContent push = ((TdApi.NotificationTypeNewPushMessage) notification.type).content; @@ -274,9 +278,9 @@ public TdApi.NotificationType getNotificationContent () { public boolean isStickerContent () { switch (notification.type.getConstructor()) { case TdApi.NotificationTypeNewMessage.CONSTRUCTOR: - return ((TdApi.NotificationTypeNewMessage) notification.type).message.content.getConstructor() == TdApi.MessageSticker.CONSTRUCTOR; + return Td.isSticker(((TdApi.NotificationTypeNewMessage) notification.type).message.content); case TdApi.NotificationTypeNewPushMessage.CONSTRUCTOR: - return ((TdApi.NotificationTypeNewPushMessage) notification.type).content.getConstructor() == TdApi.PushMessageContentSticker.CONSTRUCTOR; + return Td.isSticker(((TdApi.NotificationTypeNewPushMessage) notification.type).content); case TdApi.NotificationTypeNewCall.CONSTRUCTOR: case TdApi.NotificationTypeNewSecretChat.CONSTRUCTOR: break; @@ -317,7 +321,7 @@ public CharSequence getTextRepresentation (Tdlib tdlib, boolean onlyPinned, bool boolean isForward = false; for (TdlibNotification notification : mergedList) { TdApi.Message message = notification.findMessage(); - if (ChatId.isSecret(group.getChatId()) && message.selfDestructTime != 0) { + if (ChatId.isSecret(group.getChatId()) && message.selfDestructType != null) { return Lang.plural(R.string.xNewMessages, mergedList.size()); } if (message.forwardInfo != null) { @@ -325,12 +329,12 @@ public CharSequence getTextRepresentation (Tdlib tdlib, boolean onlyPinned, bool } messages.add(message); } - TD.ContentPreview content; + ContentPreview content; if (isForward) { - content = new TD.ContentPreview(TD.EMOJI_FORWARD, 0, Lang.plural(R.string.xForwards, mergedList.size()), true); + content = new ContentPreview(ContentPreview.EMOJI_FORWARD, 0, Lang.plural(R.string.xForwards, mergedList.size()), true); } else { Tdlib.Album album = new Tdlib.Album(messages); - content = TD.getAlbumPreview(tdlib, messages.get(0), album, allowContent); + content = ContentPreview.getAlbumPreview(tdlib, messages.get(0), album, allowContent); } if (hasCustomText != null && !content.isTranslatable) { hasCustomText[0] = true; @@ -343,26 +347,23 @@ public CharSequence getTextRepresentation (Tdlib tdlib, boolean onlyPinned, bool case TdApi.NotificationTypeNewMessage.CONSTRUCTOR: { TdApi.Message message = ((TdApi.NotificationTypeNewMessage) notification.type).message; - if (ChatId.isSecret(group.getChatId()) && message.selfDestructTime != 0) { + if (ChatId.isSecret(group.getChatId()) && message.selfDestructType != null) { return Lang.getString(R.string.YouHaveNewMessage); } // TODO move this to TD.getNotificationPreview? - switch (message.content.getConstructor()) { - case TdApi.MessagePinMessage.CONSTRUCTOR: { - long messageId = ((TdApi.MessagePinMessage) message.content).messageId; - TdApi.Message pinnedMessage = messageId != 0 ? tdlib.getMessageLocally(message.chatId, messageId) : null; - if (onlyPinned) { - if (pinnedMessage != null) - message = pinnedMessage; - } else { - return wrapEdited(Lang.getPinnedMessageText(tdlib, message.senderId, pinnedMessage, false)); - } - break; + if (Td.isPinned(message.content)) { + long messageId = ((TdApi.MessagePinMessage) message.content).messageId; + TdApi.Message pinnedMessage = messageId != 0 ? tdlib.getMessageLocally(message.chatId, messageId) : null; + if (onlyPinned) { + if (pinnedMessage != null) + message = pinnedMessage; + } else { + return wrapEdited(Lang.getPinnedMessageText(tdlib, message.senderId, pinnedMessage, false)); } } - TD.ContentPreview content = TD.getNotificationPreview(tdlib, getChatId(), message, allowContent); + ContentPreview content = ContentPreview.getNotificationPreview(tdlib, getChatId(), message, allowContent); if (hasCustomText != null && !content.isTranslatable) { hasCustomText[0] = true; } @@ -373,9 +374,7 @@ public CharSequence getTextRepresentation (Tdlib tdlib, boolean onlyPinned, bool } case TdApi.NotificationTypeNewPushMessage.CONSTRUCTOR: { TdApi.NotificationTypeNewPushMessage push = (TdApi.NotificationTypeNewPushMessage) notification.type; - TD.ContentPreview content = TD.getNotificationPreview(tdlib, getChatId(), push, allowContent); - if (content == null) - throw new UnsupportedOperationException(Integer.toString(push.content.getConstructor())); + ContentPreview content = ContentPreview.getNotificationPreview(tdlib, getChatId(), push, allowContent); if (hasCustomText != null && !content.isTranslatable) { hasCustomText[0] = true; } @@ -385,7 +384,7 @@ public CharSequence getTextRepresentation (Tdlib tdlib, boolean onlyPinned, bool return null; } - private CharSequence getPreview (TD.ContentPreview content) { + private CharSequence getPreview (ContentPreview content) { TdApi.FormattedText formattedText = content.buildFormattedText(false); CharSequence text = TD.toCharSequence(formattedText, false, false); if (text instanceof Spanned) { diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationChannelGroup.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationChannelGroup.java index 0921259646..5f6ea1f789 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationChannelGroup.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationChannelGroup.java @@ -406,7 +406,7 @@ public static void cleanupChannels (Tdlib tdlib) { String groupId = channel.getGroup(); if (StringUtils.isEmpty(groupId) || !groupId.startsWith(groupPrefix)) continue; - int userId = StringUtils.parseInt(groupId.substring(groupPrefix.length())); + long userId = StringUtils.parseInt(groupId.substring(groupPrefix.length())); if (userId != accountUserId) continue; String id = channel.getId(); @@ -467,7 +467,7 @@ public static void cleanupChannelGroups (TdlibManager context) { android.app.NotificationChannelGroup group = groups.get(i); String groupId = group.getId(); if (!StringUtils.isEmpty(groupId) && groupId.startsWith(prefix)) { - int userId = StringUtils.parseInt(groupId.substring(prefix.length())); + long userId = StringUtils.parseInt(groupId.substring(prefix.length())); if (userId == 0 || Arrays.binarySearch(userIds, userId) < 0) { m.deleteNotificationChannelGroup(groupId); } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationExtras.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationExtras.java index bce454d60f..2413f3ef46 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationExtras.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationExtras.java @@ -116,6 +116,7 @@ public TdlibNotificationExtras (int accountId, int category) { public final int maxNotificationId; public final int notificationGroupId; public final boolean needReply; + public final boolean forceExternalReply = false; // Keep this flag in case it will be needed in future public final boolean areMentions; public final long[] messageIds; public final long[] userIds; diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationManager.java index 1fc6f38847..0583352e06 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationManager.java @@ -1354,7 +1354,11 @@ private static void resetToDefault (TdApi.ScopeNotificationSettings settings) { } private static TdApi.ScopeNotificationSettings newDefaults () { - return new TdApi.ScopeNotificationSettings(0, 0, true, false, false); + return new TdApi.ScopeNotificationSettings( + 0, 0, true, + true, false, 0, true, + false, false + ); } // Ringtone @@ -1612,7 +1616,7 @@ public void resetNotificationSettings (boolean onlyLocal) { updated = Settings.instance().setNeedSplitNotificationCategories(true); updated = Settings.instance().setNeedHideSecretChats(false) || updated; if (Settings.instance().resetBadge()) { - tdlib.context().resetBadge(); + tdlib.context().resetBadge(true); } if (updated) { tdlib.context().onUpdateAllNotifications(); @@ -2098,6 +2102,7 @@ void onUpdateMessageSendSucceeded (TdApi.UpdateMessageSendSucceeded update) { c = UI.getCurrentStackItem(); } catch (IndexOutOfBoundsException ignored) { } if (((c instanceof MessagesController && ((MessagesController) c).compareChat(sentMessage.chatId)) || (c instanceof MainController)) && !c.isPaused()) { + //noinspection SwitchIntDef switch (sentMessage.content.getConstructor()) { case TdApi.MessageScreenshotTaken.CONSTRUCTOR: case TdApi.MessageChatSetMessageAutoDeleteTime.CONSTRUCTOR: { diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationMediaFile.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationMediaFile.java index 3c4debec4c..612cd4605b 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationMediaFile.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationMediaFile.java @@ -21,6 +21,8 @@ import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.unsorted.Settings; +import me.vkryl.td.Td; + public class TdlibNotificationMediaFile { public static final int TYPE_IMAGE = 0; public static final int TYPE_STICKER = 1; @@ -57,8 +59,10 @@ private static int toType (TdApi.StickerFormat format) { return TYPE_LOTTIE_STICKER; case TdApi.StickerFormatWebp.CONSTRUCTOR: return TYPE_STICKER; + default: + Td.assertStickerFormat_4fea4648(); + throw Td.unsupported(format); } - throw new UnsupportedOperationException(format.toString()); } @Nullable @@ -73,6 +77,7 @@ public static TdlibNotificationMediaFile newFile (Tdlib tdlib, TdApi.Chat chat, switch (notificationType.getConstructor()) { case TdApi.NotificationTypeNewMessage.CONSTRUCTOR: { TdApi.Message message = ((TdApi.NotificationTypeNewMessage) notificationType).message; + //noinspection SwitchIntDef switch (message.content.getConstructor()) { case TdApi.MessagePhoto.CONSTRUCTOR: { TdApi.MessagePhoto photo = (TdApi.MessagePhoto) message.content; @@ -113,6 +118,7 @@ public static TdlibNotificationMediaFile newFile (Tdlib tdlib, TdApi.Chat chat, } case TdApi.NotificationTypeNewPushMessage.CONSTRUCTOR: { TdApi.PushMessageContent push = ((TdApi.NotificationTypeNewPushMessage) notificationType).content; + //noinspection SwitchIntDef switch (push.getConstructor()) { case TdApi.PushMessageContentPhoto.CONSTRUCTOR: { TdApi.PushMessageContentPhoto photo = (TdApi.PushMessageContentPhoto) push; diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationStyle.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationStyle.java index c63f865444..39714c25d4 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationStyle.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationStyle.java @@ -56,7 +56,6 @@ import org.thunderdog.challegram.receiver.TGRemoveAllReceiver; import org.thunderdog.challegram.receiver.TGRemoveReceiver; import org.thunderdog.challegram.receiver.TGWearReplyReceiver; -import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.DrawAlgorithms; import org.thunderdog.challegram.tool.Intents; import org.thunderdog.challegram.tool.Strings; @@ -80,6 +79,7 @@ import me.leolin.shortcutbadger.ShortcutBadger; import me.vkryl.core.StringUtils; import me.vkryl.td.ChatId; +import me.vkryl.td.Td; public class TdlibNotificationStyle implements TdlibNotificationStyleDelegate, FileUpdateListener { private static final boolean USE_GROUPS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH; @@ -137,7 +137,8 @@ public final void rebuildNotificationsSilently (@NonNull Context context, @NonNu matches = tdlib.isChannelFast(group.getChatId()); break; default: - throw new UnsupportedOperationException(scope.toString()); + Td.assertNotificationSettingsScope_edff9c28(); + throw Td.unsupported(scope); } if (matches) { if (displayChildNotification(manager, context, helper, badgeCount, allowPreview, group, null, true) != DISPLAY_STATE_FAIL) { @@ -821,12 +822,16 @@ protected static String makeSortKey (TdlibNotification notification, boolean isS static Person buildPerson (TdlibNotificationManager context, boolean isSelfChat, boolean isGroupChat, boolean isChannel, TdApi.User user, @Nullable String id, boolean isScheduled, boolean isSilent, boolean allowDownload) { if (user == null) - return new Person.Builder().setName("").build(); + return new Person.Builder().setName(Strings.ELLIPSIS).build(); if (context.isSelfUserId(user.id)) id = "0"; else if (id == null) id = Long.toString(user.id); - return buildPerson(context, isSelfChat, isGroupChat, isChannel, id, TD.isBot(user), TD.getUserName(user), TD.getLetters(user), TD.getAvatarColorId(user.id, context.myUserId()), user.profilePhoto != null ? user.profilePhoto.small : null, isScheduled, isSilent, allowDownload); + String name = TD.getUserName(user); + if (StringUtils.isEmpty(name)) { + name = Strings.ELLIPSIS; + } + return buildPerson(context, isSelfChat, isGroupChat, isChannel, id, TD.isBot(user), name, TD.getLetters(user), context.tdlib().cache().userAccentColor(user), user.profilePhoto != null ? user.profilePhoto.small : null, isScheduled, isSilent, allowDownload); } public static Person buildPerson (TdlibNotificationManager context, TdApi.Chat chat, TdlibNotification notification, boolean isScheduled, boolean isSilent, boolean allowDownload) { @@ -843,12 +848,12 @@ public static Person buildPerson (TdlibNotificationManager context, TdApi.Chat c if (TD.isMultiChat(chat)) { String senderName = notification.findSenderName(); TdApi.Chat senderChat = tdlib.chat(senderChatId); - return buildPerson(context, notification.isSelfChat(), TD.isMultiChat(chat), tdlib.isChannelChat(chat), Long.toString(senderChatId), tdlib.isBotChat(senderChatId) || tdlib.isChannel(senderChatId), senderName, TD.getLetters(senderName), tdlib.chatAvatarColorId(senderChatId), senderChat != null && senderChat.photo != null ? senderChat.photo.small : null, isScheduled, isSilent, allowDownload); + return buildPerson(context, notification.isSelfChat(), TD.isMultiChat(chat), tdlib.isChannelChat(chat), Long.toString(senderChatId), tdlib.isBotChat(senderChatId) || tdlib.isChannel(senderChatId), senderName, TD.getLetters(senderName), tdlib.chatAccentColor(senderChatId), senderChat != null && senderChat.photo != null ? senderChat.photo.small : null, isScheduled, isSilent, allowDownload); } - return buildPerson(context, notification.isSelfChat(), TD.isMultiChat(chat), tdlib.isChannelChat(chat), Long.toString(chat.id), tdlib.isBotChat(chat) || tdlib.isChannelChat(chat), chat.title, tdlib.chatLetters(chat), tdlib.chatAvatarColorId(chat), chat.photo != null ? chat.photo.small : null, isScheduled, isSilent, allowDownload); + return buildPerson(context, notification.isSelfChat(), TD.isMultiChat(chat), tdlib.isChannelChat(chat), Long.toString(chat.id), tdlib.isBotChat(chat) || tdlib.isChannelChat(chat), chat.title, tdlib.chatLetters(chat), tdlib.chatAccentColor(chat), chat.photo != null ? chat.photo.small : null, isScheduled, isSilent, allowDownload); } - public static Person buildPerson (TdlibNotificationManager context, boolean isSelfChat, boolean isGroupChat, boolean isChannel, String id, boolean isBot, String name, Letters letters, @ColorId int colorId, TdApi.File photo, boolean isScheduled, boolean isSilent, boolean allowDownload) { + public static Person buildPerson (TdlibNotificationManager context, boolean isSelfChat, boolean isGroupChat, boolean isChannel, String id, boolean isBot, String name, Letters letters, TdlibAccentColor accentColor, TdApi.File photo, boolean isScheduled, boolean isSilent, boolean allowDownload) { Person.Builder b = new Person.Builder(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { b.setKey(id); @@ -856,7 +861,7 @@ public static Person buildPerson (TdlibNotificationManager context, boolean isSe b.setName(Lang.getSilentNotificationTitle(name, true, isSelfChat, isGroupChat, isChannel, isScheduled, isSilent)); Bitmap bitmap = null; // TODO load from cache if (!U.isValidBitmap(bitmap)) { - bitmap = isSelfChat ? TdlibNotificationUtils.buildSelfIcon(context.tdlib()) : TdlibNotificationUtils.buildLargeIcon(context.tdlib(), photo, colorId, letters, true, allowDownload); + bitmap = isSelfChat ? TdlibNotificationUtils.buildSelfIcon(context.tdlib()) : TdlibNotificationUtils.buildLargeIcon(context.tdlib(), photo, accentColor, letters, true, allowDownload); } if (U.isValidBitmap(bitmap)) { b.setIcon(IconCompat.createWithBitmap(bitmap)); diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationUtils.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationUtils.java index bf6d73fd99..28f3ed431d 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationUtils.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibNotificationUtils.java @@ -32,7 +32,6 @@ import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.TDLib; -import org.thunderdog.challegram.TokenRetrieverFactory; import org.thunderdog.challegram.U; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.config.Device; @@ -40,8 +39,10 @@ import org.thunderdog.challegram.loader.ImageCache; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.ImageReader; +import org.thunderdog.challegram.push.TokenRetrieverFactory; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.theme.ThemeId; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Fonts; import org.thunderdog.challegram.tool.Intents; @@ -52,6 +53,8 @@ import org.thunderdog.challegram.util.TokenRetriever; import org.thunderdog.challegram.util.text.Letters; +import me.vkryl.td.Td; + public class TdlibNotificationUtils { private static TextPaint lettersPaint; private static TextPaint lettersPaintFake; @@ -62,7 +65,7 @@ public static Bitmap buildLargeIcon (Tdlib tdlib, TdApi.Chat chat, boolean allow if (tdlib.isSelfChat(chat)) { return buildSelfIcon(tdlib); } else { - return buildLargeIcon(tdlib, chat.photo != null ? chat.photo.small : null, tdlib.chatAvatarColorId(chat), tdlib.chatLetters(chat), true, allowDownload); + return buildLargeIcon(tdlib, chat.photo != null ? chat.photo.small : null, tdlib.chatAccentColor(chat), tdlib.chatLetters(chat), true, allowDownload); } } @@ -104,7 +107,7 @@ public static Bitmap buildSelfIcon (Tdlib tdlib) { bitmapPaint.setColor(color); if (Device.ROUND_NOTIFICAITON_IMAGE) { fillingPaint.setColor(color); - c.drawCircle(size / 2, size / 2, size / 2, fillingPaint); + c.drawCircle(size / 2f, size / 2f, size / 2f, fillingPaint); } else { c.drawColor(color); } @@ -113,9 +116,9 @@ public static Bitmap buildSelfIcon (Tdlib tdlib) { float scale = (float) size / (float) Screen.dp(44f); if (scale != 1f) { c.save(); - c.scale(scale, scale, size / 2, size / 2); + c.scale(scale, scale, size / 2f, size / 2f); } - Drawables.draw(c, d, size / 2 - d.getMinimumWidth() / 2, size / 2 - d.getMinimumHeight() / 2, PorterDuffPaint.get(ColorId.avatar_content)); + Drawables.draw(c, d, size / 2f - d.getMinimumWidth() / 2f, size / 2f - d.getMinimumHeight() / 2f, PorterDuffPaint.get(ColorId.avatar_content)); if (scale != 1f) { c.restore(); } @@ -130,7 +133,7 @@ public static Bitmap buildSelfIcon (Tdlib tdlib) { return bitmap; } - public static Bitmap buildLargeIcon (Tdlib tdlib, TdApi.File rawFile, @ColorId int colorId, Letters letters, boolean allowSyncDownload, boolean allowDownload) { + public static Bitmap buildLargeIcon (Tdlib tdlib, TdApi.File rawFile, TdlibAccentColor accentColor, Letters letters, boolean allowSyncDownload, boolean allowDownload) { Bitmap avatarBitmap = null; if (rawFile != null) { tdlib.files().syncFile(rawFile, null, 500); @@ -188,23 +191,29 @@ public static Bitmap buildLargeIcon (Tdlib tdlib, TdApi.File rawFile, @ColorId i Bitmap createdBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(createdBitmap); - final int color = Theme.getColor(colorId, tdlib.settings().globalTheme()); + long complexColor = accentColor.getPrimaryComplexColor(); + final @ThemeId int themeId = tdlib.settings().globalTheme(); + final int color = Theme.toColorInt(complexColor, themeId); bitmapPaint.setColor(color); if (Device.ROUND_NOTIFICAITON_IMAGE) { fillingPaint.setColor(color); - c.drawCircle(size / 2, size / 2, size / 2, fillingPaint); + c.drawCircle(size / 2f, size / 2f, size / 2f, fillingPaint); } else { c.drawColor(color); } if (avatarBitmap == null) { - c.drawText(letters.text, size / 2 - U.measureText(letters.text, letters.needFakeBold ? lettersPaintFake : lettersPaint) / 2, size / 2 + Screen.dp(8f, MAX_DENSITY), letters.needFakeBold ? lettersPaintFake : lettersPaint); + final long lettersComplexColor = accentColor.getPrimaryContentComplexColor(); + final int lettersColor = Theme.toColorInt(lettersComplexColor, themeId); + final Paint paint = letters.needFakeBold ? lettersPaintFake : lettersPaint; + paint.setColor(lettersColor); + c.drawText(letters.text, size / 2f - U.measureText(letters.text, letters.needFakeBold ? lettersPaintFake : lettersPaint) / 2, size / 2f + Screen.dp(8f, MAX_DENSITY), paint); } else { float scale = (float) size / (float) avatarBitmap.getWidth(); c.save(); - c.scale(scale, scale, size / 2, size / 2); - c.drawBitmap(avatarBitmap, size / 2 - avatarBitmap.getWidth() / 2, size / 2 - avatarBitmap.getHeight() / 2, bitmapPaint); + c.scale(scale, scale, size / 2f, size / 2f); + c.drawBitmap(avatarBitmap, size / 2f - avatarBitmap.getWidth() / 2f, size / 2f - avatarBitmap.getHeight() / 2f, bitmapPaint); c.restore(); } bitmap = createdBitmap; @@ -245,14 +254,25 @@ public static synchronized boolean initialize () { return tokenRetriever.initialize(UI.getAppContext()); } + public static @NonNull TokenRetriever getTokenRetriever () { + if (tokenRetriever == null) { + initialize(); + } + return tokenRetriever; + } + @DeviceTokenType public static int getDeviceTokenType (TdApi.DeviceToken deviceToken) { switch (deviceToken.getConstructor()) { - // TODO more push services case TdApi.DeviceTokenFirebaseCloudMessaging.CONSTRUCTOR: return DeviceTokenType.FIREBASE_CLOUD_MESSAGING; + case TdApi.DeviceTokenHuaweiPush.CONSTRUCTOR: + return DeviceTokenType.HUAWEI_PUSH_SERVICE; + case TdApi.DeviceTokenSimplePush.CONSTRUCTOR: + return DeviceTokenType.SIMPLE_PUSH_SERVICE; default: - throw new UnsupportedOperationException(deviceToken.toString()); + Td.assertDeviceToken_de4a4f61(); + throw Td.unsupported(deviceToken); } } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibOptionListener.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibOptionListener.java index d417c6c38a..f0ef0d1966 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibOptionListener.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibOptionListener.java @@ -23,4 +23,6 @@ default void onSuggestedLanguagePackChanged (String suggestedLanguagePackId, TdA default void onContactRegisteredNotificationsDisabled (boolean areDisabled) { } default void onSuggestedActionsChanged (TdApi.SuggestedAction[] addedActions, TdApi.SuggestedAction[] removedActions) { } default void onArchiveAndMuteChatsFromUnknownUsersEnabled (boolean enabled) { } + default void onAccentColorsChanged () { } + default void onProfileAccentColorsChanged (boolean listChanged) { } } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibProvider.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibProvider.java index b49ad5ef4c..56f5968371 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibProvider.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibProvider.java @@ -17,6 +17,8 @@ import androidx.annotation.NonNull; public interface TdlibProvider { - int accountId (); + default int accountId () { + return tdlib().id(); + } @NonNull Tdlib tdlib (); } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibSender.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibSender.java index b7e3a2ed05..02cdb76228 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibSender.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibSender.java @@ -12,6 +12,7 @@ */ package org.thunderdog.challegram.telegram; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.drinkless.tdlib.TdApi; @@ -34,34 +35,39 @@ public class TdlibSender { private final Tdlib tdlib; private final long inChatId; - private final TdApi.MessageSender sender; - - private final String name, nameShort; - private final TdApi.Usernames usernames; - private final TdApi.ChatPhotoInfo photo; - private final Letters letters; - private final AvatarPlaceholder.Metadata placeholderMetadata; + private final @Nullable TdApi.MessageSender sender; + private final TdApi.MessageSponsor sponsor; + + private String name, nameShort; + private TdApi.Usernames usernames; + private TdApi.ChatPhotoInfo photo; + private Letters letters; + private AvatarPlaceholder.Metadata placeholderMetadata; private final int flags; public TdlibSender (Tdlib tdlib, long inChatId, TdApi.MessageSender sender) { this(tdlib, inChatId, sender, null, false); } - public TdlibSender (Tdlib tdlib, long inChatId, TdApi.MessageSender sender, @Nullable MessagesManager manager, boolean isDemo) { + public TdlibSender (Tdlib tdlib, long inChatId, @NonNull TdApi.MessageSender sender, @Nullable MessagesManager manager, boolean isDemo) { this.tdlib = tdlib; this.inChatId = inChatId; this.sender = sender; + this.sponsor = null; + this.flags = setSender(sender, manager, isDemo); + } + private int setSender (@NonNull TdApi.MessageSender sender, @Nullable MessagesManager manager, boolean isDemo) { int flags = BitwiseUtils.setFlag(0, FLAG_DEMO, isDemo); switch (sender.getConstructor()) { case TdApi.MessageSenderChat.CONSTRUCTOR: { final long chatId = ((TdApi.MessageSenderChat) sender).chatId; - TdApi.Chat chat = tdlib.chat(chatId); + TdApi.Chat chat = tdlib.chatStrict(chatId); this.name = tdlib.chatTitle(chat, false); this.nameShort = tdlib.chatTitle(chat, false, true); this.usernames = tdlib.chatUsernames(chat); - this.photo = chat != null ? chat.photo : null; + this.photo = chat.photo; this.letters = tdlib.chatLetters(chat); this.placeholderMetadata = tdlib.chatPlaceholderMetadata(chatId, chat, false); @@ -92,10 +98,48 @@ public TdlibSender (Tdlib tdlib, long inChatId, TdApi.MessageSender sender, @Nul break; } default: { - throw new UnsupportedOperationException(sender.toString()); + Td.assertMessageSender_439d4c9c(); + throw Td.unsupported(sender); } } - this.flags = flags; + return flags; + } + + public TdlibSender (Tdlib tdlib, long inChatId, TdApi.MessageSponsor sponsor) { + this.tdlib = tdlib; + this.inChatId = inChatId; + this.sponsor = sponsor; + TdApi.MessageSender sender; + switch (sponsor.type.getConstructor()) { + case TdApi.MessageSponsorTypeBot.CONSTRUCTOR: + TdApi.MessageSponsorTypeBot bot = (TdApi.MessageSponsorTypeBot) sponsor.type; + sender = new TdApi.MessageSenderUser(bot.botUserId); + break; + case TdApi.MessageSponsorTypePublicChannel.CONSTRUCTOR: + TdApi.MessageSponsorTypePublicChannel publicChannel = (TdApi.MessageSponsorTypePublicChannel) sponsor.type; + sender = new TdApi.MessageSenderChat(publicChannel.chatId); + break; + case TdApi.MessageSponsorTypePrivateChannel.CONSTRUCTOR: + case TdApi.MessageSponsorTypeWebsite.CONSTRUCTOR: + case TdApi.MessageSponsorTypeWebApp.CONSTRUCTOR: + sender = null; + break; + default: + Td.assertMessageSponsorType_cdabde01(); + throw Td.unsupported(sponsor.type); + } + if (sender != null) { + this.sender = sender; + this.flags = setSender(sender, null, false); + } else { + this.sender = null; + this.flags = 0; + this.photo = sponsor.photo; + String name = tdlib.sponsorName(sponsor); + this.name = this.nameShort = name; + this.letters = TD.getLetters(this.name); + this.placeholderMetadata = new AvatarPlaceholder.Metadata(tdlib.accentColor(TdlibAccentColor.BuiltInId.GREEN), this.letters); + } } public TdApi.MessageSender toSender () { @@ -103,11 +147,11 @@ public TdApi.MessageSender toSender () { } public boolean isUser () { - return sender.getConstructor() == TdApi.MessageSenderUser.CONSTRUCTOR; + return sender != null && sender.getConstructor() == TdApi.MessageSenderUser.CONSTRUCTOR; } public boolean isChat () { - return sender.getConstructor() == TdApi.MessageSenderChat.CONSTRUCTOR; + return sender != null && sender.getConstructor() == TdApi.MessageSenderChat.CONSTRUCTOR; } public boolean isAnonymousGroupAdmin () { @@ -168,12 +212,8 @@ public AvatarPlaceholder.Metadata getPlaceholderMetadata () { return placeholderMetadata; } - public int getAvatarColorId () { - return placeholderMetadata.colorId; - } - - public int getNameColorId () { - return TD.getNameColorId(getAvatarColorId()); + public TdlibAccentColor getAccentColor () { + return placeholderMetadata.accentColor; } public ImageFile getAvatar () { @@ -182,8 +222,10 @@ public ImageFile getAvatar () { return tdlib.chatAvatar(((TdApi.MessageSenderChat) sender).chatId); case TdApi.MessageSenderUser.CONSTRUCTOR: return tdlib.cache().userAvatar(((TdApi.MessageSenderUser) sender).userId); + default: + Td.assertMessageSender_439d4c9c(); + throw Td.unsupported(sender); } - throw new AssertionError(); } // flags diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibSettingsManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibSettingsManager.java index 86d0d95456..798c833c26 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibSettingsManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibSettingsManager.java @@ -25,6 +25,7 @@ import org.thunderdog.challegram.BuildConfig; import org.thunderdog.challegram.Log; import org.thunderdog.challegram.U; +import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.theme.ChatStyle; import org.thunderdog.challegram.theme.TGBackground; @@ -40,10 +41,12 @@ import me.vkryl.core.BitwiseUtils; import me.vkryl.core.StringUtils; +import me.vkryl.core.collection.IntSet; import me.vkryl.core.collection.LongSparseLongArray; import me.vkryl.core.reference.ReferenceList; import me.vkryl.core.util.Blob; import me.vkryl.leveldb.LevelDB; +import me.vkryl.td.ChatPosition; import me.vkryl.td.Td; public class TdlibSettingsManager implements CleanupStartupDelegate { @@ -69,7 +72,7 @@ public class TdlibSettingsManager implements CleanupStartupDelegate { private static final String NOTIFICATION_DATA_PREFIX = "notification_data_"; public static final String CONVERSION_PREFIX = "pending_conversion_"; - public static final String DEVICE_TOKEN_KEY = "registered_device_token"; + public static final String DEVICE_TOKEN_OR_ENDPOINT_KEY = "registered_device_token"; public static final String DEVICE_TOKEN_TYPE_KEY = "registered_device_token_type"; public static final String DEVICE_UID_KEY = "registered_device_uid"; public static final String DEVICE_OTHER_UID_KEY = "registered_device_uid_other"; @@ -157,6 +160,15 @@ public void onPerformUserCleanup () { editor.remove(key(THEME_GLOBAL_THEME_NIGHT_KEY, accountId)); editor.remove(key(THEME_GLOBAL_THEME_DAYLIGHT_KEY, accountId)); editor.remove(key(LOCAL_CHAT_IDS_COUNT, accountId)); + editor.remove(key(MAIN_CHAT_LIST_ENABLED, accountId)); + editor.remove(key(ARCHIVE_CHAT_LIST_ENABLED, accountId)); + editor.remove(key(ARCHIVE_CHAT_LIST_POSITION, accountId)); + editor.remove(key(DISABLED_CHAT_FILTER_IDS, accountId)); + if (!Config.CHAT_FOLDERS_APPEARANCE_IS_GLOBAL) { + editor.remove(key(CHAT_FOLDER_BADGE, accountId)); + editor.remove(key(CHAT_FOLDER_STYLE, accountId)); + editor.remove(key(CHAT_FOLDER_OPTIONS, accountId)); + } // editor.remove(key(PEER_TO_PEER_KEY, accountId)); Settings.instance().removeScrollPositions(accountId, editor); String dismissPrefix = key(DISMISS_MESSAGE_PREFIX, accountId); @@ -183,6 +195,13 @@ public void onPerformUserCleanup () { _forcePlainModeInChannels = null; _userPreferences = null; _localChatIdsCount = null; + _mainChatListEnabled = null; + _archiveChatListEnabled = null; + _archiveChatListPosition = null; + _disabledChatFolderIds = null; + _chatFolderBadgeFlags = null; + _chatFolderStyle = null; + _chatFolderOptions = null; remoteToLocalChatIds.clear(); localToRemoteChatIds.clear(); @@ -597,49 +616,32 @@ private static String getRegisteredDeviceTdlibVersion2 (int accountId) { @Nullable private static TdApi.DeviceToken getRegisteredDeviceToken (int accountId) { @DeviceTokenType int tokenType = Settings.instance().getInt(key(DEVICE_TOKEN_TYPE_KEY, accountId), DeviceTokenType.FIREBASE_CLOUD_MESSAGING); - switch (tokenType) { - case DeviceTokenType.FIREBASE_CLOUD_MESSAGING: - default: { - String token = Settings.instance().getString(key(DEVICE_TOKEN_KEY, accountId), null); - if (!StringUtils.isEmpty(token)) { - return new TdApi.DeviceTokenFirebaseCloudMessaging(token, true); - } - break; - } - } - return null; + String tokenOrEndpoint = Settings.instance().getString(key(DEVICE_TOKEN_OR_ENDPOINT_KEY, accountId), null); + return Settings.newDeviceToken(tokenType, tokenOrEndpoint); } private static long[] getRegisteredDeviceOtherUserIds (int accountId) { return Settings.instance().pmc().getLongArray(key(DEVICE_OTHER_UID_KEY, accountId)); } - public static void setRegisteredDevice (int accountId, long userId, TdApi.DeviceToken deviceToken, @Nullable long[] otherUserIds) { + public static void setRegisteredDevice (int accountId, long userId, @Nullable TdApi.DeviceToken deviceToken, @Nullable long[] otherUserIds) { if (deviceToken == null) { unregisterDevice(accountId); + return; + } + LevelDB pmc = Settings.instance().edit(); + Settings.storeDeviceToken(deviceToken, pmc, + key(DEVICE_TOKEN_TYPE_KEY, accountId), + key(DEVICE_TOKEN_OR_ENDPOINT_KEY, accountId) + ); + pmc.putLong(key(DEVICE_UID_KEY, accountId), userId); + pmc.putString(key(DEVICE_TDLIB_VERSION2_KEY, accountId), BuildConfig.TDLIB_VERSION); + if (otherUserIds != null && otherUserIds.length > 0) { + pmc.putLongArray(key(DEVICE_OTHER_UID_KEY, accountId), otherUserIds); } else { - int tokenType = TdlibNotificationUtils.getDeviceTokenType(deviceToken); - LevelDB pmc = Settings.instance().edit(); - switch (deviceToken.getConstructor()) { - case TdApi.DeviceTokenFirebaseCloudMessaging.CONSTRUCTOR: { - String token = ((TdApi.DeviceTokenFirebaseCloudMessaging) deviceToken).token; - pmc.putInt(key(DEVICE_TOKEN_TYPE_KEY, accountId), tokenType) - .putString(key(DEVICE_TOKEN_KEY, accountId), token); - break; - } - default: { - throw new UnsupportedOperationException(deviceToken.toString()); - } - } - pmc.putLong(key(DEVICE_UID_KEY, accountId), userId); - pmc.putString(key(DEVICE_TDLIB_VERSION2_KEY, accountId), BuildConfig.TDLIB_VERSION); - if (otherUserIds != null && otherUserIds.length > 0) { - pmc.putLongArray(key(DEVICE_OTHER_UID_KEY, accountId), otherUserIds); - } else { - pmc.remove(key(DEVICE_OTHER_UID_KEY, accountId)); - } - pmc.apply(); + pmc.remove(key(DEVICE_OTHER_UID_KEY, accountId)); } + pmc.apply(); } public static boolean checkRegisteredDeviceToken (int accountId, long userId, TdApi.DeviceToken token, long[] otherUserIds, boolean skipOtherUserIdsCheck) { @@ -652,7 +654,7 @@ public static boolean checkRegisteredDeviceToken (int accountId, long userId, Td public static void unregisterDevice (int accountId) { Settings.instance().edit() - .remove(key(DEVICE_TOKEN_KEY, accountId)) + .remove(key(DEVICE_TOKEN_OR_ENDPOINT_KEY, accountId)) .remove(key(DEVICE_TOKEN_TYPE_KEY, accountId)) .remove(key(DEVICE_UID_KEY, accountId)) .remove(key(DEVICE_OTHER_UID_KEY, accountId)) @@ -1046,4 +1048,270 @@ public String buildNotificationReport () { public boolean hasNotificationProblems () { return getNotificationProblemCount() > 0; } + + // user-specific + private @Nullable Boolean _mainChatListEnabled; + private @Nullable Boolean _archiveChatListEnabled; + private @Nullable Integer _archiveChatListPosition; + private @Nullable IntSet _disabledChatFolderIds; + private static final String MAIN_CHAT_LIST_ENABLED = "main_chat_list_enabled"; + private static final String ARCHIVE_CHAT_LIST_ENABLED = "archive_chat_list_enabled"; + private static final String ARCHIVE_CHAT_LIST_POSITION = "archive_chat_list_position"; + private static final String DISABLED_CHAT_FILTER_IDS = "disabled_chat_filter_ids"; + // may be global + private @Nullable Integer _chatFolderBadgeFlags; + private @Nullable Integer _chatFolderOptions; + private @Nullable Integer _chatFolderStyle; + private static final String CHAT_FOLDER_STYLE = "chat_folder_style"; + private static final String CHAT_FOLDER_BADGE = "chat_folder_badge"; + private static final String CHAT_FOLDER_OPTIONS = "chat_folder_options"; + private static final int DEFAULT_ARCHIVE_CHAT_LIST_POSITION = Integer.MAX_VALUE; + private static final boolean DEFAULT_MAIN_CHAT_LIST_ENABLED = true; + private static final boolean DEFAULT_ARCHIVE_CHAT_LIST_ENABLED = false; + public static final int DEFAULT_CHAT_FOLDER_OPTIONS = ChatFolderOptions.DISPLAY_AT_TOP; + public static final int DEFAULT_CHAT_FOLDER_STYLE = ChatFolderStyle.LABEL_AND_ICON; + private static final int DEFAULT_CHAT_FOLDER_BADGE_FLAGS = 0; + + private boolean isMainChatListEnabled () { + if (_mainChatListEnabled == null) { + _mainChatListEnabled = Settings.instance().getBoolean(key(MAIN_CHAT_LIST_ENABLED, tdlib.accountId()), DEFAULT_MAIN_CHAT_LIST_ENABLED); + } + return _mainChatListEnabled; + } + + private void setMainChatListEnabled (boolean isMainChatListEnabled) { + if (isMainChatListEnabled() != isMainChatListEnabled) { + _mainChatListEnabled = isMainChatListEnabled; + Settings.instance().putBoolean(key(MAIN_CHAT_LIST_ENABLED, tdlib.accountId()), isMainChatListEnabled); + if (chatListPositionListeners != null) { + for (ChatListPositionListener chatListPositionListener : chatListPositionListeners) { + chatListPositionListener.onChatListStateChanged(tdlib, ChatPosition.CHAT_LIST_MAIN, isMainChatListEnabled); + } + } + } + } + + private boolean isArchiveChatListEnabled () { + if (_archiveChatListEnabled == null) { + _archiveChatListEnabled = Settings.instance().getBoolean(key(ARCHIVE_CHAT_LIST_ENABLED, tdlib.accountId()), DEFAULT_ARCHIVE_CHAT_LIST_ENABLED); + } + return _archiveChatListEnabled; + } + + private void setArchiveChatListEnabled (boolean isArchiveChatListEnabled) { + if (isArchiveChatListEnabled() != isArchiveChatListEnabled) { + _archiveChatListEnabled = isArchiveChatListEnabled; + Settings.instance().putBoolean(key(ARCHIVE_CHAT_LIST_ENABLED, tdlib.accountId()), isArchiveChatListEnabled); + if (chatListPositionListeners != null) { + for (ChatListPositionListener chatListPositionListener : chatListPositionListeners) { + chatListPositionListener.onChatListStateChanged(tdlib, ChatPosition.CHAT_LIST_ARCHIVE, isArchiveChatListEnabled); + } + } + } + } + + public int archiveChatListPosition () { + if (_archiveChatListPosition == null) { + _archiveChatListPosition = Settings.instance().getInt(key(ARCHIVE_CHAT_LIST_POSITION, tdlib.accountId()), DEFAULT_ARCHIVE_CHAT_LIST_POSITION); + } + return _archiveChatListPosition; + } + + public void setArchiveChatListPosition (int position) { + if (archiveChatListPosition() != position) { + _archiveChatListPosition = position; + Settings.instance().putInt(key(ARCHIVE_CHAT_LIST_POSITION, tdlib.accountId()), position); + if (chatListPositionListeners != null) { + for (ChatListPositionListener chatListPositionListener : chatListPositionListeners) { + chatListPositionListener.onArchiveChatListPositionChanged(tdlib, position); + } + } + } + } + + public boolean isChatFolderEnabled (int chatFolderId) { + IntSet disabledChatFolderIds = disabledChatFolderIds(); + return !disabledChatFolderIds.has(chatFolderId); + } + + public boolean isChatListEnabled (TdApi.ChatList chatList) { + switch (chatList.getConstructor()) { + case TdApi.ChatListMain.CONSTRUCTOR: { + return isMainChatListEnabled(); + } + case TdApi.ChatListArchive.CONSTRUCTOR: { + return isArchiveChatListEnabled(); + } + case TdApi.ChatListFolder.CONSTRUCTOR: { + int chatFolderId = ((TdApi.ChatListFolder) chatList).chatFolderId; + return isChatFolderEnabled(chatFolderId); + } + default: { + Td.assertChatList_db6c93ab(); + throw Td.unsupported(chatList); + } + } + } + + public void setChatListEnabled (TdApi.ChatList chatList, boolean isEnabled) { + if (isChatListEnabled(chatList) == isEnabled) { + return; + } + switch (chatList.getConstructor()) { + case TdApi.ChatListMain.CONSTRUCTOR: { + if (Config.RESTRICT_HIDING_MAIN_LIST && !isEnabled) { + return; + } + setMainChatListEnabled(isEnabled); + break; + } + case TdApi.ChatListArchive.CONSTRUCTOR: { + setArchiveChatListEnabled(isEnabled); + break; + } + case TdApi.ChatListFolder.CONSTRUCTOR: { + int chatFolderId = ((TdApi.ChatListFolder) chatList).chatFolderId; + IntSet disabledChatFolderIds = disabledChatFolderIds(); + if (isEnabled) { + disabledChatFolderIds.remove(chatFolderId); + } else { + disabledChatFolderIds.add(chatFolderId); + } + if (disabledChatFolderIds.isEmpty()) { + Settings.instance().remove(key(DISABLED_CHAT_FILTER_IDS, tdlib.accountId())); + } else { + Settings.instance().putIntArray(key(DISABLED_CHAT_FILTER_IDS, tdlib.accountId()), disabledChatFolderIds.toArray()); + } + break; + } + default: { + Td.assertChatList_db6c93ab(); + throw Td.unsupported(chatList); + } + } + if (chatListPositionListeners != null) { + for (ChatListPositionListener chatListPositionListener : chatListPositionListeners) { + chatListPositionListener.onChatListStateChanged(tdlib, chatList, isEnabled); + } + } + } + + private IntSet disabledChatFolderIds () { + if (_disabledChatFolderIds == null) { + int[] disabledChatFolderIds = Settings.instance().getIntArray(key(DISABLED_CHAT_FILTER_IDS, tdlib.accountId())); + _disabledChatFolderIds = disabledChatFolderIds != null ? new IntSet(disabledChatFolderIds) : new IntSet(0); + } + return _disabledChatFolderIds; + } + + public interface ChatListPositionListener { + default void onChatListStateChanged (Tdlib tdlib, TdApi.ChatList chatList, boolean isEnabled) { } + default void onArchiveChatListPositionChanged (Tdlib tdlib, int archiveChatListPosition) { } + default void onBadgeFlagsChanged (Tdlib tdlib, int newFlags) { } + default void onChatFolderStyleChanged (Tdlib tdlib, @ChatFolderStyle int newStyle) { } + default void onChatFolderOptionsChanged (Tdlib tdlib, @ChatFolderOptions int newOptions) { } + } + + private @Nullable ReferenceList chatListPositionListeners; + + public void addChatListPositionListener (ChatListPositionListener listener) { + if (chatListPositionListeners == null) { + chatListPositionListeners = new ReferenceList<>(); + } + chatListPositionListeners.add(listener); + } + + public void removeChatListPositionListener (ChatListPositionListener listener) { + if (chatListPositionListeners != null) { + chatListPositionListeners.remove(listener); + } + } + + public int getChatFolderBadgeFlags () { + if (Config.CHAT_FOLDERS_APPEARANCE_IS_GLOBAL) { + return Settings.instance().getBadgeFlags(); + } else { + if (_chatFolderBadgeFlags == null) { + _chatFolderBadgeFlags = Settings.instance().getInt(key(CHAT_FOLDER_BADGE, tdlib.accountId()), DEFAULT_CHAT_FOLDER_BADGE_FLAGS); + } + return _chatFolderBadgeFlags; + } + } + + public void setChatFolderBadgeFlags (int flags) { + if (Config.CHAT_FOLDERS_APPEARANCE_IS_GLOBAL) { + if (Settings.instance().setBadgeFlags(flags)) { + tdlib.context().resetBadge(true); + } + } else { + if (getChatFolderBadgeFlags() == flags) { + return; + } + Settings.instance().putInt(key(CHAT_FOLDER_BADGE, tdlib.accountId()), flags); + _chatFolderBadgeFlags = flags; + if (chatListPositionListeners != null) { + for (ChatListPositionListener chatListPositionListener : chatListPositionListeners) { + chatListPositionListener.onBadgeFlagsChanged(tdlib, flags); + } + } + } + } + + public @ChatFolderStyle int chatFolderStyle () { + if (Config.CHAT_FOLDERS_APPEARANCE_IS_GLOBAL) { + return Settings.instance().getChatFolderStyle(); + } else { + if (_chatFolderStyle == null) { + _chatFolderStyle = Settings.instance().getInt(key(CHAT_FOLDER_STYLE, tdlib.accountId()), DEFAULT_CHAT_FOLDER_STYLE); + } + return _chatFolderStyle; + } + } + + public void setChatFolderStyle (@ChatFolderStyle int chatFolderStyle) { + if (Config.CHAT_FOLDERS_APPEARANCE_IS_GLOBAL) { + Settings.instance().setChatFolderStyle(chatFolderStyle); + } else { + if (chatFolderStyle() != chatFolderStyle) { + _chatFolderStyle = chatFolderStyle; + Settings.instance().putInt(key(CHAT_FOLDER_STYLE, tdlib.accountId()), chatFolderStyle); + if (chatListPositionListeners != null) { + for (ChatListPositionListener chatListPositionListener : chatListPositionListeners) { + chatListPositionListener.onChatFolderStyleChanged(tdlib, chatFolderStyle); + } + } + } + } + } + + public int chatFolderOptions () { + if (Config.CHAT_FOLDERS_APPEARANCE_IS_GLOBAL) { + return Settings.instance().getChatFolderOptions(); + } else { + if (_chatFolderOptions == null) { + _chatFolderOptions = Settings.instance().getInt(key(CHAT_FOLDER_OPTIONS, tdlib.accountId()), DEFAULT_CHAT_FOLDER_OPTIONS); + } + return _chatFolderOptions; + } + } + + public void setChatFolderOptions (@ChatFolderOptions int options) { + if (Config.CHAT_FOLDERS_APPEARANCE_IS_GLOBAL) { + Settings.instance().setChatFolderOptions(options); + } else { + if (chatFolderOptions() != options) { + Settings.instance().putInt(key(CHAT_FOLDER_OPTIONS, tdlib.accountId()), options); + _chatFolderOptions = options; + } + } + } + + public boolean displayFoldersAtTop () { + return BitwiseUtils.hasFlag(chatFolderOptions(), ChatFolderOptions.DISPLAY_AT_TOP); + } + + public void setDisplayFoldersAtTop (boolean displayFoldersAtTop) { + int options = BitwiseUtils.setFlag(chatFolderOptions(), ChatFolderOptions.DISPLAY_AT_TOP, displayFoldersAtTop); + setChatFolderOptions(options); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibUi.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibUi.java index e8129d5d83..6dcb8ad1db 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibUi.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibUi.java @@ -14,6 +14,7 @@ */ package org.thunderdog.challegram.telegram; +import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; @@ -23,20 +24,26 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.SystemClock; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.collection.LongSparseArray; +import androidx.core.os.CancellationSignal; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; @@ -46,9 +53,10 @@ import org.thunderdog.challegram.MainActivity; import org.thunderdog.challegram.R; import org.thunderdog.challegram.U; +import org.thunderdog.challegram.component.MediaCollectorDelegate; +import org.thunderdog.challegram.component.attach.MediaLayout; import org.thunderdog.challegram.component.base.SettingView; import org.thunderdog.challegram.component.chat.MessagesManager; -import org.thunderdog.challegram.component.dialogs.ChatView; import org.thunderdog.challegram.component.popups.ModernActionedLayout; import org.thunderdog.challegram.component.preview.PreviewLayout; import org.thunderdog.challegram.component.sticker.StickerSetWrap; @@ -56,14 +64,16 @@ import org.thunderdog.challegram.core.Background; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.core.LangUtils; +import org.thunderdog.challegram.core.Media; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGBotStart; import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.data.TGSwitchInline; import org.thunderdog.challegram.data.ThreadInfo; +import org.thunderdog.challegram.filegen.PhotoGenerationInfo; import org.thunderdog.challegram.filegen.SimpleGenerationInfo; -import org.thunderdog.challegram.loader.ImageFile; -import org.thunderdog.challegram.loader.ImageFileLocal; +import org.thunderdog.challegram.loader.ImageGalleryFile; +import org.thunderdog.challegram.mediaview.AvatarPickerMode; import org.thunderdog.challegram.mediaview.MediaViewController; import org.thunderdog.challegram.navigation.EditHeaderView; import org.thunderdog.challegram.navigation.HeaderView; @@ -73,6 +83,7 @@ import org.thunderdog.challegram.navigation.SettingsWrapBuilder; import org.thunderdog.challegram.navigation.TooltipOverlayView; import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.support.RippleSupport; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.PropertyId; import org.thunderdog.challegram.theme.Theme; @@ -88,11 +99,14 @@ import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.ui.ChatJoinRequestsController; import org.thunderdog.challegram.ui.ChatLinkMembersController; import org.thunderdog.challegram.ui.ChatLinksController; import org.thunderdog.challegram.ui.ChatsController; +import org.thunderdog.challegram.ui.EditChatFolderController; import org.thunderdog.challegram.ui.EditChatLinkController; +import org.thunderdog.challegram.ui.EditDeleteAccountReasonController; import org.thunderdog.challegram.ui.EditNameController; import org.thunderdog.challegram.ui.EditProxyController; import org.thunderdog.challegram.ui.EditRightsController; @@ -112,6 +126,7 @@ import org.thunderdog.challegram.ui.SettingHolder; import org.thunderdog.challegram.ui.Settings2FAController; import org.thunderdog.challegram.ui.SettingsController; +import org.thunderdog.challegram.ui.SettingsFoldersController; import org.thunderdog.challegram.ui.SettingsLanguageController; import org.thunderdog.challegram.ui.SettingsLogOutController; import org.thunderdog.challegram.ui.SettingsNotificationController; @@ -128,6 +143,7 @@ import org.thunderdog.challegram.util.CustomTypefaceSpan; import org.thunderdog.challegram.util.HapticMenuHelper; import org.thunderdog.challegram.util.OptionDelegate; +import org.thunderdog.challegram.util.Permissions; import org.thunderdog.challegram.util.StringList; import org.thunderdog.challegram.widget.CheckBoxView; import org.thunderdog.challegram.widget.ForceTouchView; @@ -155,11 +171,13 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import me.vkryl.android.widget.FrameLayoutFix; import me.vkryl.core.ArrayUtils; import me.vkryl.core.BitwiseUtils; import me.vkryl.core.ColorUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.collection.IntList; +import me.vkryl.core.collection.LongSet; import me.vkryl.core.lambda.CancellableRunnable; import me.vkryl.core.lambda.Future; import me.vkryl.core.lambda.FutureBool; @@ -167,6 +185,7 @@ import me.vkryl.core.lambda.RunnableData; import me.vkryl.core.lambda.RunnableLong; import me.vkryl.core.unit.ByteUnit; +import me.vkryl.core.util.ConditionalExecutor; import me.vkryl.td.ChatId; import me.vkryl.td.ChatPosition; import me.vkryl.td.MessageId; @@ -772,6 +791,8 @@ boolean shouldSendScreenshotHint (TdApi.Chat chat) { // TTL public static class TTLOption { + public static final int IMMEDIATE = -1; + private final int ttlTime; private final String ttlString; @@ -784,6 +805,14 @@ public int getTtlTime () { return ttlTime; } + public boolean isOff () { + return ttlTime == 0; + } + + public boolean isImmediate () { + return ttlTime == IMMEDIATE; + } + public String getTtlString () { return ttlString; } @@ -813,12 +842,17 @@ private void setTTL (TdApi.Chat chat, int newTtl) { } public void showTTLPicker (final Context context, final TdApi.Chat chat) { - showTTLPicker(context, tdlib.chatTTL(chat.id), false, false, 0, result -> setTTL(chat, result.ttlTime)); + int ttl = tdlib.chatTTL(chat.id); + TdApi.MessageSelfDestructType selfDestructType = ttl != 0 ? new TdApi.MessageSelfDestructTypeTimer(ttl) : null; + showTTLPicker(context, selfDestructType, !ChatId.isSecret(chat.id), false, false, 0, result -> setTTL(chat, result.ttlTime)); } - public static void showTTLPicker (final Context context, int currentTTL, boolean useDarkMode, boolean precise, @StringRes int message, final RunnableData callback) { + public static void showTTLPicker (final Context context, @Nullable TdApi.MessageSelfDestructType currentSelfDestructType, boolean allowInstant, boolean useDarkMode, boolean precise, @StringRes int message, final RunnableData callback) { final ArrayList ttlOptions = new ArrayList<>(21); ttlOptions.add(new TTLOption(0, Lang.getString(R.string.Off))); + if (allowInstant) { + ttlOptions.add(new TTLOption(-1, Lang.getString(R.string.TimerInstant))); + } final int secondsCount = precise ? 20 : 15; for (int i = 1; i <= secondsCount; i++) { ttlOptions.add(new TTLOption(i, Lang.plural(R.string.xSeconds, i))); @@ -839,7 +873,23 @@ public static void showTTLPicker (final Context context, int currentTTL, boolean int i = 0, foundIndex = 0; for (TTLOption option : ttlOptions) { - if (option.ttlTime == currentTTL) { + boolean isValid; + if (currentSelfDestructType == null) { + isValid = option.isOff(); + } else { + switch (currentSelfDestructType.getConstructor()) { + case TdApi.MessageSelfDestructTypeImmediately.CONSTRUCTOR: + isValid = option.isImmediate(); + break; + case TdApi.MessageSelfDestructTypeTimer.CONSTRUCTOR: + isValid = option.ttlTime == ((TdApi.MessageSelfDestructTypeTimer) currentSelfDestructType).selfDestructTime; + break; + default: + Td.assertMessageSelfDestructType_58882d8c(); + throw Td.unsupported(currentSelfDestructType); + } + } + if (isValid) { foundIndex = i; break; } @@ -1267,7 +1317,7 @@ public boolean handleProfileOption (ViewController context, final @IdRes int UI.copyText('@' + Td.primaryUsername(user), R.string.CopiedUsername); return true; } else if (id == R.id.btn_username_copy_link) { - UI.copyText(TD.getLink(user), R.string.CopiedLink); + UI.copyText(context.tdlib().tMeUrl(user.usernames), R.string.CopiedLink); return true; } else if (id == R.id.btn_username_share) { shareUsername(context, user); @@ -1524,97 +1574,7 @@ public Object getShareItem () { context.context().navigation().navigateTo(c); } - // Change photo - - public void showChangePhotoOptions (ViewController context, boolean canDelete) { - if (canDelete) { - context.showOptions(null, new int[] {R.id.btn_changePhotoCamera, R.id.btn_changePhotoGallery, R.id.btn_changePhotoDelete}, new String[] {Lang.getString(R.string.takePhoto), Lang.getString(R.string.pickFromGallery), Lang.getString(R.string.DeletePhoto)}, new int[] {ViewController.OPTION_COLOR_NORMAL, ViewController.OPTION_COLOR_NORMAL, ViewController.OPTION_COLOR_RED}, new int[] {R.drawable.baseline_camera_alt_24, R.drawable.baseline_image_24, R.drawable.baseline_remove_circle_24}); - } else { - context.showOptions(null, new int[] {R.id.btn_changePhotoCamera, R.id.btn_changePhotoGallery}, new String[] {Lang.getString(R.string.takePhoto), Lang.getString(R.string.pickFromGallery)}, null, new int[] {R.drawable.baseline_camera_alt_24, R.drawable.baseline_image_24}); - } - } - - public boolean handlePhotoOption (BaseActivity context, final @IdRes int id, TdApi.User user, EditHeaderView headerView) { - if (user == null && id == R.id.btn_changePhotoDelete && headerView == null) { - return false; - } - if (id == R.id.btn_changePhotoCamera) { - UI.openCameraDelayed(context); - return true; - } else if (id == R.id.btn_changePhotoGallery) { - UI.openGalleryDelayed(context, false); - return true; - } else if (id == R.id.btn_changePhotoDelete) { - if (user != null && user.profilePhoto != null) { - deleteProfilePhoto(user.profilePhoto.id); - } else { - headerView.setPhoto(null); - } - return true; - } - return false; - } - - public void handlePhotoChange (int requestCode, Intent data, EditHeaderView headerView) { - handlePhotoChange(requestCode, data, headerView, headerView == null); - } - - public void handlePhotoChange (int requestCode, Intent data, EditHeaderView headerView, boolean isProfile) { - // TODO show editor - switch (requestCode) { - case Intents.ACTIVITY_RESULT_IMAGE_CAPTURE: { - File image = Intents.takeLastOutputMedia(); - if (image != null) { - U.addToGallery(image); - if (isProfile) { - setProfilePhoto(image.getPath()); - } else { - setEditPhotoCompressed(image.getPath(), headerView); - } - } - break; - } - case Intents.ACTIVITY_RESULT_GALLERY: { - if (data != null) { - final Uri image = data.getData(); - if (image != null) { - String imagePath = U.tryResolveFilePath(image); - if (imagePath != null) { - if (imagePath.endsWith(".webp")) { - UI.showToast("Webp is not supported for profile photos", Toast.LENGTH_LONG); - return; - } - if (isProfile) { - setProfilePhoto(imagePath); - } else { - setEditPhotoCompressed(imagePath, headerView); - } - return; - } - } - } - UI.showToast("Error", Toast.LENGTH_SHORT); - break; - } - } - } - - private static void setEditPhotoCompressed (final String path, final EditHeaderView headerView) { - ImageFile file = new ImageFileLocal(path); - file.setSize(ChatView.getDefaultAvatarCacheSize()); - file.setDecodeSquare(true); - headerView.setPhoto(file); - } - - private void setProfilePhoto (String path) { - UI.showToast(R.string.UploadingPhotoWait, Toast.LENGTH_SHORT); - tdlib.client().send(new TdApi.SetProfilePhoto(new TdApi.InputChatPhotoStatic(new TdApi.InputFileGenerated(path, SimpleGenerationInfo.makeConversion(path), 0)), false), tdlib.profilePhotoHandler()); - } - - private void deleteProfilePhoto (long photoId) { - UI.showToast(R.string.DeletingPhotoWait, Toast.LENGTH_SHORT); - tdlib.client().send(new TdApi.DeleteProfilePhoto(photoId), tdlib.profilePhotoHandler()); - } + // Logs public static void sendTdlibLogs (final ViewController context, final boolean old, final boolean export) { File tdlibLogFile = TdlibManager.getLogFile(old); @@ -1633,8 +1593,6 @@ public static void sendTdlibLogs (final ViewController context, final boolean share.show(); } - // Logs - public static void clearLogs (final boolean old, final RunnableLong after) { Background.instance().post(() -> { try { @@ -1672,21 +1630,11 @@ private Client.ResultHandler newStickerSetHandler (final TdlibDelegate context, switch (object.getConstructor()) { case TdApi.StickerSet.CONSTRUCTOR: { TdApi.StickerSet stickerSet = (TdApi.StickerSet) object; - if (stickerSet.stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR) { - // TODO support custom emoji sets - showLinkTooltip(context.tdlib(), R.drawable.baseline_warning_24, Lang.getString(R.string.InternalUrlUnsupported), openParameters); - return; - } StickerSetWrap.showStickerSet(context, stickerSet); break; } case TdApi.StickerSetInfo.CONSTRUCTOR: { TdApi.StickerSetInfo stickerSetInfo = (TdApi.StickerSetInfo) object; - if (stickerSetInfo.stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR) { - // TODO support custom emoji sets - showLinkTooltip(context.tdlib(), R.drawable.baseline_warning_24, Lang.getString(R.string.InternalUrlUnsupported), openParameters); - return; - } StickerSetWrap.showStickerSet(context, stickerSetInfo); break; } @@ -1708,6 +1656,14 @@ public void showStickerSet (TdlibDelegate context, long setId, @Nullable UrlOpen tdlib.client().send(new TdApi.GetStickerSet(setId), newStickerSetHandler(context, openParameters)); } + public void showStickerSets (TdlibDelegate context, long[] setIds, boolean isEmojiPacks, @Nullable UrlOpenParameters openParameters) { + if (setIds.length == 1) { + showStickerSet(context, setIds[0], openParameters); + } else { + StickerSetWrap.showStickerSets(context, setIds, isEmojiPacks); + } + } + // Confirm phone public void confirmPhone (TdlibDelegate context, TdApi.AuthenticationCodeInfo info, String phoneNumber) { @@ -1788,7 +1744,7 @@ public boolean saveInstanceState (Bundle outState, String keyPrefix) { if (threadInfo != null) threadInfo.saveTo(outState, keyPrefix + "cp_messageThread"); if (filter != null) - TD.saveFilter(outState, keyPrefix + "cp_filter", filter); + Td.put(outState, keyPrefix + "cp_filter", filter); return true; } @@ -1803,7 +1759,7 @@ public boolean saveInstanceState (Bundle outState, String keyPrefix) { if (threadInfo == ThreadInfo.INVALID) return null; params.threadInfo = threadInfo; - params.filter = TD.restoreFilter(in, keyPrefix + "cp_filter"); + params.filter = Td.restoreSearchMessagesFilter(in, keyPrefix + "cp_filter"); return params; } @@ -1978,8 +1934,10 @@ public void openChat (final TdlibDelegate context, final TdApi.MessageSender sen openPrivateChat(context, ((TdApi.MessageSenderChat) senderId).chatId, new TdlibUi.ChatOpenParameters().keepStack()); break; } - default: - throw new UnsupportedOperationException(senderId.toString()); + default: { + Td.assertMessageSender_439d4c9c(); + throw Td.unsupported(senderId); + } } } @@ -2427,29 +2385,24 @@ public void openMessage (final TdlibDelegate context, final TdApi.MessageLinkInf MessageId messageId = new MessageId(messageLink.message.chatId, messageLink.message.id); if (messageLink.messageThreadId != 0) { // FIXME TDLib/Server: need GetMessageThread alternative that accepts (chatId, messageThreadId) - context.tdlib().send(new TdApi.GetMessageThread(messageId.getChatId(), messageId.getMessageId()), (result) -> { - switch (result.getConstructor()) { - case TdApi.MessageThreadInfo.CONSTRUCTOR: - ThreadInfo messageThread = ThreadInfo.openedFromMessage(context.tdlib(), (TdApi.MessageThreadInfo) result, openParameters.messageId); - if (Config.SHOW_CHANNEL_POST_REPLY_INFO_IN_COMMENTS) { - TdApi.Message message = messageThread.getOldestMessage(); - if (message != null && message.replyToMessageId == 0 && message.forwardInfo != null && tdlib.isChannelAutoForward(message)) { - tdlib.send(new TdApi.GetRepliedMessage(message.forwardInfo.fromChatId, message.forwardInfo.fromMessageId), (object) -> { - if (object.getConstructor() == TdApi.Message.CONSTRUCTOR) { - TdApi.Message repliedMessage = (TdApi.Message) object; - message.replyInChatId = repliedMessage.chatId; - message.replyToMessageId = repliedMessage.id; - } - openMessage(context, messageThread.getChatId(), messageId, messageThread, openParameters); - }); - break; - } + context.tdlib().send(new TdApi.GetMessageThread(messageId.getChatId(), messageId.getMessageId()), (messageThreadInfo, error) -> { + if (error != null) { + openMessage(context, messageLink.chatId, messageId, openParameters); + } else { + ThreadInfo messageThread = ThreadInfo.openedFromMessage(context.tdlib(), messageThreadInfo, openParameters.messageId); + if (Config.SHOW_CHANNEL_POST_REPLY_INFO_IN_COMMENTS) { + TdApi.Message message = messageThread.getOldestMessage(); + if (message != null && message.replyTo == null && message.forwardInfo != null && tdlib.isChannelAutoForward(message)) { + tdlib.send(new TdApi.GetRepliedMessage(message.forwardInfo.fromChatId, message.forwardInfo.fromMessageId), (repliedMessage, repliedMessageError) -> { + if (repliedMessage != null) { + message.replyTo = new TdApi.MessageReplyToMessage(repliedMessage.chatId, repliedMessage.id, null, null, repliedMessage.date, repliedMessage.content); + } + openMessage(context, messageThread.getChatId(), messageId, messageThread, openParameters); + }); + return; } - openMessage(context, messageThread.getChatId(), messageId, messageThread, openParameters); - break; - case TdApi.Error.CONSTRUCTOR: - openMessage(context, messageLink.chatId, messageId, openParameters); - break; + } + openMessage(context, messageThread.getChatId(), messageId, messageThread, openParameters); } }); } else { @@ -2488,7 +2441,8 @@ public void openSenderProfile (final TdlibDelegate context, final TdApi.MessageS openChatProfile(context, ((TdApi.MessageSenderChat) senderId).chatId, null, openParameters); break; default: - throw new UnsupportedOperationException(senderId.toString()); + Td.assertMessageSender_439d4c9c(); + throw Td.unsupported(senderId); } } @@ -2574,25 +2528,15 @@ private boolean installLanguage (final TdlibDelegate context, final String langu return true; } // TODO progress - tdlib.client().send(new TdApi.GetLanguagePackInfo(languagePackId), result -> { - switch (result.getConstructor()) { - case TdApi.LanguagePackInfo.CONSTRUCTOR: { - TdApi.LanguagePackInfo info = (TdApi.LanguagePackInfo) result; - tdlib.ui().post(() -> { - if (context.context().getActivityState() != UI.STATE_DESTROYED) { - showLanguageInstallPrompt(context, info); - } - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - showLinkTooltip(context.tdlib(), R.drawable.baseline_warning_24, TD.toErrorString(result), openParameters); - break; - } - default: { - Log.unexpectedTdlibResponse(result, TdApi.GetLanguagePackInfo.class, TdApi.LanguagePackInfo.class, TdApi.Error.class); - break; - } + tdlib.send(new TdApi.GetLanguagePackInfo(languagePackId), (info, error) -> { + if (error != null) { + showLinkTooltip(context.tdlib(), R.drawable.baseline_warning_24, TD.toErrorString(error), openParameters); + } else { + tdlib.ui().post(() -> { + if (context.context().getActivityState() != UI.State.DESTROYED) { + showLanguageInstallPrompt(context, info); + } + }); } }); return true; @@ -2638,6 +2582,7 @@ public UrlOpenParameters (@Nullable UrlOpenParameters options) { this.displayUrl = options.displayUrl; this.parentController = options.parentController; this.originalUrl = options.originalUrl; + this.sourceWebPage = options.sourceWebPage; if (options.sourceMessage != null) { sourceMessage(options.sourceMessage); } @@ -2678,13 +2623,15 @@ public UrlOpenParameters sourceMessage (MessageId messageId) { public UrlOpenParameters sourceMessage (TGMessage message) { if (message != null) { - if (message.isSending()) { + controller(message.controller()); + if (message.isSponsoredMessage()) { + return this; + } else if (message.isSending()) { this.sourceMessage = message; message.getMessageIdChangeListeners().add(this); } else { this.sourceMessage = null; } - controller(message.controller()); return sourceMessage(new MessageId(message.getChatId(), message.getId(), message.getOtherMessageIds(message.getId()))); } else { this.sourceMessage = null; @@ -2773,26 +2720,30 @@ public void openUrlOptions (final ViewController context, final String url, @ }); } - private void openExternalUrl (final TdlibDelegate context, final String originalUrl, @Nullable UrlOpenParameters options) { + private void openExternalUrl (final TdlibDelegate context, final String originalUrl, @Nullable UrlOpenParameters options, @Nullable RunnableBool after) { if (options != null && options.messageId != null && ChatId.isSecret(options.messageId.getChatId())) { - openUrlImpl(context, originalUrl, options); + openUrlImpl(context, originalUrl, options, after); return; } - tdlib.client().send(new TdApi.GetExternalLinkInfo(originalUrl), externalLinkInfoResult -> { - switch (externalLinkInfoResult.getConstructor()) { + tdlib.send(new TdApi.GetExternalLinkInfo(originalUrl), (loginUrlInfo, error) -> { + if (error != null) { + openUrlImpl(context, originalUrl, options, after); + return; + } + switch (loginUrlInfo.getConstructor()) { case TdApi.LoginUrlInfoOpen.CONSTRUCTOR: { - TdApi.LoginUrlInfoOpen open = (TdApi.LoginUrlInfoOpen) externalLinkInfoResult; + TdApi.LoginUrlInfoOpen open = (TdApi.LoginUrlInfoOpen) loginUrlInfo; if (options != null) { if (open.skipConfirmation) { options.disableOpenPrompt(); } options.displayUrl(originalUrl); } - openUrlImpl(context, open.url, options); + openUrlImpl(context, open.url, options, after); break; } case TdApi.LoginUrlInfoRequestConfirmation.CONSTRUCTOR: { - TdApi.LoginUrlInfoRequestConfirmation confirm = (TdApi.LoginUrlInfoRequestConfirmation) externalLinkInfoResult; + TdApi.LoginUrlInfoRequestConfirmation confirm = (TdApi.LoginUrlInfoRequestConfirmation) loginUrlInfo; List items = new ArrayList<>(); items.add(new ListItem(ListItem.TYPE_CHECKBOX_OPTION_MULTILINE, R.id.btn_signIn, 0, @@ -2823,19 +2774,13 @@ private void openExternalUrl (final TdlibDelegate context, final String original boolean needSignIn = items.get(0).isSelected(); boolean needWriteAccess = items.size() > 1 && items.get(1).isSelected(); if (needSignIn) { - context.tdlib().client().send( - new TdApi.GetExternalLink(originalUrl, needWriteAccess), externalLinkResult -> { - switch (externalLinkResult.getConstructor()) { - case TdApi.HttpUrl.CONSTRUCTOR: - openUrlImpl(context, ((TdApi.HttpUrl) externalLinkResult).url, options != null ? options.disableOpenPrompt() : null); - break; - case TdApi.Error.CONSTRUCTOR: - openUrlImpl(context, originalUrl, options != null ? options.disableOpenPrompt() : null); - break; - } + context.tdlib().send( + new TdApi.GetExternalLink(originalUrl, needWriteAccess), (httpUrl, error1) -> { + String destinationUrl = error1 != null ? originalUrl : httpUrl.url; + openUrlImpl(context, destinationUrl, options != null ? options.disableOpenPrompt() : null, after); }); } else { - openUrlImpl(context, originalUrl, options != null ? options.disableOpenPrompt() : null); + openUrlImpl(context, originalUrl, options != null ? options.disableOpenPrompt() : null, after); } }) .setSettingProcessor((item, itemView, isUpdate) -> { @@ -2866,20 +2811,24 @@ private void openExternalUrl (final TdlibDelegate context, final String original .setSaveStr(R.string.Open) .setRawItems(items) ); + } else { + if (after != null) { + after.runWithBool(false); + } } break; } - case TdApi.Error.CONSTRUCTOR: { - openUrlImpl(context, originalUrl, options); - break; + default: { + Td.assertLoginUrlInfo_7af29c11(); + throw Td.unsupported(loginUrlInfo); } } }); } - private void openUrlImpl (final TdlibDelegate context, final String url, @Nullable UrlOpenParameters options) { + private void openUrlImpl (final TdlibDelegate context, final String url, @Nullable UrlOpenParameters options, @Nullable RunnableBool after) { if (!UI.inUiThread()) { - tdlib.ui().post(() -> openUrlImpl(context, url, options)); + tdlib.ui().post(() -> openUrlImpl(context, url, options, after)); return; } @@ -2893,7 +2842,7 @@ private void openUrlImpl (final TdlibDelegate context, final String url, @Nullab b.setMessage(Lang.getString(R.string.OpenThisLink, !StringUtils.isEmpty(options.displayUrl) ? options.displayUrl : url)); b.setPositiveButton(Lang.getString(R.string.Open), (dialog, which) -> tdlib.ui() - .openExternalUrl(context, url, options.disableOpenPrompt()) + .openExternalUrl(context, url, options.disableOpenPrompt(), after) ); b.setNegativeButton(Lang.getString(R.string.Cancel), (dialog, which) -> dialog.dismiss()); c.showAlert(b); @@ -2902,7 +2851,10 @@ private void openUrlImpl (final TdlibDelegate context, final String url, @Nullab } if (uri == null) { - UI.openUrl(url); + boolean result = Intents.openLink(url); + if (after != null) { + after.runWithBool(result); + } return; } @@ -2946,7 +2898,10 @@ private void openUrlImpl (final TdlibDelegate context, final String url, @Nullab final String externalUrl = options == null || StringUtils.isEmpty(options.instantViewFallbackUrl) ? url : options.instantViewFallbackUrl; final Uri uriFinal = uri; if (instantViewMode == INSTANT_VIEW_DISABLED && embedViewMode == EMBED_VIEW_DISABLED) { - UI.openUrl(url); + boolean result = Intents.openLink(url); + if (after != null) { + after.runWithBool(result); + } return; } @@ -2956,6 +2911,9 @@ private void openUrlImpl (final TdlibDelegate context, final String url, @Nullab (webPage != null && PreviewLayout.show((ViewController) context, webPage, isFromSecretChat)) || (webPage == null && PreviewLayout.show((ViewController) context, url, isFromSecretChat)) ) { + if (after != null) { + after.runWithBool(true); + } return; } } @@ -2964,60 +2922,51 @@ private void openUrlImpl (final TdlibDelegate context, final String url, @Nullab final AtomicReference foundWebPage = new AtomicReference<>(); CancellableRunnable[] runnable = new CancellableRunnable[1]; - tdlib.client().send(new TdApi.GetWebPagePreview(new TdApi.FormattedText(url, null)), page -> { - switch (page.getConstructor()) { - case TdApi.WebPage.CONSTRUCTOR: { - TdApi.WebPage webPage = (TdApi.WebPage) page; - foundWebPage.set(webPage); - if (instantViewMode == INSTANT_VIEW_DISABLED || !TD.hasInstantView(webPage.instantViewVersion) || TD.shouldInlineIv(webPage)) { - post(runnable[0]); - return; - } - tdlib.client().send(new TdApi.GetWebPageInstantView(url, false), preview -> { - switch (preview.getConstructor()) { - case TdApi.WebPageInstantView.CONSTRUCTOR: { - TdApi.WebPageInstantView instantView = (TdApi.WebPageInstantView) preview; - if (!TD.hasInstantView(instantView.version)) { - post(runnable[0]); - return; - } - post(() -> { - if (!signal.getAndSet(true)) { - runnable[0].cancel(); - - InstantViewController controller = new InstantViewController(context.context(), context.tdlib()); - try { - controller.setArguments(new InstantViewController.Args(webPage, instantView, Uri.parse(url).getEncodedFragment())); - controller.show(); - } catch (Throwable t) { - Log.e("Unable to open instantView, url:%s", t, url); - UI.showToast(R.string.InstantViewUnsupported, Toast.LENGTH_SHORT); - UI.openUrl(externalUrl); - } - } - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - post(runnable[0]); - break; - } - } - }); - break; + tdlib.send(new TdApi.GetWebPagePreview(new TdApi.FormattedText(url, null), null), (webPage, error) -> { + if (error != null) { + post(runnable[0]); + return; + } + foundWebPage.set(webPage); + if (instantViewMode == INSTANT_VIEW_DISABLED || !TD.hasInstantView(webPage.instantViewVersion) || TD.shouldInlineIv(webPage)) { + post(runnable[0]); + return; + } + tdlib.send(new TdApi.GetWebPageInstantView(url, false), (instantView, error1) -> { + if (error1 != null) { + post(runnable[0]); + return; } - case TdApi.Error.CONSTRUCTOR: { + if (!TD.hasInstantView(instantView.version)) { post(runnable[0]); - break; + return; } - } + post(() -> { + if (!signal.getAndSet(true)) { + runnable[0].cancel(); + + InstantViewController controller = new InstantViewController(context.context(), context.tdlib()); + try { + controller.setArguments(new InstantViewController.Args(webPage, instantView, Uri.parse(url).getEncodedFragment())); + controller.show(); + if (after != null) { + after.runWithBool(true); + } + } catch (Throwable t) { + Log.e("Unable to open instantView, url:%s", t, url); + UI.showToast(R.string.InstantViewUnsupported, Toast.LENGTH_SHORT); + UI.openUrl(externalUrl); + } + } + }); + }); }); runnable[0] = new CancellableRunnable() { @Override public void act () { if (!signal.getAndSet(true)) { if (options != null && !StringUtils.isEmpty(options.instantViewFallbackUrl) && !options.instantViewFallbackUrl.equals(url) && !options.instantViewFallbackUrl.equals(options.originalUrl)) { - openUrl(context, options.instantViewFallbackUrl, new UrlOpenParameters(options).instantViewMode(INSTANT_VIEW_UNSPECIFIED)); + openUrl(context, options.instantViewFallbackUrl, new UrlOpenParameters(options).instantViewMode(INSTANT_VIEW_UNSPECIFIED), after); return; } if (tdlib.isKnownHost(uriFinal.getHost(), false)) { @@ -3025,7 +2974,7 @@ public void act () { if (segments != null && segments.size() == 1 && "iv".equals(segments.get(0))) { String originalUrl = uriFinal.getQueryParameter("url"); if (Strings.isValidLink(originalUrl)) { - openUrl(context, originalUrl, new UrlOpenParameters(options).disableInstantView()); + openUrl(context, originalUrl, new UrlOpenParameters(options).disableInstantView(), after); return; } } @@ -3033,13 +2982,19 @@ public void act () { if (embedViewMode == EMBED_VIEW_ENABLED) { TdApi.WebPage webPage = foundWebPage.get(); if (context instanceof ViewController && webPage != null && PreviewLayout.show((ViewController) context, webPage, isFromSecretChat)) { + if (after != null) { + after.runWithBool(true); + } return; } } if (!externalUrl.equals(url) && !(options != null && externalUrl.equals(options.originalUrl))) { - openUrl(context, externalUrl, new UrlOpenParameters(options).instantViewMode(INSTANT_VIEW_UNSPECIFIED)); + openUrl(context, externalUrl, new UrlOpenParameters(options).instantViewMode(INSTANT_VIEW_UNSPECIFIED), after); } else { - UI.openUrl(externalUrl); + boolean result = Intents.openLink(externalUrl); + if (after != null) { + after.runWithBool(result); + } } } } @@ -3049,9 +3004,17 @@ public void act () { } public void openUrl (final TdlibDelegate context, final String url, @Nullable UrlOpenParameters options) { + openUrl(context, url, options, null); + } + + public void openUrl (final TdlibDelegate context, final String url, @Nullable UrlOpenParameters options, @Nullable RunnableBool after) { openTelegramUrl(context, url, options, processed -> { if (!processed) { - openExternalUrl(context, url, options); + openExternalUrl(context, url, options, after); + } else { + if (after != null) { + after.runWithBool(true); + } } }); } @@ -3359,22 +3322,22 @@ public void openTelegramUrl (final TdlibDelegate context, final String rawUrl, @ return; } AtomicReference url = new AtomicReference<>(preProcessTelegramUrl(rawUrl)); - tdlib.client().send(new TdApi.GetInternalLinkType(url.get()), new Client.ResultHandler() { + tdlib.send(new TdApi.GetInternalLinkType(url.get()), new Tdlib.ResultHandler<>() { @Override - public void onResult (TdApi.Object result) { + public void onResult (TdApi.InternalLinkType internalLinkType, @Nullable TdApi.Error error) { final String currentUrl = url.get(); TdApi.InternalLinkType linkType; - if (result instanceof TdApi.InternalLinkTypeUnknownDeepLink) { + if (error != null) { + linkType = parseTelegramUrl(rawUrl); + } else if (internalLinkType instanceof TdApi.InternalLinkTypeUnknownDeepLink) { TdApi.InternalLinkType parsedType = parseTelegramUrl(rawUrl); - linkType = parsedType != null ? parsedType : (TdApi.InternalLinkType) result; - } else if (result instanceof TdApi.InternalLinkType) { - linkType = (TdApi.InternalLinkType) result; + linkType = parsedType != null ? parsedType : internalLinkType; } else { - linkType = parseTelegramUrl(rawUrl); + linkType = internalLinkType; } - if ((linkType == null || result instanceof TdApi.InternalLinkTypeUnknownDeepLink) && !url.get().equals(rawUrl)) { + if ((linkType == null || internalLinkType instanceof TdApi.InternalLinkTypeUnknownDeepLink) && !url.get().equals(rawUrl)) { url.set(rawUrl); - tdlib.client().send(new TdApi.GetInternalLinkType(url.get()), this); + tdlib.send(new TdApi.GetInternalLinkType(url.get()), this); return; } if (linkType == null) { @@ -3383,279 +3346,296 @@ public void onResult (TdApi.Object result) { } return; } - post(() -> { - if (context.context().navigation().isDestroyed()) - return; - boolean ok = true; - switch (linkType.getConstructor()) { - case TdApi.InternalLinkTypeStickerSet.CONSTRUCTOR: { - TdApi.InternalLinkTypeStickerSet stickerSet = (TdApi.InternalLinkTypeStickerSet) linkType; - showStickerSet(context, stickerSet.stickerSetName, openParameters); - break; - } - case TdApi.InternalLinkTypeAuthenticationCode.CONSTRUCTOR: { - TdApi.InternalLinkTypeAuthenticationCode authCode = (TdApi.InternalLinkTypeAuthenticationCode) linkType; - tdlib.listeners().updateAuthorizationCodeReceived(authCode.code); - break; - } - case TdApi.InternalLinkTypeLanguagePack.CONSTRUCTOR: { - TdApi.InternalLinkTypeLanguagePack languagePack = (TdApi.InternalLinkTypeLanguagePack) linkType; - ok = installLanguage(context, languagePack.languagePackId, openParameters); - break; - } - case TdApi.InternalLinkTypeChatInvite.CONSTRUCTOR: { - TdApi.InternalLinkTypeChatInvite chatInvite = (TdApi.InternalLinkTypeChatInvite) linkType; - checkInviteLink(context, chatInvite.inviteLink, openParameters); - break; - } - case TdApi.InternalLinkTypeMessageDraft.CONSTRUCTOR: { - TdApi.InternalLinkTypeMessageDraft messageDraft = (TdApi.InternalLinkTypeMessageDraft) linkType; - ShareController c = new ShareController(context.context(), context.tdlib()); - c.setArguments(new ShareController.Args(messageDraft.text)); - c.show(); - break; - } - case TdApi.InternalLinkTypePhoneNumberConfirmation.CONSTRUCTOR: { - TdApi.InternalLinkTypePhoneNumberConfirmation confirmPhone = (TdApi.InternalLinkTypePhoneNumberConfirmation) linkType; - TdApi.PhoneNumberAuthenticationSettings authenticationSettings = context.tdlib().phoneNumberAuthenticationSettings(context.context()); - // TODO progress? - ViewController currentController = context.context().navigation().getCurrentStackItem(); - tdlib.client().send(new TdApi.SendPhoneNumberConfirmationCode(confirmPhone.hash, confirmPhone.phoneNumber, authenticationSettings), confirmationResult -> { - switch (confirmationResult.getConstructor()) { - case TdApi.AuthenticationCodeInfo.CONSTRUCTOR: { - TdApi.AuthenticationCodeInfo info = (TdApi.AuthenticationCodeInfo) confirmationResult; - post(() -> { - if (currentController != null && !currentController.isDestroyed()) { - confirmPhone(context, info, confirmPhone.phoneNumber); - } - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - showLinkTooltip(tdlib, R.drawable.baseline_warning_24, TD.toErrorString(confirmationResult), openParameters); - break; - } + post(() -> + openInternalLinkType(context, currentUrl, linkType, openParameters, after) + ); + } + }); + } + + public void openInternalLinkType (TdlibDelegate context, @Nullable String originalUrl, @NonNull TdApi.InternalLinkType linkType, @Nullable UrlOpenParameters openParameters, @Nullable RunnableBool after) { + if (!UI.inUiThread()) { + post(() -> + openInternalLinkType(context, originalUrl, linkType, openParameters, after) + ); + return; + } + if (context.context().navigation().isDestroyed()) { + if (after != null) { + after.runWithBool(false); + } + return; + } + boolean ok = true; + switch (linkType.getConstructor()) { + case TdApi.InternalLinkTypeStickerSet.CONSTRUCTOR: { + TdApi.InternalLinkTypeStickerSet stickerSet = (TdApi.InternalLinkTypeStickerSet) linkType; + showStickerSet(context, stickerSet.stickerSetName, openParameters); + break; + } + case TdApi.InternalLinkTypeAuthenticationCode.CONSTRUCTOR: { + TdApi.InternalLinkTypeAuthenticationCode authCode = (TdApi.InternalLinkTypeAuthenticationCode) linkType; + tdlib.listeners().updateAuthorizationCodeReceived(authCode.code); + break; + } + case TdApi.InternalLinkTypeLanguagePack.CONSTRUCTOR: { + TdApi.InternalLinkTypeLanguagePack languagePack = (TdApi.InternalLinkTypeLanguagePack) linkType; + ok = installLanguage(context, languagePack.languagePackId, openParameters); + break; + } + case TdApi.InternalLinkTypeChatInvite.CONSTRUCTOR: { + TdApi.InternalLinkTypeChatInvite chatInvite = (TdApi.InternalLinkTypeChatInvite) linkType; + checkInviteLink(context, chatInvite.inviteLink, openParameters); + break; + } + case TdApi.InternalLinkTypeMessageDraft.CONSTRUCTOR: { + TdApi.InternalLinkTypeMessageDraft messageDraft = (TdApi.InternalLinkTypeMessageDraft) linkType; + ShareController c = new ShareController(context.context(), context.tdlib()); + c.setArguments(new ShareController.Args(messageDraft.text)); + c.show(); + break; + } + case TdApi.InternalLinkTypePhoneNumberConfirmation.CONSTRUCTOR: { + TdApi.InternalLinkTypePhoneNumberConfirmation confirmPhone = (TdApi.InternalLinkTypePhoneNumberConfirmation) linkType; + TdApi.PhoneNumberAuthenticationSettings authenticationSettings = context.tdlib().phoneNumberAuthenticationSettings(context.context()); + // TODO progress? + ViewController currentController = context.context().navigation().getCurrentStackItem(); + tdlib.client().send(new TdApi.SendPhoneNumberConfirmationCode(confirmPhone.hash, confirmPhone.phoneNumber, authenticationSettings), confirmationResult -> { + switch (confirmationResult.getConstructor()) { + case TdApi.AuthenticationCodeInfo.CONSTRUCTOR: { + TdApi.AuthenticationCodeInfo info = (TdApi.AuthenticationCodeInfo) confirmationResult; + post(() -> { + if (currentController != null && !currentController.isDestroyed()) { + confirmPhone(context, info, confirmPhone.phoneNumber); } }); break; } - case TdApi.InternalLinkTypeProxy.CONSTRUCTOR: { - TdApi.InternalLinkTypeProxy proxy = (TdApi.InternalLinkTypeProxy) linkType; - openProxyAlert(context, proxy.server, proxy.port, proxy.type, newProxyDescription(proxy.server, Integer.toString(proxy.port)).toString()); - break; - } - case TdApi.InternalLinkTypeUnsupportedProxy.CONSTRUCTOR: { - showLinkTooltip(tdlib, R.drawable.baseline_warning_24, Lang.getString(R.string.ProxyLinkUnsupported), openParameters); - break; - } - case TdApi.InternalLinkTypeUserPhoneNumber.CONSTRUCTOR: { - final String phoneNumber = ((TdApi.InternalLinkTypeUserPhoneNumber) linkType).phoneNumber; - openChatProfile(context, 0, null, new TdApi.SearchUserByPhoneNumber(phoneNumber), openParameters); + case TdApi.Error.CONSTRUCTOR: { + showLinkTooltip(tdlib, R.drawable.baseline_warning_24, TD.toErrorString(confirmationResult), openParameters); break; } + } + }); + break; + } + case TdApi.InternalLinkTypeProxy.CONSTRUCTOR: { + TdApi.InternalLinkTypeProxy proxy = (TdApi.InternalLinkTypeProxy) linkType; + openProxyAlert(context, proxy.server, proxy.port, proxy.type, newProxyDescription(proxy.server, Integer.toString(proxy.port)).toString()); + break; + } + case TdApi.InternalLinkTypeUnsupportedProxy.CONSTRUCTOR: { + showLinkTooltip(tdlib, R.drawable.baseline_warning_24, Lang.getString(R.string.ProxyLinkUnsupported), openParameters); + break; + } + case TdApi.InternalLinkTypeUserPhoneNumber.CONSTRUCTOR: { + final String phoneNumber = ((TdApi.InternalLinkTypeUserPhoneNumber) linkType).phoneNumber; + openChatProfile(context, 0, null, new TdApi.SearchUserByPhoneNumber(phoneNumber), openParameters); + break; + } - case TdApi.InternalLinkTypeUserToken.CONSTRUCTOR: { - final String token = ((TdApi.InternalLinkTypeUserToken) linkType).token; - openChatProfile(context, 0, null, new TdApi.SearchUserByToken(token), openParameters); - break; - } - case TdApi.InternalLinkTypeVideoChat.CONSTRUCTOR: { - TdApi.InternalLinkTypeVideoChat voiceChatInvitation = (TdApi.InternalLinkTypeVideoChat) linkType; - openVideoChatOrLiveStream(context, voiceChatInvitation, openParameters); - break; - } - case TdApi.InternalLinkTypeMessage.CONSTRUCTOR: { - TdApi.InternalLinkTypeMessage messageLink = (TdApi.InternalLinkTypeMessage) linkType; - // TODO show progress? - tdlib.client().send(new TdApi.GetMessageLinkInfo(messageLink.url), messageLinkResult -> { - switch (messageLinkResult.getConstructor()) { - case TdApi.MessageLinkInfo.CONSTRUCTOR: { - TdApi.MessageLinkInfo messageLinkInfo = (TdApi.MessageLinkInfo) messageLinkResult; - post(() -> { - openMessage(context, messageLinkInfo, openParameters); - if (after != null) { - after.runWithBool(true); - } - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - if (after != null) { - post(() -> after.runWithBool(false)); - } - break; - } + case TdApi.InternalLinkTypeUserToken.CONSTRUCTOR: { + final String token = ((TdApi.InternalLinkTypeUserToken) linkType).token; + openChatProfile(context, 0, null, new TdApi.SearchUserByToken(token), openParameters); + break; + } + case TdApi.InternalLinkTypeVideoChat.CONSTRUCTOR: { + TdApi.InternalLinkTypeVideoChat voiceChatInvitation = (TdApi.InternalLinkTypeVideoChat) linkType; + openVideoChatOrLiveStream(context, voiceChatInvitation, openParameters); + break; + } + case TdApi.InternalLinkTypeMessage.CONSTRUCTOR: { + TdApi.InternalLinkTypeMessage messageLink = (TdApi.InternalLinkTypeMessage) linkType; + // TODO show progress? + tdlib.client().send(new TdApi.GetMessageLinkInfo(messageLink.url), messageLinkResult -> { + switch (messageLinkResult.getConstructor()) { + case TdApi.MessageLinkInfo.CONSTRUCTOR: { + TdApi.MessageLinkInfo messageLinkInfo = (TdApi.MessageLinkInfo) messageLinkResult; + post(() -> { + openMessage(context, messageLinkInfo, openParameters); + if (after != null) { + after.runWithBool(true); } }); - return; // async - } - case TdApi.InternalLinkTypeBotStart.CONSTRUCTOR: { - TdApi.InternalLinkTypeBotStart startBot = (TdApi.InternalLinkTypeBotStart) linkType; - startBot(context, startBot.botUsername, startBot.startParameter, BOT_MODE_START, openParameters); - break; - } - case TdApi.InternalLinkTypeBotStartInGroup.CONSTRUCTOR: { - TdApi.InternalLinkTypeBotStartInGroup startBot = (TdApi.InternalLinkTypeBotStartInGroup) linkType; - startBot(context, startBot.botUsername, startBot.startParameter, BOT_MODE_START_IN_GROUP, openParameters); - break; - } - case TdApi.InternalLinkTypeBotAddToChannel.CONSTRUCTOR: { - TdApi.InternalLinkTypeBotAddToChannel addToChannel = (TdApi.InternalLinkTypeBotAddToChannel) linkType; - // TODO add to channel flow - showLinkTooltip(tdlib, R.drawable.baseline_warning_24, Lang.getString(R.string.InternalUrlUnsupported), openParameters); - break; - } - case TdApi.InternalLinkTypeGame.CONSTRUCTOR: { - TdApi.InternalLinkTypeGame game = (TdApi.InternalLinkTypeGame) linkType; - startBot(context, game.botUsername, game.gameShortName, BOT_MODE_START_GAME, openParameters); - break; - } - case TdApi.InternalLinkTypeSettings.CONSTRUCTOR: { - SettingsController c = new SettingsController(context.context(), context.tdlib()); - context.context().navigation().navigateTo(c); - break; - } - case TdApi.InternalLinkTypeLanguageSettings.CONSTRUCTOR: { - SettingsLanguageController c = new SettingsLanguageController(context.context(), context.tdlib()); - context.context().navigation().navigateTo(c); - break; - } - case TdApi.InternalLinkTypePrivacyAndSecuritySettings.CONSTRUCTOR: { - SettingsPrivacyController c = new SettingsPrivacyController(context.context(), context.tdlib()); - context.context().navigation().navigateTo(c); break; } - case TdApi.InternalLinkTypeThemeSettings.CONSTRUCTOR: { - SettingsThemeController c = new SettingsThemeController(context.context(), context.tdlib()); - c.setArguments(new SettingsThemeController.Args(SettingsThemeController.MODE_THEMES)); - context.context().navigation().navigateTo(c); - break; - } - case TdApi.InternalLinkTypePublicChat.CONSTRUCTOR: { - TdApi.InternalLinkTypePublicChat publicChat = (TdApi.InternalLinkTypePublicChat) linkType; - if (TdConstants.IV_PREVIEW_USERNAME.equals(publicChat.chatUsername)) { - openExternalUrl(context, currentUrl, new UrlOpenParameters(openParameters).forceInstantView()); - } else { - openPublicChat(context, publicChat.chatUsername, openParameters); + case TdApi.Error.CONSTRUCTOR: { + if (after != null) { + post(() -> after.runWithBool(false)); } break; } - case TdApi.InternalLinkTypeInstantView.CONSTRUCTOR: { - TdApi.InternalLinkTypeInstantView instantView = (TdApi.InternalLinkTypeInstantView) linkType; - openExternalUrl(context, instantView.url, new UrlOpenParameters(openParameters) - .forceInstantView() - .originalUrl(currentUrl) - .instantViewFallbackUrl(instantView.fallbackUrl) - ); - break; - } - case TdApi.InternalLinkTypeActiveSessions.CONSTRUCTOR: { - SettingsSessionsController sessions = new SettingsSessionsController(context.context(), context.tdlib()); - SettingsWebsitesController websites = new SettingsWebsitesController(context.context(), context.tdlib()); - ViewController c = new SimpleViewPagerController(context.context(), context.tdlib(), new ViewController[] {sessions, websites}, new String[] {Lang.getString(R.string.Devices).toUpperCase(), Lang.getString(R.string.Websites).toUpperCase()}, false); - context.context().navigation().navigateTo(c); - break; - } - - case TdApi.InternalLinkTypeAttachmentMenuBot.CONSTRUCTOR: - case TdApi.InternalLinkTypeInvoice.CONSTRUCTOR: - case TdApi.InternalLinkTypePremiumFeatures.CONSTRUCTOR: - case TdApi.InternalLinkTypeRestorePurchases.CONSTRUCTOR: { - showLinkTooltip(tdlib, R.drawable.baseline_warning_24, Lang.getString(R.string.InternalUrlUnsupported), openParameters); - break; - } - case TdApi.InternalLinkTypeChangePhoneNumber.CONSTRUCTOR: { - SettingsPhoneController c = new SettingsPhoneController(context.context(), context.tdlib()); - context.context().navigation().navigateTo(c); - break; - } - case TdApi.InternalLinkTypeQrCodeAuthentication.CONSTRUCTOR: { - showLinkTooltip(tdlib, R.drawable.baseline_warning_24, Lang.getString(R.string.ScanQRLinkHint), openParameters); - break; - } - case TdApi.InternalLinkTypeTheme.CONSTRUCTOR: { - TdApi.InternalLinkTypeTheme theme = (TdApi.InternalLinkTypeTheme) linkType; - // TODO tdlib - showLinkTooltip(tdlib, R.drawable.baseline_info_24, Lang.getMarkdownString(context, R.string.NoCloudThemeSupport), openParameters); - break; - } - case TdApi.InternalLinkTypeBackground.CONSTRUCTOR: { - TdApi.InternalLinkTypeBackground background = (TdApi.InternalLinkTypeBackground) linkType; - // TODO show progress? - tdlib.client().send(new TdApi.SearchBackground(background.backgroundName), backgroundObj -> { - switch (backgroundObj.getConstructor()) { - case TdApi.Background.CONSTRUCTOR: { - TdApi.Background wallpaper = (TdApi.Background) backgroundObj; - - post(() -> { - MessagesController c = new MessagesController(context.context(), context.tdlib()); - c.setArguments(new MessagesController.Arguments(MessagesController.PREVIEW_MODE_WALLPAPER_OBJECT, null, null).setWallpaperObject(wallpaper)); - context.context().navigation().navigateTo(c); - - if (after != null) { - after.runWithBool(true); - } - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - if (after != null) { - post(() -> after.runWithBool(false)); - } - break; - } - } - }); - return; - } - case TdApi.InternalLinkTypeChatFolderSettings.CONSTRUCTOR: { - // TODO show chat folders screen - ok = false; - break; - } - case TdApi.InternalLinkTypePassportDataRequest.CONSTRUCTOR: { - TdApi.InternalLinkTypePassportDataRequest passportData = (TdApi.InternalLinkTypePassportDataRequest) linkType; - // TODO ? - break; - } - case TdApi.InternalLinkTypeUnknownDeepLink.CONSTRUCTOR: { - // TODO progress - tdlib.client().send(new TdApi.GetDeepLinkInfo(currentUrl), deepLinkResult -> { - switch (deepLinkResult.getConstructor()) { - case TdApi.DeepLinkInfo.CONSTRUCTOR: { - TdApi.DeepLinkInfo deepLink = (TdApi.DeepLinkInfo) deepLinkResult; - post(() -> { - ViewController c = context.context().navigation().getCurrentStackItem(); - if (c != null) { - c.processDeepLinkInfo(deepLink); - } - if (after != null) { - after.runWithBool(true); - } - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - if (after != null) { - post(() -> after.runWithBool(false)); - } - break; - } - } - }); - return; // async - } - default: { - ok = false; - break; + } + }); + return; // async + } + case TdApi.InternalLinkTypeBotStart.CONSTRUCTOR: { + TdApi.InternalLinkTypeBotStart startBot = (TdApi.InternalLinkTypeBotStart) linkType; + startBot(context, startBot.botUsername, startBot.startParameter, BOT_MODE_START, openParameters); + break; + } + case TdApi.InternalLinkTypeBotStartInGroup.CONSTRUCTOR: { + TdApi.InternalLinkTypeBotStartInGroup startBot = (TdApi.InternalLinkTypeBotStartInGroup) linkType; + startBot(context, startBot.botUsername, startBot.startParameter, BOT_MODE_START_IN_GROUP, openParameters); + break; + } + case TdApi.InternalLinkTypeBotAddToChannel.CONSTRUCTOR: { + TdApi.InternalLinkTypeBotAddToChannel addToChannel = (TdApi.InternalLinkTypeBotAddToChannel) linkType; + // TODO add to channel flow + showLinkTooltip(tdlib, R.drawable.baseline_warning_24, Lang.getString(R.string.InternalUrlUnsupported), openParameters); + break; + } + case TdApi.InternalLinkTypeGame.CONSTRUCTOR: { + TdApi.InternalLinkTypeGame game = (TdApi.InternalLinkTypeGame) linkType; + startBot(context, game.botUsername, game.gameShortName, BOT_MODE_START_GAME, openParameters); + break; + } + case TdApi.InternalLinkTypeSettings.CONSTRUCTOR: + case TdApi.InternalLinkTypeEditProfileSettings.CONSTRUCTOR: { + SettingsController c = new SettingsController(context.context(), context.tdlib()); + context.context().navigation().navigateTo(c); + break; + } + case TdApi.InternalLinkTypeLanguageSettings.CONSTRUCTOR: { + SettingsLanguageController c = new SettingsLanguageController(context.context(), context.tdlib()); + context.context().navigation().navigateTo(c); + break; + } + case TdApi.InternalLinkTypePrivacyAndSecuritySettings.CONSTRUCTOR: { + SettingsPrivacyController c = new SettingsPrivacyController(context.context(), context.tdlib()); + context.context().navigation().navigateTo(c); + break; + } + case TdApi.InternalLinkTypeThemeSettings.CONSTRUCTOR: { + SettingsThemeController c = new SettingsThemeController(context.context(), context.tdlib()); + c.setArguments(new SettingsThemeController.Args(SettingsThemeController.MODE_THEMES)); + context.context().navigation().navigateTo(c); + break; + } + case TdApi.InternalLinkTypePublicChat.CONSTRUCTOR: { + TdApi.InternalLinkTypePublicChat publicChat = (TdApi.InternalLinkTypePublicChat) linkType; + if (TdConstants.IV_PREVIEW_USERNAME.equals(publicChat.chatUsername) & !StringUtils.isEmpty(originalUrl)) { + openExternalUrl(context, originalUrl, new UrlOpenParameters(openParameters).forceInstantView(), after); + } else { + openPublicChat(context, publicChat.chatUsername, openParameters); + } + break; + } + case TdApi.InternalLinkTypeInstantView.CONSTRUCTOR: { + TdApi.InternalLinkTypeInstantView instantView = (TdApi.InternalLinkTypeInstantView) linkType; + UrlOpenParameters instantViewOpenParameters = new UrlOpenParameters(openParameters) + .forceInstantView() + .instantViewFallbackUrl(instantView.fallbackUrl); + if (!StringUtils.isEmpty(originalUrl)) { + instantViewOpenParameters.originalUrl(originalUrl); + } + openExternalUrl(context, instantView.url, instantViewOpenParameters, after); + break; + } + case TdApi.InternalLinkTypeActiveSessions.CONSTRUCTOR: { + SettingsSessionsController sessions = new SettingsSessionsController(context.context(), context.tdlib()); + SettingsWebsitesController websites = new SettingsWebsitesController(context.context(), context.tdlib()); + ViewController c = new SimpleViewPagerController(context.context(), context.tdlib(), new ViewController[] {sessions, websites}, new String[] {Lang.getString(R.string.Devices).toUpperCase(), Lang.getString(R.string.Websites).toUpperCase()}, false); + context.context().navigation().navigateTo(c); + break; + } + + case TdApi.InternalLinkTypeStory.CONSTRUCTOR: + case TdApi.InternalLinkTypeChatFolderInvite.CONSTRUCTOR: + case TdApi.InternalLinkTypeDefaultMessageAutoDeleteTimerSettings.CONSTRUCTOR: + + case TdApi.InternalLinkTypeAttachmentMenuBot.CONSTRUCTOR: + case TdApi.InternalLinkTypeWebApp.CONSTRUCTOR: + case TdApi.InternalLinkTypeSideMenuBot.CONSTRUCTOR: + + case TdApi.InternalLinkTypeInvoice.CONSTRUCTOR: + + case TdApi.InternalLinkTypePremiumFeatures.CONSTRUCTOR: + case TdApi.InternalLinkTypeRestorePurchases.CONSTRUCTOR: + case TdApi.InternalLinkTypeChatBoost.CONSTRUCTOR: + case TdApi.InternalLinkTypePremiumGiftCode.CONSTRUCTOR: + case TdApi.InternalLinkTypePremiumGift.CONSTRUCTOR: + + case TdApi.InternalLinkTypePassportDataRequest.CONSTRUCTOR: { + showLinkTooltip(tdlib, R.drawable.baseline_warning_24, Lang.getString(R.string.InternalUrlUnsupported), openParameters); + break; + } + case TdApi.InternalLinkTypeChangePhoneNumber.CONSTRUCTOR: { + SettingsPhoneController c = new SettingsPhoneController(context.context(), context.tdlib()); + context.context().navigation().navigateTo(c); + break; + } + case TdApi.InternalLinkTypeQrCodeAuthentication.CONSTRUCTOR: { + showLinkTooltip(tdlib, R.drawable.baseline_warning_24, Lang.getString(R.string.ScanQRLinkHint), openParameters); + break; + } + case TdApi.InternalLinkTypeTheme.CONSTRUCTOR: { + TdApi.InternalLinkTypeTheme theme = (TdApi.InternalLinkTypeTheme) linkType; + // TODO tdlib + showLinkTooltip(tdlib, R.drawable.baseline_info_24, Lang.getMarkdownString(context, R.string.NoCloudThemeSupport), openParameters); + break; + } + case TdApi.InternalLinkTypeBackground.CONSTRUCTOR: { + TdApi.InternalLinkTypeBackground background = (TdApi.InternalLinkTypeBackground) linkType; + // TODO show progress? + tdlib.send(new TdApi.SearchBackground(background.backgroundName), (wallpaper, error) -> { + if (error != null) { + if (after != null) { + post(() -> after.runWithBool(false)); } + } else { + post(() -> { + MessagesController c = new MessagesController(context.context(), context.tdlib()); + c.setArguments(new MessagesController.Arguments(MessagesController.PREVIEW_MODE_WALLPAPER_OBJECT, null, null).setWallpaperObject(wallpaper)); + context.context().navigation().navigateTo(c); + + if (after != null) { + after.runWithBool(true); + } + }); } - if (after != null) { - after.runWithBool(ok); + }); + return; + } + case TdApi.InternalLinkTypeChatFolderSettings.CONSTRUCTOR: { + if (Settings.instance().chatFoldersEnabled()) { + SettingsFoldersController chatFolders = new SettingsFoldersController(context.context(), context.tdlib()); + context.context().navigation().navigateTo(chatFolders); + } else { + showLinkTooltip(tdlib, R.drawable.baseline_warning_24, Lang.getString(R.string.InternalUrlUnsupported), openParameters); + } + break; + } + case TdApi.InternalLinkTypeUnknownDeepLink.CONSTRUCTOR: { + // TODO progress + TdApi.InternalLinkTypeUnknownDeepLink unknownDeepLink = (TdApi.InternalLinkTypeUnknownDeepLink) linkType; + tdlib.send(new TdApi.GetDeepLinkInfo(unknownDeepLink.link), (deepLink, error) -> { + if (error != null) { + if (after != null) { + post(() -> after.runWithBool(false)); + } + } else { + post(() -> { + ViewController c = context.context().navigation().getCurrentStackItem(); + if (c != null) { + c.processDeepLinkInfo(deepLink); + } + if (after != null) { + after.runWithBool(true); + } + }); } }); + return; // async } - }); + default: { + Td.assertInternalLinkType_18c73626(); + throw Td.unsupported(linkType); + } + } + if (after != null) { + after.runWithBool(ok); + } } public static StringBuilder newProxyDescription (String server, String port) { @@ -3752,6 +3732,69 @@ public void openProxyAlert (TdlibDelegate context, String server, int port, TdAp }); } + // Delete account on server + + public void permanentlyDeleteAccount (ViewController context, boolean showAlternatives) { + boolean needShowAlternatives = tdlib.isAuthorized() && showAlternatives; + context.showOptions( + Lang.getMarkdownString(context, needShowAlternatives ? R.string.DeleteAccountConfirmFirst : R.string.DeleteAccountConfirm), + new int[] {R.id.btn_deleteAccount, R.id.btn_cancel}, + new String[] {Lang.getString(needShowAlternatives ? R.string.DeleteAccountConfirmFirstBtn : R.string.DeleteAccountConfirmBtn), Lang.getString(R.string.Cancel)}, + new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, + new int[] {R.drawable.baseline_delete_alert_24, R.drawable.baseline_cancel_24}, + (optionItemView, id) -> { + if (id == R.id.btn_deleteAccount) { + if (needShowAlternatives) { + SettingsLogOutController c = new SettingsLogOutController(context.context(), tdlib); + c.setArguments(SettingsLogOutController.Type.DELETE_ACCOUNT); + context.navigateTo(c); + return true; + } + + tdlib.send(new TdApi.GetPasswordState(), (passwordState, error) -> context.runOnUiThreadOptional(() -> { + if (error != null) { + UI.showError(error); + return; + } + context.runOnUiThreadOptional(() -> { + if (!passwordState.hasPassword) { + context.navigateTo(new EditDeleteAccountReasonController(context.context(), tdlib)); + return; + } + promptPassword(context, passwordState, new PasswordController.CustomConfirmDelegate() { + @Override + public CharSequence getName () { + return Lang.getString(R.string.DeleteAccount); + } + + @Override + public boolean needNext () { + return true; + } + + @Override + public void onPasswordConfirmed (ViewController c, String password) { + EditDeleteAccountReasonController target = new EditDeleteAccountReasonController(context.context(), tdlib); + target.setArguments(password); + c.navigateTo(target); + } + }); + }); + })); + } + return true; + } + ); + } + + private void promptPassword (ViewController context, TdApi.PasswordState passwordState, @NonNull PasswordController.CustomConfirmDelegate confirmDelegate) { + PasswordController controller = new PasswordController(context.context(), context.tdlib()); + controller.setArguments(new PasswordController.Args(PasswordController.MODE_CUSTOM_CONFIRM, passwordState) + .setConfirmDelegate(confirmDelegate) + ); + context.navigateTo(controller); + } + // Log out public void logOut (ViewController context, boolean showAlternatives) { @@ -3930,7 +3973,7 @@ public boolean processLeaveButton (ViewController context, TdApi.ChatList cha showDeleteChatConfirm(context, chatId, false, actionId == R.id.btn_removeChatFromListAndStop, after); return true; } else if (actionId == R.id.btn_removeChatFromListOrClearHistory) { - showDeleteChatConfirm(context, chatId, true, false, after); + showDeleteChatConfirm(context, chatId, !tdlib.isChannel(chatId), tdlib.suggestStopBot(chatId), after); return true; } else if (actionId == R.id.btn_clearChatHistory) { showClearHistoryConfirm(context, chatId, after); @@ -4171,14 +4214,14 @@ public void exitToChatScreen (ViewController context, long chatId) { // Delete chats - private @StringRes int getDeleteChatStringRes (long chatId) { + private @StringRes int getDeleteChatStringRes (long chatId, boolean allowBlock) { TdApi.Chat chat = tdlib.chat(chatId); if (chat == null) return R.string.DeleteChat; switch (ChatId.getType(chatId)) { case TdApi.ChatTypePrivate.CONSTRUCTOR: case TdApi.ChatTypeSecret.CONSTRUCTOR: { - return tdlib.suggestStopBot(chat) ? R.string.DeleteAndStop : R.string.DeleteChat; + return allowBlock && tdlib.suggestStopBot(chat) ? R.string.DeleteAndStop : R.string.DeleteChat; } case TdApi.ChatTypeBasicGroup.CONSTRUCTOR: { TdApi.BasicGroup basicGroup = tdlib.chatToBasicGroup(chatId); @@ -4204,7 +4247,11 @@ public void exitToChatScreen (ViewController context, long chatId) { } private void showClearHistoryConfirm (ViewController context, final long chatId, @Nullable Runnable after) { - if (tdlib.canRevokeChat(chatId) || tdlib.canClearHistoryForEveryone(chatId)) { + showClearHistoryConfirm(context, chatId, after, false); + } + + private void showClearHistoryConfirm (ViewController context, final long chatId, @Nullable Runnable after, boolean isSecondaryConfirm) { + if (tdlib.canRevokeChat(chatId) || (tdlib.canClearHistoryForAllUsers(chatId) && tdlib.canClearHistoryOnlyForSelf(chatId))) { context.showSettings(new SettingsWrapBuilder(R.id.btn_removeChatFromList) .setAllowResize(false) .addHeaderItem(tdlib.isSelfChat(chatId) ? Lang.getMarkdownString(context, R.string.ClearSavedMessagesConfirm) : Lang.getString(R.string.ClearHistoryConfirm)) @@ -4226,15 +4273,37 @@ private void showClearHistoryConfirm (ViewController context, final long chat U.run(after); })); } else { + final boolean revoke = !tdlib.canClearHistoryOnlyForSelf(chatId); + final boolean needSecondaryConfirm; + final CharSequence info; + final String confirmButton; + @DrawableRes int confirmButtonIcon = isSecondaryConfirm ? R.drawable.baseline_delete_forever_24 : R.drawable.templarian_baseline_broom_24; + if (tdlib.isSelfChat(chatId)) { + needSecondaryConfirm = true; + info = Lang.getMarkdownString(context, isSecondaryConfirm ? R.string.ClearSavedMessagesSecondaryConfirm : R.string.ClearSavedMessagesConfirm); + confirmButton = Lang.getString(isSecondaryConfirm ? R.string.ClearSavedMessages : R.string.ClearHistory); + } else if (tdlib.isChannel(chatId)) { + needSecondaryConfirm = true; + info = Lang.getMarkdownString(context, isSecondaryConfirm ? R.string.ClearChannelSecondaryConfirm : R.string.ClearChannelConfirm); + confirmButton = Lang.getString(isSecondaryConfirm ? R.string.ClearChannel : R.string.ClearHistoryAll); + } else { + needSecondaryConfirm = false; + info = Lang.getString(revoke ? R.string.ClearHistoryAllConfirm : R.string.ClearHistoryConfirm); + confirmButton = Lang.getString(revoke ? R.string.ClearHistoryAll : R.string.ClearHistory); + } context.showOptions( - tdlib.isSelfChat(chatId) ? Lang.getMarkdownString(context, R.string.ClearSavedMessagesConfirm) : Lang.getString(R.string.ClearHistoryConfirm), + info, new int[]{R.id.btn_clearChatHistory, R.id.btn_cancel}, - new String[]{Lang.getString(R.string.ClearHistory), Lang.getString(R.string.Cancel)}, + new String[]{confirmButton, Lang.getString(R.string.Cancel)}, new int[]{ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, - new int[]{R.drawable.templarian_baseline_broom_24, R.drawable.baseline_cancel_24}, (itemView, id) -> { + new int[]{confirmButtonIcon, R.drawable.baseline_cancel_24}, (itemView, id) -> { if (id == R.id.btn_clearChatHistory) { - tdlib.client().send(new TdApi.DeleteChatHistory(chatId, false, false), tdlib.okHandler()); - U.run(after); + if (needSecondaryConfirm && !isSecondaryConfirm) { + showClearHistoryConfirm(context, chatId, after, true); + } else { + tdlib.client().send(new TdApi.DeleteChatHistory(chatId, false, revoke), tdlib.okHandler()); + U.run(after); + } } return true; }); @@ -4245,12 +4314,12 @@ public void showDeleteChatConfirm (final ViewController context, final long c showDeleteChatConfirm(context, chatId, false, tdlib.suggestStopBot(chatId), null); } - private void showDeleteOrClearHistory (final ViewController context, final long chatId, final CharSequence chatName, final Runnable onDelete, final boolean allowClearHistory, Runnable after) { + private void showDeleteOrClearHistory (final ViewController context, final long chatId, final CharSequence chatName, final Runnable onDelete, final boolean allowClearHistory, final boolean allowBlock, Runnable after) { if (!allowClearHistory || !tdlib.canClearHistory(chatId)) { onDelete.run(); return; } - context.showOptions(chatName, new int[] {R.id.btn_removeChatFromList, R.id.btn_clearChatHistory}, new String[] {Lang.getString(getDeleteChatStringRes(chatId)), Lang.getString(R.string.ClearHistory)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_24, R.drawable.templarian_baseline_broom_24}, (itemView, id) -> { + context.showOptions(chatName, new int[] {R.id.btn_removeChatFromList, R.id.btn_clearChatHistory}, new String[] {Lang.getString(getDeleteChatStringRes(chatId, allowBlock)), Lang.getString(R.string.ClearHistory)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_24, R.drawable.templarian_baseline_broom_24}, (itemView, id) -> { if (id == R.id.btn_removeChatFromList) { onDelete.run(); } else if (id == R.id.btn_clearChatHistory) { @@ -4327,7 +4396,7 @@ private void showDeleteChatConfirm (final ViewController context, final long .setIntDelegate((id, result) -> { boolean clearHistory = result.get(R.id.btn_clearChatHistory) == R.id.btn_clearChatHistory; if (blockUser) { - tdlib.blockSender(new TdApi.MessageSenderUser(userId), true, blockResult -> deleter.runWithBool(clearHistory)); + tdlib.blockSender(new TdApi.MessageSenderUser(userId), new TdApi.BlockListMain(), blockResult -> deleter.runWithBool(clearHistory)); } else { deleter.runWithBool(clearHistory); } @@ -4337,7 +4406,7 @@ private void showDeleteChatConfirm (final ViewController context, final long context.showOptions(info, new int[] {R.id.btn_removeChatFromList, R.id.btn_cancel}, new String[] {Lang.getString(deleteAndStop ? R.string.DeleteAndStop : R.string.DeleteChat), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_24, R.drawable.baseline_cancel_24}, (itemView, resultId) -> { if (resultId == R.id.btn_removeChatFromList) { if (blockUser) { - tdlib.blockSender(new TdApi.MessageSenderUser(userId), true, blockResult -> deleter.runWithBool(false)); + tdlib.blockSender(new TdApi.MessageSenderUser(userId), new TdApi.BlockListMain(), blockResult -> deleter.runWithBool(false)); } else { deleter.runWithBool(false); } @@ -4345,7 +4414,7 @@ private void showDeleteChatConfirm (final ViewController context, final long return true; }); } - }, allowClearHistory, after); + }, allowClearHistory, deleteAndStop, after); break; } case TdApi.ChatTypeSecret.CONSTRUCTOR: { @@ -4384,7 +4453,7 @@ private void showDeleteChatConfirm (final ViewController context, final long context.showOptions(Lang.getStringBold(secretChat.state.getConstructor() == TdApi.SecretChatStatePending.CONSTRUCTOR ? R.string.DeleteSecretChatPendingConfirm : R.string.DeleteSecretChatClosedConfirm, userName), new int[] {R.id.btn_removeChatFromList, R.id.btn_cancel}, new String[]{Lang.getString(R.string.DeleteChat), Lang.getString(R.string.Cancel)}, new int[]{ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[]{R.drawable.baseline_delete_24, R.drawable.baseline_cancel_24}, (itemView, id) -> { if (id == R.id.btn_removeChatFromList) { if (blockUser) { - tdlib.blockSender(new TdApi.MessageSenderUser(secretChat.userId), true, blockResult -> deleter.runWithBool(false)); + tdlib.blockSender(new TdApi.MessageSenderUser(secretChat.userId), new TdApi.BlockListMain(), blockResult -> deleter.runWithBool(false)); } else { deleter.runWithBool(false); } @@ -4392,7 +4461,7 @@ private void showDeleteChatConfirm (final ViewController context, final long return true; }); } - }, allowClearHistory, after); + }, allowClearHistory, blockUser, after); break; } case TdApi.ChatTypeBasicGroup.CONSTRUCTOR: { @@ -4408,11 +4477,11 @@ private void showDeleteChatConfirm (final ViewController context, final long return true; }); } - }, allowClearHistory, after); + }, allowClearHistory, blockUser, after); break; } case TdApi.ChatTypeSupergroup.CONSTRUCTOR: { - showDeleteOrClearHistory(context, chatId, tdlib.chatTitle(chatId), () -> leaveJoinChat(context, chatId, false, after), allowClearHistory, after); + showDeleteOrClearHistory(context, chatId, tdlib.chatTitle(chatId), () -> leaveJoinChat(context, chatId, false, after), allowClearHistory, blockUser, after); break; } } @@ -4469,7 +4538,7 @@ public void showChatOptions (ViewController context, final TdApi.ChatList cha final boolean hasNotifications = tdlib.chatNotificationsEnabled(chat); - final int size = canSelect && onSelect != null ? 6 : 5; + final int size = canSelect && onSelect != null ? 8 : 7; IntList ids = new IntList(size); StringList strings = new StringList(size); @@ -4505,6 +4574,20 @@ public void showChatOptions (ViewController context, final TdApi.ChatList cha icons.append(isArchived ? R.drawable.baseline_unarchive_24 : R.drawable.baseline_archive_24); } + if (Settings.instance().chatFoldersEnabled()) { + if (TD.isChatListMain(chatList) || TD.isChatListArchive(chatList)) { + ids.append(R.id.btn_addChatToFolder); + strings.append(R.string.AddToFolder); + colors.append(ViewController.OPTION_COLOR_NORMAL); + icons.append(R.drawable.templarian_baseline_folder_plus_24); + } else if (TD.isChatListFolder(chatList)) { + ids.append(R.id.btn_removeChatFromFolder); + strings.append(R.string.RemoveFromFolder); + colors.append(ViewController.OPTION_COLOR_NORMAL); + icons.append(R.drawable.templarian_baseline_folder_remove_24); + } + } + boolean hasPasscode = tdlib.hasPasscode(chat); boolean canRead = tdlib.canMarkAsRead(chat); @@ -4595,12 +4678,12 @@ private void showArchiveUnarchiveChat (ViewController context, final TdApi.Ch } public void showInviteLinkOptionsPreload (ViewController context, final TdApi.ChatInviteLink link, final long chatId, final boolean showNavigatingToLinks, @Nullable Runnable onLinkDeleted, @Nullable RunnableData onLinkRevoked) { - context.tdlib().send(new TdApi.GetChatInviteLink(chatId, link.inviteLink), result -> { + context.tdlib().send(new TdApi.GetChatInviteLink(chatId, link.inviteLink), (inviteLink, error) -> { context.runOnUiThreadOptional(() -> { - if (result.getConstructor() == TdApi.ChatInviteLink.CONSTRUCTOR) { - showInviteLinkOptions(context, (TdApi.ChatInviteLink) result, chatId, showNavigatingToLinks, false, onLinkDeleted, onLinkRevoked); - } else { + if (error != null) { showInviteLinkOptions(context, link, chatId, showNavigatingToLinks, true, onLinkDeleted, onLinkRevoked); + } else { + showInviteLinkOptions(context, inviteLink, chatId, showNavigatingToLinks, false, onLinkDeleted, onLinkRevoked); } }); }); @@ -4711,7 +4794,7 @@ public void showInviteLinkOptions (ViewController context, final TdApi.ChatIn context.showOptions(Lang.getString(R.string.AreYouSureDeleteInviteLink), new int[] {R.id.btn_deleteLink, R.id.btn_cancel}, new String[] {Lang.getString(R.string.InviteLinkDelete), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_24, R.drawable.baseline_cancel_24}, (itemView2, id2) -> { if (id2 == R.id.btn_deleteLink) { if (onLinkDeleted != null) onLinkDeleted.run(); - context.tdlib().client().send(new TdApi.DeleteRevokedChatInviteLink(chatId, link.inviteLink), null); + context.tdlib().client().send(new TdApi.DeleteRevokedChatInviteLink(chatId, link.inviteLink), tdlib.okHandler()); } return true; @@ -4734,6 +4817,47 @@ public void showInviteLinkOptions (ViewController context, final TdApi.ChatIn }); } + public void showAddChatToFolderOptions (ViewController context, long chatId, @Nullable Runnable after) { + showAddChatsToFolderOptions(context, new long[] {chatId}, after); + } + + public void showAddChatsToFolderOptions (ViewController context, long[] chatIds, @Nullable Runnable after) { + if (chatIds.length == 0) + return; + + TdApi.ChatFolderInfo[] chatFolders = tdlib.chatFolders(); + List items = new ArrayList<>(chatFolders.length + 1); + for (TdApi.ChatFolderInfo chatFolderInfo : chatFolders) { + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.chatFolder, TD.findFolderIcon(chatFolderInfo.icon, R.drawable.baseline_folder_24), chatFolderInfo.title).setIntValue(chatFolderInfo.id)); + } + if (tdlib.chatFoldersCount() < tdlib.chatFolderCountMax()) { + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_createNewFolder, R.drawable.baseline_create_new_folder_24, R.string.CreateNewFolder).setTextColorId(ColorId.textNeutral)); + } + SettingsWrap[] settings = new SettingsWrap[1]; + settings[0] = context.showSettings(new SettingsWrapBuilder(R.id.btn_addChatToFolder) + .addHeaderItem(Lang.getString(R.string.ChooseFolder)) + .setRawItems(items) + .setNeedSeparators(false) + .setDisableFooter(true) + .setNeedRootInsets(true) + .setSettingProcessor((item, view, isUpdate) -> { + view.setIconColorId(item.getId() == R.id.btn_createNewFolder ? ColorId.inlineIcon : ColorId.NONE); + }) + .setOnSettingItemClick((view, id, item, done, adapter) -> { + settings[0].window.hideWindow(true); + if (item.getId() == R.id.btn_createNewFolder) { + TdApi.ChatFolder chatFolder = TD.newChatFolder(chatIds); + context.context().navigation().navigateTo(EditChatFolderController.newFolder(context.context(), tdlib, chatFolder)); + } else { + int chatFolderId = item.getIntValue(); + tdlib.addChatsToChatFolder(context, chatFolderId, chatIds); + } + if (after != null) { + after.run(); + } + })); + } + public boolean processChatAction (ViewController context, final TdApi.ChatList chatList, final long chatId, final @Nullable ThreadInfo messageThread, final TdApi.MessageSource source, final int actionId, @Nullable Runnable after) { TdApi.Chat chat = tdlib.chat(chatId); if (chat == null) @@ -4772,6 +4896,15 @@ public boolean processChatAction (ViewController context, final TdApi.ChatLis } else if (actionId == R.id.btn_phone_call) { tdlib.context().calls().makeCallDelayed(context, TD.getUserId(chat), null, true); return true; + } else if (actionId == R.id.btn_addChatToFolder) { + showAddChatToFolderOptions(context, chatId, /* after */ null); + return true; + } else if (actionId == R.id.btn_removeChatFromFolder) { + if (TD.isChatListFolder(chatList)) { + int chatFolderId = ((TdApi.ChatListFolder) chatList).chatFolderId; + tdlib.removeChatFromChatFolder(chatFolderId, chatId); + } + return true; } return processLeaveButton(context, chatList, chatId, actionId, after); } @@ -5273,8 +5406,8 @@ public void showLanguageInstallPrompt (TdlibDelegate c, TdApi.LanguagePackInfo i public void showLanguageInstallPrompt (ViewController c, CustomLangPackResult out, TdApi.Message sourceMessage) { if (sourceMessage != null) { TdApi.Chat sourceChat; - if (sourceMessage.forwardInfo != null && sourceMessage.forwardInfo.origin.getConstructor() == TdApi.MessageForwardOriginChannel.CONSTRUCTOR) { - sourceChat = tdlib.chat(((TdApi.MessageForwardOriginChannel) sourceMessage.forwardInfo.origin).chatId); + if (sourceMessage.forwardInfo != null && sourceMessage.forwardInfo.origin.getConstructor() == TdApi.MessageOriginChannel.CONSTRUCTOR) { + sourceChat = tdlib.chat(((TdApi.MessageOriginChannel) sourceMessage.forwardInfo.origin).chatId); } else { sourceChat = (!sourceMessage.isOutgoing || sourceMessage.isChannelPost) ? tdlib.chat(sourceMessage.chatId) : null; } @@ -5330,7 +5463,7 @@ public static void removeAccount (ViewController context, final TdlibAccount } private static void removeAccount (ViewController context, final TdlibAccount account, boolean isSignOut) { - context.showOptions(Lang.getStringBold(isSignOut ? R.string.SignOutHint2 : R.string.RemoveAccountHint2, account.getName()), new int[]{R.id.btn_removeAccount, R.id.btn_cancel}, new String[]{Lang.getString(R.string.LogOut), Lang.getString(R.string.Cancel)}, new int[]{ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_forever_24, R.drawable.baseline_cancel_24}, (itemView, id) -> { + context.showOptions(Lang.getStringBold(isSignOut ? R.string.SignOutHint2 : R.string.RemoveAccountHint2, account.getName()), new int[]{R.id.btn_removeAccount, R.id.btn_cancel}, new String[]{Lang.getString(R.string.LogOut), Lang.getString(R.string.Cancel)}, new int[]{ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_logout_24, R.drawable.baseline_cancel_24}, (itemView, id) -> { if (id == R.id.btn_removeAccount) { account.tdlib().signOut(); } @@ -5736,7 +5869,16 @@ public void readCustomTheme (ViewController context, TdApi.File doc, @Nullabl tdlib.ui().post(() -> { CharSequence info; if (theme.author != null) { - info = Lang.getString(R.string.ThemeInstallAuthor, (target, argStart, argEnd, argIndex, fakeBold) -> argIndex == 1 ? new CustomTypefaceSpan(null, ColorId.textLink).setEntityType(new TdApi.TextEntityTypeMention()).setForcedTheme(theme) : Lang.newBoldSpan(fakeBold), theme.name, "@" + theme.author); + info = Lang.getString(R.string.ThemeInstallAuthor, (target, argStart, argEnd, argIndex, fakeBold) -> { + if (argIndex == 1) { + CustomTypefaceSpan span = new CustomTypefaceSpan(null, ColorId.textLink); + span.setTextEntityType(new TdApi.TextEntityTypeMention()); + span.setForcedTheme(theme); + return span; + } else { + return Lang.newBoldSpan(fakeBold); + } + }, theme.name, "@" + theme.author); } else { info = Lang.getStringBold(R.string.ThemeInstall, theme.name); } @@ -5868,25 +6010,26 @@ private static void fillReportReasons (IntList ids, StringList strings) { } private static > void toReportReasons (ViewController context, int reportReasonId, CharSequence title, T request, boolean forceText, RunnableData reportCallback) { - final TdApi.ChatReportReason reason; + final TdApi.ReportReason reason; + Td.assertReportReason_cf03e541(); if (reportReasonId == R.id.btn_reportChatSpam) { - reason = new TdApi.ChatReportReasonSpam(); + reason = new TdApi.ReportReasonSpam(); } else if (reportReasonId == R.id.btn_reportChatFake) { - reason = new TdApi.ChatReportReasonFake(); + reason = new TdApi.ReportReasonFake(); } else if (reportReasonId == R.id.btn_reportChatViolence) { - reason = new TdApi.ChatReportReasonViolence(); + reason = new TdApi.ReportReasonViolence(); } else if (reportReasonId == R.id.btn_reportChatPornography) { - reason = new TdApi.ChatReportReasonPornography(); + reason = new TdApi.ReportReasonPornography(); } else if (reportReasonId == R.id.btn_reportChatCopyright) { - reason = new TdApi.ChatReportReasonCopyright(); + reason = new TdApi.ReportReasonCopyright(); } else if (reportReasonId == R.id.btn_reportChatChildAbuse) { - reason = new TdApi.ChatReportReasonChildAbuse(); + reason = new TdApi.ReportReasonChildAbuse(); } else if (reportReasonId == R.id.btn_reportChatIllegalDrugs) { - reason = new TdApi.ChatReportReasonIllegalDrugs(); + reason = new TdApi.ReportReasonIllegalDrugs(); } else if (reportReasonId == R.id.btn_reportChatPersonalDetails) { - reason = new TdApi.ChatReportReasonPersonalDetails(); + reason = new TdApi.ReportReasonPersonalDetails(); } else if (reportReasonId == R.id.btn_reportChatOther) { // TODO replace with openInputAlert - reason = new TdApi.ChatReportReasonCustom(); + reason = new TdApi.ReportReasonCustom(); forceText = true; } else { throw new IllegalArgumentException(Lang.getResourceEntryName(reportReasonId)); @@ -6292,68 +6435,80 @@ default void onOwnershipTransferAbilityChecked (TdApi.Object result) { } } public void requestTransferOwnership (ViewController context, CharSequence finalAlertMessageText, OwnershipTransferListener listener) { - tdlib.client().send(new TdApi.CanTransferOwnership(), result -> { - tdlib.ui().post(() -> { - listener.onOwnershipTransferAbilityChecked(result); - switch (result.getConstructor()) { - case TdApi.CanTransferOwnershipResultOk.CONSTRUCTOR: - tdlib.client().send(new TdApi.GetPasswordState(), state -> { - if (state.getConstructor() != TdApi.PasswordState.CONSTRUCTOR) return; - post(() -> { - PasswordController controller = new PasswordController(context.context(), context.tdlib()); - controller.setArguments(new PasswordController.Args(PasswordController.MODE_TRANSFER_OWNERSHIP_CONFIRM, (TdApi.PasswordState) state).setSuccessListener(password -> { - // Ask if the user REALLY wants to transfer ownership, because this operation is serious - context.addOneShotFocusListener(() -> - context.showOptions(new ViewController.Options.Builder() - .info(Strings.getTitleAndText(Lang.getString(R.string.TransferOwnershipAlert), finalAlertMessageText)) - .item(new ViewController.OptionItem(R.id.btn_next, Lang.getString(R.string.TransferOwnershipConfirm), ViewController.OPTION_COLOR_RED, R.drawable.templarian_baseline_account_switch_24)) - .cancelItem() - .build(), (optionView, id) -> { - if (id == R.id.btn_next) { - listener.onOwnershipTransferConfirmed(password); - } - return true; - }) - ); - })); - context.navigateTo(controller); - }); + tdlib.send(new TdApi.CanTransferOwnership(), (canTransferOwnership, error) -> post(() -> { + listener.onOwnershipTransferAbilityChecked(canTransferOwnership != null ? canTransferOwnership : error); + if (error != null) { + UI.showError(error); + return; + } + switch (canTransferOwnership.getConstructor()) { + case TdApi.CanTransferOwnershipResultOk.CONSTRUCTOR: { + tdlib.send(new TdApi.GetPasswordState(), (passwordState, error1) -> { + if (error1 != null) { + UI.showError(error1); + return; + } + post(() -> { + PasswordController controller = new PasswordController(context.context(), context.tdlib()); + controller.setArguments(new PasswordController.Args(PasswordController.MODE_TRANSFER_OWNERSHIP_CONFIRM, passwordState).setSuccessListener(password -> { + // Ask if the user REALLY wants to transfer ownership, because this operation is serious + context.addOneShotFocusListener(() -> + context.showOptions(new ViewController.Options.Builder() + .info(Strings.getTitleAndText(Lang.getString(R.string.TransferOwnershipAlert), finalAlertMessageText)) + .item(new ViewController.OptionItem(R.id.btn_next, Lang.getString(R.string.TransferOwnershipConfirm), ViewController.OPTION_COLOR_RED, R.drawable.templarian_baseline_account_switch_24)) + .cancelItem() + .build(), (optionView, id) -> { + if (id == R.id.btn_next) { + listener.onOwnershipTransferConfirmed(password); + } + return true; + }) + ); + })); + context.navigateTo(controller); }); - break; - case TdApi.CanTransferOwnershipResultPasswordNeeded.CONSTRUCTOR: - context.showOptions(new ViewController.Options.Builder() - .info(Strings.getTitleAndText(Lang.getString(R.string.TransferOwnershipSecurityAlert), Lang.getMarkdownString(context, R.string.TransferOwnershipSecurityPasswordNeeded))) - .item(new ViewController.OptionItem(R.id.btn_next, Lang.getString(R.string.TransferOwnershipSecurityActionSetPassword), ViewController.OPTION_COLOR_BLUE, R.drawable.mrgrigri_baseline_textbox_password_24)) - .cancelItem() - .build(), (optionView, id) -> { - if (id == R.id.btn_next) { - Settings2FAController controller = new Settings2FAController(context.context(), context.tdlib()); - controller.setArguments(new Settings2FAController.Args(null)); - context.navigateTo(controller); - } - return true; + }); + break; + } + case TdApi.CanTransferOwnershipResultPasswordNeeded.CONSTRUCTOR: { + context.showOptions(new ViewController.Options.Builder() + .info(Strings.getTitleAndText(Lang.getString(R.string.TransferOwnershipSecurityAlert), Lang.getMarkdownString(context, R.string.TransferOwnershipSecurityPasswordNeeded))) + .item(new ViewController.OptionItem(R.id.btn_next, Lang.getString(R.string.TransferOwnershipSecurityActionSetPassword), ViewController.OPTION_COLOR_BLUE, R.drawable.mrgrigri_baseline_textbox_password_24)) + .cancelItem() + .build(), (optionView, id) -> { + if (id == R.id.btn_next) { + Settings2FAController controller = new Settings2FAController(context.context(), context.tdlib()); + controller.setArguments(new Settings2FAController.Args(null)); + context.navigateTo(controller); } - ); - break; - case TdApi.CanTransferOwnershipResultPasswordTooFresh.CONSTRUCTOR: - context.showOptions(new ViewController.Options.Builder() - .info(Strings.getTitleAndText(Lang.getString(R.string.TransferOwnershipSecurityAlert), Lang.getMarkdownString(context, R.string.TransferOwnershipSecurityWaitPassword, Lang.getDuration(((TdApi.CanTransferOwnershipResultPasswordTooFresh) result).retryAfter)))) - .item(new ViewController.OptionItem(R.id.btn_next, Lang.getString(R.string.OK), ViewController.OPTION_COLOR_NORMAL, R.drawable.baseline_check_circle_24)) - .cancelItem() - .build(), (optionView, id) -> true - ); - break; - case TdApi.CanTransferOwnershipResultSessionTooFresh.CONSTRUCTOR: - context.showOptions(new ViewController.Options.Builder() - .info(Strings.getTitleAndText(Lang.getString(R.string.TransferOwnershipSecurityAlert), Lang.getMarkdownString(context, R.string.TransferOwnershipSecurityWaitSession, Lang.getDuration(((TdApi.CanTransferOwnershipResultSessionTooFresh) result).retryAfter)))) - .item(new ViewController.OptionItem(R.id.btn_next, Lang.getString(R.string.OK), ViewController.OPTION_COLOR_NORMAL, R.drawable.baseline_check_circle_24)) - .cancelItem() - .build(), (optionView, id) -> true - ); - break; + return true; + } + ); + break; } - }); - }); + case TdApi.CanTransferOwnershipResultPasswordTooFresh.CONSTRUCTOR: { + context.showOptions(new ViewController.Options.Builder() + .info(Strings.getTitleAndText(Lang.getString(R.string.TransferOwnershipSecurityAlert), Lang.getMarkdownString(context, R.string.TransferOwnershipSecurityWaitPassword, Lang.getDuration(((TdApi.CanTransferOwnershipResultPasswordTooFresh) canTransferOwnership).retryAfter)))) + .item(new ViewController.OptionItem(R.id.btn_next, Lang.getString(R.string.OK), ViewController.OPTION_COLOR_NORMAL, R.drawable.baseline_check_circle_24)) + .cancelItem() + .build(), (optionView, id) -> true + ); + break; + } + case TdApi.CanTransferOwnershipResultSessionTooFresh.CONSTRUCTOR: { + context.showOptions(new ViewController.Options.Builder() + .info(Strings.getTitleAndText(Lang.getString(R.string.TransferOwnershipSecurityAlert), Lang.getMarkdownString(context, R.string.TransferOwnershipSecurityWaitSession, Lang.getDuration(((TdApi.CanTransferOwnershipResultSessionTooFresh) canTransferOwnership).retryAfter)))) + .item(new ViewController.OptionItem(R.id.btn_next, Lang.getString(R.string.OK), ViewController.OPTION_COLOR_NORMAL, R.drawable.baseline_check_circle_24)) + .cancelItem() + .build(), (optionView, id) -> true + ); + break; + } + default: + Td.assertCanTransferOwnershipResult_ac091006(); + throw Td.unsupported(canTransferOwnership); + } + })); } public void saveGifs (List downloadedFiles) { @@ -6457,4 +6612,669 @@ public void openVoiceChatInvitation (ViewController context, TdApi.InternalLi public void openVoiceChat (ViewController context, int groupCallId, @Nullable UrlOpenParameters openParameters) { // TODO open voice chat layer } + + // Suggestions by emoji + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + StickersType.INSTALLED, + StickersType.INSTALLED_EXTRA, + StickersType.RECOMMENDED + }) + public @interface StickersType { + int INSTALLED = 0, INSTALLED_EXTRA = 1, RECOMMENDED = 2; + } + + public static class EmojiStickers { + private final Tdlib tdlib; + public final String query; + public final boolean isComplexQuery; + + private TdApi.Stickers installedStickers; + private TdApi.Stickers installedExtraStickers; + private TdApi.Stickers recommendedStickers; + + public EmojiStickers (Tdlib tdlib, TdApi.StickerType stickerType, String query, boolean isComplexQuery, int limit, long chatId, boolean needRecommended) { + this.tdlib = tdlib; + this.query = query; + this.isComplexQuery = isComplexQuery; + + this.haveInstalledStickers = new ConditionalExecutor(() -> + this.installedStickers != null + ); + this.haveInstalledExtraStickers = new ConditionalExecutor(() -> + this.installedExtraStickers != null + ); + this.haveRecommendedStickers = new ConditionalExecutor(() -> + this.recommendedStickers != null + ); + + if (!isComplexQuery) { + setStickers(noStickers(), StickersType.INSTALLED_EXTRA); + } + if (!needRecommended) { + setStickers(noStickers(), StickersType.RECOMMENDED); + } + tdlib.client().send(new TdApi.GetStickers(stickerType, query, limit, chatId), object -> + setStickers(object, StickersType.INSTALLED) + ); + if (isComplexQuery) { + tdlib.client().send(new TdApi.SearchEmojis(query, false, U.getInputLanguages()), result -> { + String[] emojis = result.getConstructor() == TdApi.Emojis.CONSTRUCTOR ? ((TdApi.Emojis) result).emojis : null; + if (emojis != null && emojis.length > 0) { + String emojisQuery = TextUtils.join(" ", emojis); + // Request 2x more than limit for the case all of the stickers returned by GetStickers + tdlib.client().send(new TdApi.GetStickers(stickerType, emojisQuery, limit * 2, chatId), object -> + setStickers(object, StickersType.INSTALLED_EXTRA) + ); + if (needRecommended) { + tdlib.client().send(new TdApi.SearchStickers(stickerType, emojisQuery, limit * 3), object -> + setStickers(object, StickersType.RECOMMENDED) + ); + } + } else { + setStickers(noStickers(), StickersType.INSTALLED_EXTRA); + if (needRecommended) { + setStickers(noStickers(), StickersType.RECOMMENDED); + } + } + }); + } else { + if (needRecommended) { + // Request 2x more than limit for the case all of the stickers returned by GetStickers + tdlib.client().send(new TdApi.SearchStickers(stickerType, query, limit * 2), object -> + setStickers(object, StickersType.RECOMMENDED) + ); + } + } + } + + private static TdApi.Stickers noStickers () { + return new TdApi.Stickers(new TdApi.Sticker[0]); + } + + private boolean isLoading () { + return installedStickers == null || installedExtraStickers == null || recommendedStickers == null; + } + + void setStickers (TdApi.Object stickersRaw, @StickersType int type) { + TdApi.Stickers stickers = stickersRaw.getConstructor() == TdApi.Stickers.CONSTRUCTOR ? (TdApi.Stickers) stickersRaw : new TdApi.Stickers(new TdApi.Sticker[0]); + switch (type) { + case StickersType.INSTALLED: + this.installedStickers = stickers; + haveInstalledStickers.notifyConditionChanged(); + break; + case StickersType.INSTALLED_EXTRA: + this.installedExtraStickers = stickers; + haveInstalledExtraStickers.notifyConditionChanged(); + break; + case StickersType.RECOMMENDED: + this.recommendedStickers = stickers; + haveRecommendedStickers.notifyConditionChanged(); + break; + } + } + + private final ConditionalExecutor haveInstalledStickers; + private final ConditionalExecutor haveInstalledExtraStickers; + private final ConditionalExecutor haveRecommendedStickers; + + private TdApi.Sticker[] getInstalledStickers (boolean onlyExtra) { + int installedCount = (this.installedStickers != null ? this.installedStickers.stickers.length : 0); + int maxCount = installedCount + (this.installedExtraStickers != null ? this.installedExtraStickers.stickers.length : 0); + List stickers = new ArrayList<>(maxCount); + if (this.installedStickers != null && !onlyExtra) { + Collections.addAll(stickers, this.installedStickers.stickers); + } + if (this.installedExtraStickers != null && this.installedExtraStickers.stickers.length > 0) { + LongSet excludeStickerIds = new LongSet(installedCount); + if (installedCount > 0) { + for (TdApi.Sticker sticker : this.installedStickers.stickers) { + excludeStickerIds.add(sticker.id); + } + } + ArrayUtils.ensureCapacity(stickers, stickers.size() + this.installedExtraStickers.stickers.length); + for (TdApi.Sticker sticker : this.installedExtraStickers.stickers) { + if (!excludeStickerIds.has(sticker.id)) { + stickers.add(sticker); + } + } + } + return stickers.toArray(new TdApi.Sticker[0]); + } + + private TdApi.Sticker[] getRecommendedStickers (@Nullable TdApi.Sticker[] excludeStickers) { + int maxCount = this.recommendedStickers != null ? this.recommendedStickers.stickers.length : 0; + List stickers = new ArrayList<>(maxCount); + if (this.recommendedStickers != null) { + if (excludeStickers != null && excludeStickers.length > 0) { + LongSet excludeStickerIds = new LongSet(excludeStickers.length); + for (TdApi.Sticker sticker : excludeStickers) { + excludeStickerIds.add(sticker.id); + } + for (TdApi.Sticker sticker : this.recommendedStickers.stickers) { + if (!excludeStickerIds.has(sticker.id)) { + stickers.add(sticker); + } + } + } else { + Collections.addAll(stickers, this.recommendedStickers.stickers); + } + } + return stickers.toArray(new TdApi.Sticker[0]); + } + + public interface Callback { + void onStickersLoaded (EmojiStickers context, @NonNull TdApi.Sticker[] installedStickers, @Nullable TdApi.Sticker[] recommendedStickers, boolean expectMoreStickers); + + default void onMoreInstalledStickersLoaded (EmojiStickers context, @NonNull TdApi.Sticker[] moreInstalledStickers) { } + default void onRecommendedStickersLoaded (EmojiStickers context, @NonNull TdApi.Sticker[] recommendedStickers) { } + default void onAllStickersFinishedLoading (EmojiStickers context) { } + } + + public void getStickers (Callback callback, long totalTimeoutMs) { + if (totalTimeoutMs <= 0) { + // Lazy path: wait for all methods to finish, invoke callback.onStickersLoaded + haveInstalledStickers.executeOrPostponeTask(() -> { + haveInstalledExtraStickers.executeOrPostponeTask(() -> { + haveRecommendedStickers.executeOrPostponeTask(() -> { + TdApi.Sticker[] installedStickers = getInstalledStickers(false); + TdApi.Sticker[] recommendedStickers = getRecommendedStickers(installedStickers); + tdlib.uiExecute(() -> + callback.onStickersLoaded(this, installedStickers, recommendedStickers, false) + ); + }); + }); + }); + return; + } + + // Async path: wait up to max(totalTimeoutMs, first non-empty result) + // Then invoke callback.onStickersLoaded with at least one sticker. + // If `expectMoreStickers` was true: + // - callback.onMoreInstalledStickersLoaded gets called if more installed stickers were loaded (non-empty) + // - callback.onRecommendedStickersLoaded gets called if recommended stickers were loaded (might be empty) + // - callback.onAllStickersFinishedLoading gets called after all requests are complete, no more stickers + + final long startTime = SystemClock.uptimeMillis(); + + haveInstalledStickers.executeOrPostponeTask(() -> { + CancellationSignal timeoutSignal = new CancellationSignal(); + AtomicBoolean timeoutPostponed = new AtomicBoolean(false); + AtomicBoolean isExpectingMoreStickers = new AtomicBoolean(false); + Runnable postponeTimeout = () -> { + if (timeoutPostponed.getAndSet(true)) { + return; + } + final long elapsedMs = SystemClock.uptimeMillis() - startTime; + final long timeoutMs = Math.max(0, totalTimeoutMs - elapsedMs); + tdlib.runOnTdlibThread(() -> { + synchronized (isExpectingMoreStickers) { + if (timeoutSignal.isCanceled()) { + // Do nothing, because result was already sent + return; + } + TdApi.Sticker[] installedStickers = getInstalledStickers(false); + TdApi.Sticker[] recommendedStickers = this.recommendedStickers != null ? getRecommendedStickers(installedStickers) : null; + boolean expectMoreStickers = isLoading(); + tdlib.ui().post(() -> + callback.onStickersLoaded(this, installedStickers, recommendedStickers, expectMoreStickers) + ); + isExpectingMoreStickers.set(expectMoreStickers); + timeoutSignal.cancel(); + } + }, (double) timeoutMs / 1000.0, false); + }; + haveInstalledExtraStickers.executeOrPostponeTask(() -> { + if (installedExtraStickers.stickers.length > 0) { + synchronized (isExpectingMoreStickers) { + if (isExpectingMoreStickers.get()) { + TdApi.Sticker[] installedStickers = getInstalledStickers(true); + tdlib.ui().post(() -> + callback.onMoreInstalledStickersLoaded(this, installedStickers) + ); + } + } + } + + haveRecommendedStickers.executeOrPostponeTask(() -> { + TdApi.Sticker[] installedStickers, recommendedStickers; + boolean callbackExecuted; + synchronized (isExpectingMoreStickers) { + timeoutSignal.cancel(); + installedStickers = getInstalledStickers(false); + recommendedStickers = getRecommendedStickers(installedStickers); + callbackExecuted = isExpectingMoreStickers.get(); + if (callbackExecuted) { + tdlib.ui().post(() -> { + callback.onRecommendedStickersLoaded(this, recommendedStickers); + callback.onAllStickersFinishedLoading(this); + }); + } + } + if (!callbackExecuted) { + tdlib.uiExecute(() -> + callback.onStickersLoaded(this, installedStickers, recommendedStickers, false) + ); + } + }); + + if (installedExtraStickers.stickers.length > 0) { + postponeTimeout.run(); + } + }); + if (installedStickers.stickers.length > 0) { + postponeTimeout.run(); + } + }); + } + } + + public EmojiStickers getEmojiStickers (final TdApi.StickerType stickerType, final String query, final boolean isComplexQuery, int limit, long chatId) { + int mode; + if (stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR) { + mode = Settings.instance().getEmojiMode(); + } else { + mode = Settings.instance().getStickerMode(); + } + if (tdlib.suggestOnlyApiStickers() && mode == Settings.STICKER_MODE_ALL) { + mode = Settings.STICKER_MODE_ONLY_INSTALLED; + } + return new EmojiStickers(tdlib, stickerType, query, isComplexQuery, limit, chatId, mode == Settings.STICKER_MODE_ALL); + } + + public interface MessageProvider { + default boolean isSponsoredMessage () { + return false; + } + default TdApi.SponsoredMessage getVisibleSponsoredMessage () { + return null; + } + default boolean isMediaGroup () { + return false; + } + default List getVisibleMediaGroup () { + return null; + } + TdApi.Message getVisibleMessage (); + default @TdlibMessageViewer.Flags int getVisibleMessageFlags () { + return 0; + } + default long getVisibleChatId () { + TdApi.Message message = getVisibleMessage(); + return message != null ? message.chatId : 0; + } + } + + public interface MessageViewCallback { + boolean onMessageViewed (TdlibMessageViewer.Viewport viewport, View view, TdApi.Message message, @TdlibMessageViewer.Flags long flags, long viewId, boolean allowRequest); + default boolean needForceRead (TdlibMessageViewer.Viewport viewport) { + return false; + } + default boolean allowViewRequest (TdlibMessageViewer.Viewport viewport) { + return true; + } + + default void onSponsoredMessageViewed (TdlibMessageViewer.Viewport viewport, View view, TdApi.SponsoredMessage sponsoredMessage, @TdlibMessageViewer.Flags long flags, long viewId, boolean allowRequest) { + // Do nothing? + } + default boolean isMessageContentVisible (TdlibMessageViewer.Viewport viewport, View view) { + return true; + } + } + + public Runnable attachViewportToRecyclerView (TdlibMessageViewer.Viewport viewport, RecyclerView recyclerView) { + return attachViewportToRecyclerView(viewport, recyclerView, null); + } + + public Runnable attachViewportToRecyclerView (TdlibMessageViewer.Viewport viewport, RecyclerView recyclerView, @Nullable MessageViewCallback callback) { + Runnable viewMessages = () -> { + if (viewport.isDestroyed()) { + return; + } + boolean allowViewRequest = callback == null || callback.allowViewRequest(viewport); + boolean forceRead = callback != null && callback.needForceRead(viewport); + LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager(); + if (manager == null) + throw new IllegalStateException(); + int startIndex = manager.findFirstVisibleItemPosition(); + int endIndex = manager.findLastVisibleItemPosition(); + final long viewId = SystemClock.uptimeMillis(); + int viewedMessageCount = 0; + if (startIndex != -1 && endIndex != -1) { + for (int index = startIndex; index <= endIndex; index++) { + View view = manager.findViewByPosition(index); + if (view instanceof MessageProvider) { + MessageProvider provider = (MessageProvider) view; + boolean canViewMessage = callback == null || callback.isMessageContentVisible(viewport, view); + if (!canViewMessage) { + continue; + } + @TdlibMessageViewer.Flags int flags = provider.getVisibleMessageFlags(); + if (provider.isSponsoredMessage()) { + TdApi.SponsoredMessage sponsoredMessage = provider.getVisibleSponsoredMessage(); + long chatId = provider.getVisibleChatId(); + if (sponsoredMessage != null && viewport.addVisibleMessage(chatId, sponsoredMessage, flags, viewId, false)) { + if (callback != null) { + callback.onSponsoredMessageViewed(viewport, view, sponsoredMessage, flags, viewId, allowViewRequest); + } + viewedMessageCount++; + } + } else if (provider.isMediaGroup()) { + List mediaGroup = provider.getVisibleMediaGroup(); + if (mediaGroup != null) { + for (TdApi.Message message : mediaGroup) { + boolean forceMarkAsRecent = callback != null && callback.onMessageViewed(viewport, view, message, flags, viewId, allowViewRequest); + if (viewport.addVisibleMessage(message, flags, viewId, forceMarkAsRecent)) { + viewedMessageCount++; + } + } + } + } else { + TdApi.Message message = provider.getVisibleMessage(); + if (message != null) { + boolean forceMarkAsRecent = callback != null && callback.onMessageViewed(viewport, view, message, flags, viewId, allowViewRequest); + if (viewport.addVisibleMessage(message, flags, viewId, forceMarkAsRecent)) { + viewedMessageCount++; + } + } + } + } + } + } + viewport.removeOtherVisibleMessagesByViewId(viewId); + if (allowViewRequest && (viewedMessageCount > 0 || viewport.haveRecentlyViewedMessages())) { + viewport.viewMessages(true, forceRead, null); + } + }; + RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + viewMessages.run(); + } + + private boolean isScrolling; + + @Override + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { + boolean wasScrolling = this.isScrolling; + this.isScrolling = newState != RecyclerView.SCROLL_STATE_IDLE; + if (this.isScrolling != wasScrolling && !this.isScrolling) { + viewMessages.run(); + } + } + }; + viewport.addOnDestroyListener(() -> + recyclerView.removeOnScrollListener(onScrollListener) + ); + recyclerView.addOnScrollListener(onScrollListener); + return viewMessages; + } + + + public static class AvatarPickerManager { + public static final int MODE_PROFILE = 0; + public static final int MODE_PROFILE_PUBLIC = 1; + public static final int MODE_CHAT = 2; + public static final int MODE_NON_CREATED = 3; + + private final ViewController context; + private final Tdlib tdlib; + + public AvatarPickerManager (ViewController context) { + this.context = context; + this.tdlib = context.tdlib(); + } + + public void showMenuForProfile (@Nullable MediaCollectorDelegate delegate, boolean isPublic) { + final ViewController.Options.Builder b = new ViewController.Options.Builder(); + + final TdApi.User user = tdlib.myUser(); + final TdApi.UserFullInfo userFullInfo = tdlib.myUserFull(); + + final long profilePhotoToDelete = isPublic ? + (userFullInfo != null && userFullInfo.publicPhoto != null ? userFullInfo.publicPhoto.id : 0) : + (user != null && user.profilePhoto != null ? user.profilePhoto.id : 0); + + if (profilePhotoToDelete != 0 && !isPublic) { + b.item(new ViewController.OptionItem(R.id.btn_open, Lang.getString(R.string.Open), + ViewController.OPTION_COLOR_NORMAL, R.drawable.baseline_visibility_24)); + } + + b.item(new ViewController.OptionItem(R.id.btn_changePhotoGallery, Lang.getString(isPublic ? R.string.SetPublicPhoto : R.string.SetProfilePhoto), + ViewController.OPTION_COLOR_NORMAL, R.drawable.baseline_image_24)); + + final Runnable deleteRunnable = () -> showDeletePhotoConfirm(() -> deleteProfilePhoto(profilePhotoToDelete)); + if (profilePhotoToDelete != 0 && !isPublic) { + b.item(new ViewController.OptionItem(R.id.btn_changePhotoDelete, Lang.getString(R.string.Delete), + ViewController.OPTION_COLOR_RED, R.drawable.baseline_delete_24)); + } + + showOptions(b.build(), (itemView, id) -> { + if (id == R.id.btn_open) { + MediaViewController.openFromProfile(context, user, delegate); + } else if (id == R.id.btn_changePhotoGallery) { + openMediaView(false, false, AvatarPickerMode.PROFILE, f -> onProfilePhotoReceived(f, isPublic), + profilePhotoToDelete != 0 ? Lang.getString(isPublic ? R.string.RemovePublicPhoto : R.string.RemoveProfilePhoto) : null, + ColorId.textNegative, deleteRunnable); + } else if (id == R.id.btn_changePhotoDelete) { + deleteRunnable.run(); + } + return true; + }); + } + + public void showMenuForChat (TdApi.Chat chat, MediaCollectorDelegate delegate, boolean allowOpenPhoto) { + if (chat == null) { + return; + } + + final boolean isChannel = tdlib.isChannel(chat.id); + ViewController.Options.Builder b = new ViewController.Options.Builder(); + + if (chat.photo != null && allowOpenPhoto) { + b.item(new ViewController.OptionItem(R.id.btn_open, Lang.getString(R.string.Open), + ViewController.OPTION_COLOR_NORMAL, R.drawable.baseline_visibility_24)); + } + + b.item(new ViewController.OptionItem(R.id.btn_changePhotoGallery, Lang.getString(isChannel ? R.string.SetChannelPhoto : R.string.SetGroupPhoto), + ViewController.OPTION_COLOR_NORMAL, R.drawable.baseline_image_24)); + + final boolean canDelete = chat.photo != null; + showOptions(b.build(), (itemView, id) -> { + if (id == R.id.btn_open) { + if (chat.photo != null && !TD.isFileEmpty(chat.photo.small)) { + MediaViewController.openFromChat(context, chat, delegate); + } + } else if (id == R.id.btn_changePhotoGallery) { + openMediaView(false, false, isChannel ? AvatarPickerMode.CHANNEL : AvatarPickerMode.GROUP, f -> onChatPhotoReceived(f, chat.id), + canDelete ? Lang.getString(isChannel ? R.string.RemoveChannelPhoto : R.string.RemoveGroupPhoto) : null, ColorId.textNegative, () -> showDeletePhotoConfirm(() -> setChatPhoto(chat.id, null))); + } + return true; + }); + } + + public void showMenuForNonCreatedChat (EditHeaderView headerView, boolean isChannel) { + ViewController.Options.Builder b = new ViewController.Options.Builder(); + + b.item(new ViewController.OptionItem(R.id.btn_changePhotoGallery, Lang.getString(isChannel ? R.string.SetChannelPhoto : R.string.SetGroupPhoto), + ViewController.OPTION_COLOR_NORMAL, R.drawable.baseline_image_24)); + + final boolean canDelete = headerView.getImageFile() != null; + showOptions(b.build(), (itemView, id) -> { + if (id == R.id.btn_changePhotoGallery) { + openMediaView(false, false, isChannel ? AvatarPickerMode.CHANNEL : AvatarPickerMode.GROUP, headerView::setPhoto, + canDelete ? Lang.getString(isChannel ? R.string.RemoveChannelPhoto : R.string.RemoveGroupPhoto) : null, ColorId.textNegative, () -> showDeletePhotoConfirm(() -> headerView.setPhoto(null))); + } + return true; + }); + } + + private void showDeletePhotoConfirm (Runnable onConfirm) { + context.showConfirm(Lang.getString(R.string.RemovePhotoConfirm), Lang.getString(R.string.Delete), R.drawable.baseline_delete_24, ViewController.OPTION_COLOR_RED, () -> { + onConfirm.run(); + if (currentMediaLayout != null) { + currentMediaLayout.hide(false); + } + }); + } + + private void showOptions (ViewController.Options options, OptionDelegate delegate) { + if (options.items.length == 1 && options.items[0].id == R.id.btn_changePhotoGallery) { + delegate.onOptionItemPressed(null, R.id.btn_changePhotoGallery); + return; + } + + context.showOptions(options, delegate); + } + + + /* Picker */ + + private MediaLayout currentMediaLayout; + private boolean openingMediaLayout; + + private void openMediaView (boolean ignorePermissionRequest, boolean noMedia, @AvatarPickerMode int avatarPickerMode, RunnableData callback, String customButtonText, @ColorId int customButtonColorId, Runnable customButtonCallback) { + if (openingMediaLayout || currentMediaLayout != null && currentMediaLayout.isVisible()) { + return; + } + + if (!ignorePermissionRequest && context.context().permissions().requestReadExternalStorage(Permissions.ReadType.IMAGES_AND_VIDEOS, grantType -> openMediaView(true, grantType == Permissions.GrantResult.NONE, avatarPickerMode, callback, customButtonText, customButtonColorId, customButtonCallback))) { + return; + } + + final MediaLayout mediaLayout = new MediaLayout(context) { + @Override + public int getCameraButtonOffset () { + return !StringUtils.isEmpty(customButtonText) ? super.getCameraButtonOffset() : 0; + } + }; + mediaLayout.setAvatarPickerMode(avatarPickerMode); + mediaLayout.init(MediaLayout.MODE_AVATAR_PICKER, null); + mediaLayout.setCallback(new MediaLayout.MediaGalleryCallback() { + @Override + public void onSendVideo (ImageGalleryFile file, boolean isFirst) { + if (!isFirst) return; + callback.runWithData(file); + } + + @Override + public void onSendPhoto (ImageGalleryFile file, boolean isFirst) { + if (!isFirst) return; + callback.runWithData(file); + } + }); + if (noMedia) { + mediaLayout.setNoMediaAccess(); + } + + + if (!StringUtils.isEmpty(customButtonText)) { + TextView button = Views.newTextView(context.context(), 16, Theme.getColor(customButtonColorId), Gravity.CENTER, Views.TEXT_FLAG_BOLD | Views.TEXT_FLAG_HORIZONTAL_PADDING); + context.addThemeTextColorListener(button, customButtonColorId); + + button.setText(customButtonText.toUpperCase()); + button.setOnClickListener(v -> customButtonCallback.run()); + + RippleSupport.setSimpleWhiteBackground(button, context); + Views.setClickable(button); + + mediaLayout.setCustomBottomBar(button); + button.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(55f), Gravity.BOTTOM)); + } + + openingMediaLayout = true; + mediaLayout.preload(() -> { + if (context.isFocused() && !context.isDestroyed()) { + mediaLayout.show(); + } + openingMediaLayout = false; + }, 300L); + + currentMediaLayout = mediaLayout; + } + + + /* Callbacks */ // FIXME: video and webp file checks + + private void onProfilePhotoReceived (ImageGalleryFile file, boolean isPublic) { + Media.instance().post(() -> { + final TdApi.InputFileGenerated inputFile = PhotoGenerationInfo.newFile(file); + UI.post(() -> setProfilePhoto(inputFile, isPublic)); + }); + } + + private void onChatPhotoReceived (ImageGalleryFile file, long chatId) { + Media.instance().post(() -> { + final TdApi.InputFileGenerated inputFile = PhotoGenerationInfo.newFile(file); + UI.post(() -> setChatPhoto(chatId, inputFile)); + }); + } + + + /* Activity Result */ // TODO: show editor + + public boolean handleActivityResult (int requestCode, int resultCode, Intent data, int mode, @Nullable TdApi.Chat chat, @Nullable EditHeaderView headerView) { + if (resultCode != Activity.RESULT_OK) { + return false; + } + + if (requestCode == Intents.ACTIVITY_RESULT_IMAGE_CAPTURE) { + File image = Intents.takeLastOutputMedia(); + if (image != null) { + U.addToGallery(image); + handleActivitySetPhoto(image.getPath(), mode, chat, headerView); + } + return true; + } else if (requestCode == Intents.ACTIVITY_RESULT_GALLERY) { + if (data == null) { + UI.showToast("Error", Toast.LENGTH_SHORT); + return false; + } + + final Uri image = data.getData(); + if (image != null) { + String imagePath = U.tryResolveFilePath(image); + if (imagePath != null) { + if (imagePath.endsWith(".webp")) { + UI.showToast("Webp is not supported for profile photos", Toast.LENGTH_LONG); + return false; + } + handleActivitySetPhoto(imagePath, mode, chat, headerView); + } + } + return true; + } + return false; + } + + private void handleActivitySetPhoto (String path, int mode, @Nullable TdApi.Chat chat, @Nullable EditHeaderView headerView) { + if (mode == MODE_PROFILE || mode == MODE_PROFILE_PUBLIC) { + setProfilePhoto(new TdApi.InputFileGenerated(path, SimpleGenerationInfo.makeConversion(path), 0), mode == MODE_PROFILE_PUBLIC); + } else if (mode == MODE_CHAT && chat != null) { + setChatPhoto(chat.id, new TdApi.InputFileGenerated(path, SimpleGenerationInfo.makeConversion(path), 0)); + } else if (mode == MODE_NON_CREATED && headerView != null) { + U.toGalleryFile(new File(path), false, headerView::setPhoto); + } + } + + + /* Setters */ + + private void setProfilePhoto (TdApi.InputFileGenerated inputFile, boolean isPublic) { + UI.showToast(R.string.UploadingPhotoWait, Toast.LENGTH_SHORT); + tdlib.client().send(new TdApi.SetProfilePhoto(new TdApi.InputChatPhotoStatic(inputFile), isPublic), tdlib.profilePhotoHandler()); + } + + private void deleteProfilePhoto (long profilePhotoId) { + tdlib.client().send(new TdApi.DeleteProfilePhoto(profilePhotoId), tdlib.okHandler()); + } + + private void setChatPhoto (long chatId, @Nullable TdApi.InputFileGenerated inputFile) { + if (inputFile != null) { + UI.showToast(R.string.UploadingPhotoWait, Toast.LENGTH_SHORT); + } + tdlib.client().send(new TdApi.SetChatPhoto(chatId, inputFile != null ? new TdApi.InputChatPhotoStatic(inputFile) : null), tdlib.okHandler()); + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibWallpaperManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibWallpaperManager.java index 3cb88eacbb..71a3baec0c 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibWallpaperManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibWallpaperManager.java @@ -137,30 +137,25 @@ public static String extractWallpaperName (String wallpaper) { private final SparseArrayCompat> backgrounds = new SparseArrayCompat<>(); private void fetchBackgrounds (boolean forDarkTheme) { - tdlib.client().send(new TdApi.GetBackgrounds(forDarkTheme), result -> { - switch (result.getConstructor()) { - case TdApi.Backgrounds.CONSTRUCTOR: { - TdApi.Background[] rawBackgrounds = ((TdApi.Backgrounds) result).backgrounds; - List backgrounds = new ArrayList<>(rawBackgrounds.length); - for (TdApi.Background rawBackground : rawBackgrounds) { - backgrounds.add(new TGBackground(tdlib, rawBackground)); - } - List callbacks; - synchronized (this.backgrounds) { - this.backgrounds.put(forDarkTheme ? 1 : 0, backgrounds); - callbacks = ArrayUtils.removeWithKey(this.callbacks, forDarkTheme ? 1 : 0); - } - if (callbacks != null) { - for (Callback callback : callbacks) { - if (callback != null) - callback.onReceiveWallpapers(backgrounds); - } - } - break; + tdlib.send(new TdApi.GetInstalledBackgrounds(forDarkTheme), (result, error) -> { + if (error != null) { + UI.showError(error); + return; + } + List backgrounds = new ArrayList<>(result.backgrounds.length); + for (TdApi.Background rawBackground : result.backgrounds) { + backgrounds.add(new TGBackground(tdlib, rawBackground)); + } + List callbacks; + synchronized (this.backgrounds) { + this.backgrounds.put(forDarkTheme ? 1 : 0, backgrounds); + callbacks = ArrayUtils.removeWithKey(this.callbacks, forDarkTheme ? 1 : 0); + } + if (callbacks != null) { + for (Callback callback : callbacks) { + if (callback != null) + callback.onReceiveWallpapers(backgrounds); } - case TdApi.Error.CONSTRUCTOR: - UI.showError(result); - break; } }); } diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/UserListManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/UserListManager.java index dd1f26ff88..bc3e9419d3 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/UserListManager.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/UserListManager.java @@ -12,15 +12,19 @@ */ package org.thunderdog.challegram.telegram; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import me.vkryl.core.collection.LongSet; +import me.vkryl.core.lambda.RunnableInt; public abstract class UserListManager extends ListManager implements TdlibCache.UserDataChangeListener, TdlibCache.UserStatusChangeListener { public interface ChangeListener extends ListManager.ListChangeListener { } @@ -31,6 +35,9 @@ public UserListManager (Tdlib tdlib, int initialLoadCount, int loadCount, @Nulla super(tdlib, initialLoadCount, loadCount, false, listener); } + @Override + protected abstract TdApi.Function nextLoadFunction (boolean reverse, int itemCount, int loadCount); + @Override protected void subscribeToUpdates() { tdlib.cache().addGlobalUsersListener(this); @@ -42,7 +49,7 @@ protected void unsubscribeFromUpdates() { } @Override - protected Response processResponse (TdApi.Object response, Client.ResultHandler retryHandler, int retryLoadCount, boolean reverse) { + protected final Response processResponse (TdApi.Object response, Client.ResultHandler retryHandler, int retryLoadCount, boolean reverse) { TdApi.Users users = (TdApi.Users) response; long[] rawUserIds = users.userIds; List userIds = new ArrayList<>(rawUserIds.length); @@ -56,50 +63,51 @@ protected Response processResponse (TdApi.Object response, Client.ResultHa // Updates - private void runWithUser (long userId, Runnable act) { - runOnUiThreadIfReady(() -> { - if (userIdsCheck.has(userId)) { - act.run(); - } - }); + private void runWithUser (long userId, RunnableInt act) { + if (userIdsCheck.has(userId)) { + runOnUiThreadIfReady(() -> { + int index = indexOfItem(userId); + if (index != -1) { + // This check is needed, because + // userIdsCheck is modified on TDLib thread, + // but items list is modified on main thread + act.runWithInt(index); + } + }); + } } - public static final int REASON_USER_CHANGED = 0; - public static final int REASON_USER_FULL_CHANGED = 1; - public static final int REASON_STATUS_CHANGED = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + UpdateReason.USER_CHANGED, + UpdateReason.USER_FULL_CHANGED, + UpdateReason.USER_STATUS_CHANGED, + }) + public @interface UpdateReason { + int + USER_CHANGED = 1, + USER_FULL_CHANGED = 2, + USER_STATUS_CHANGED = 3; + } + + private void notifyUserChanged (long userId, @UpdateReason int reason) { + runWithUser(userId, index -> + notifyItemChanged(index, reason) + ); + } @Override public void onUserUpdated (TdApi.User user) { - runWithUser(user.id, () -> { - int index = indexOfItem(user.id); - if (index != -1) { - notifyItemChanged(index, REASON_USER_CHANGED); - } - }); + notifyUserChanged(user.id, UpdateReason.USER_CHANGED); } @Override public void onUserFullUpdated (long userId, TdApi.UserFullInfo userFull) { - runWithUser(userId, () -> { - int index = indexOfItem(userId); - if (index != -1) { - notifyItemChanged(index, REASON_USER_FULL_CHANGED); - } - }); + notifyUserChanged(userId, UpdateReason.USER_FULL_CHANGED); } @Override public void onUserStatusChanged(long userId, TdApi.UserStatus status, boolean uiOnly) { - runWithUser(userId, () -> { - int index = indexOfItem(userId); - if (index != -1) { - notifyItemChanged(index, REASON_STATUS_CHANGED); - } - }); - } - - @Override - public boolean needUserStatusUiUpdates() { - return false; + notifyUserChanged(userId, UpdateReason.USER_STATUS_CHANGED); } } diff --git a/app/src/main/java/org/thunderdog/challegram/theme/TGBackground.java b/app/src/main/java/org/thunderdog/challegram/theme/TGBackground.java index 24c642300a..00655798cd 100644 --- a/app/src/main/java/org/thunderdog/challegram/theme/TGBackground.java +++ b/app/src/main/java/org/thunderdog/challegram/theme/TGBackground.java @@ -140,10 +140,11 @@ private TGBackground (Tdlib tdlib, String name, TdApi.BackgroundType type, boole needImages = true; break; case TdApi.BackgroundTypeFill.CONSTRUCTOR: + case TdApi.BackgroundTypeChatTheme.CONSTRUCTOR: needImages = false; break; default: - throw new UnsupportedOperationException(type.toString()); + throw Td.unsupported(type); } } else { needImages = true; @@ -550,6 +551,7 @@ public void load (Tdlib tdlib) { private static final int BACKGROUND_TYPE_FILL = 1; private static final int BACKGROUND_TYPE_WALLPAPER = 2; private static final int BACKGROUND_TYPE_PATTERN = 3; + private static final int BACKGROUND_TYPE_CHAT_THEME = 4; private static final int FILL_TYPE_SOLID = 1; private static final int FILL_TYPE_GRADIENT = 2; @@ -596,8 +598,10 @@ private static void putFill (SharedPreferences.Editor editor, String key, TdApi. .remove(key + "_rotation_angle"); break; } - default: - throw new UnsupportedOperationException(fill.toString()); + default: { + Td.assertBackgroundFill_6086fe10(); + throw Td.unsupported(fill); + } } } @@ -668,8 +672,16 @@ public void save (int usageIdentifier) { putFill(editor, key, pattern.fill); break; } - default: - throw new UnsupportedOperationException(type.toString()); + case TdApi.BackgroundTypeChatTheme.CONSTRUCTOR: { + TdApi.BackgroundTypeChatTheme chatTheme = (TdApi.BackgroundTypeChatTheme) type; + editor.putInt(key + "_type", BACKGROUND_TYPE_CHAT_THEME); + editor.putString(key + "_theme", chatTheme.themeName); + break; + } + default: { + Td.assertBackgroundType_eedb1e16(); + throw Td.unsupported(type); + } } } else { editor @@ -683,7 +695,8 @@ public void save (int usageIdentifier) { .remove(key + "_color_top") .remove(key + "_color_bottom") .remove(key + "_colors") - .remove(key + "_rotation_angle"); + .remove(key + "_rotation_angle") + .remove(key + "_theme"); } editor.apply(); } @@ -703,6 +716,7 @@ private static TdApi.BackgroundFill restoreFill (SharedPreferences prefs, String } case FILL_TYPE_SOLID: default: { + Td.assertBackgroundFill_6086fe10(); int color = prefs.getInt(key + "_color", 0); return new TdApi.BackgroundFillSolid(color); } @@ -743,6 +757,11 @@ public static TGBackground restore (Tdlib tdlib, int usageIdentifier) { ); break; } + case BACKGROUND_TYPE_CHAT_THEME: { + String themeName = prefs.getString(key + "_theme", null); + type = new TdApi.BackgroundTypeChatTheme(themeName); + break; + } default: return null; } @@ -923,8 +942,11 @@ private static int getPatternColor (TdApi.BackgroundFill fill) { case TdApi.BackgroundFillFreeformGradient.CONSTRUCTOR: { return getPatternColorFreeform((TdApi.BackgroundFillFreeformGradient) fill); } + default: { + Td.assertBackgroundFill_6086fe10(); + throw Td.unsupported(fill); + } } - throw new UnsupportedOperationException(fill.toString()); } private static int getPatternColorFreeform (TdApi.BackgroundFillFreeformGradient gradient) { @@ -1093,8 +1115,11 @@ public static String getNameForFill (TdApi.BackgroundFill fill) { TdApi.BackgroundFillFreeformGradient gradient = (TdApi.BackgroundFillFreeformGradient) fill; return getNameForColor(gradient.colors); } + default: { + Td.assertBackgroundFill_6086fe10(); + throw Td.unsupported(fill); + } } - throw new UnsupportedOperationException(fill.toString()); } public static String getBackgroundForLegacyWallpaperId (int wallpaperId) { diff --git a/app/src/main/java/org/thunderdog/challegram/theme/Theme.java b/app/src/main/java/org/thunderdog/challegram/theme/Theme.java index 6d273db71f..2111ed1733 100644 --- a/app/src/main/java/org/thunderdog/challegram/theme/Theme.java +++ b/app/src/main/java/org/thunderdog/challegram/theme/Theme.java @@ -17,6 +17,7 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.ColorDrawable; @@ -37,11 +38,14 @@ import org.thunderdog.challegram.FillingDrawable; import org.thunderdog.challegram.R; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.CircleDrawable; import org.thunderdog.challegram.support.RectDrawable; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.tool.Drawables; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.unsorted.Settings; @@ -50,6 +54,7 @@ import java.util.ArrayList; import me.vkryl.android.ViewUtils; +import me.vkryl.core.BitwiseUtils; import me.vkryl.core.ColorUtils; public class Theme { @@ -1009,4 +1014,52 @@ public static Drawable getRoundRectSelectorDrawable(int color) { return stateListDrawable; } } + + public static long newComplexColor (boolean isId, int colorValue) { + return BitwiseUtils.mergeLong(isId ? 1 : 0, colorValue); + } + + public static boolean isColorId (long complexColor) { + return BitwiseUtils.splitLongToFirstInt(complexColor) == 1; + } + + public static int extractColorValue (long complexColor) { + return BitwiseUtils.splitLongToSecondInt(complexColor); + } + + public static @ColorInt int toColorInt (long complexColor) { + int value = extractColorValue(complexColor); + if (isColorId(complexColor)) { + return getColor(value); + } else { + return value; + } + } + + public static @ColorId int toColorInt (long complexColor, @ThemeId int themeId) { + int value = extractColorValue(complexColor); + if (isColorId(complexColor)) { + return getColor(value, themeId); + } else { + return value; + } + } + + public static void applyComplexColor (Receiver receiver, long complexColor) { + if (isColorId(complexColor)) { + @ColorId int colorId = extractColorValue(complexColor); + receiver.setThemedPorterDuffColorId(colorId); + } else { + @ColorInt int color = extractColorValue(complexColor); + receiver.setPorterDuffColorFilter(color); + } + } + + public static Paint getComplexPorterDuffPaint (long complexColor, float alpha) { + if (isColorId(complexColor)) { + return PorterDuffPaint.get(extractColorValue(complexColor), alpha); + } else { + return Paints.getPorterDuffPaint(ColorUtils.alphaColor(alpha, extractColorValue(complexColor))); + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/theme/ThemeManager.java b/app/src/main/java/org/thunderdog/challegram/theme/ThemeManager.java index cd5e349830..a3d4511982 100644 --- a/app/src/main/java/org/thunderdog/challegram/theme/ThemeManager.java +++ b/app/src/main/java/org/thunderdog/challegram/theme/ThemeManager.java @@ -333,7 +333,7 @@ public boolean changeGlobalTheme (final Tdlib tdlib, final @NonNull ThemeDelegat final boolean newDark = newTheme.isDark(); if (wasDark != newDark && Settings.instance().getNightMode() != Settings.NIGHT_MODE_NONE) { BaseActivity activity = UI.getUiContext(); - if (activity == null || activity.getActivityState() != UI.STATE_RESUMED) { + if (activity == null || activity.getActivityState() != UI.State.RESUMED) { Settings.instance().setAutoNightMode(Settings.NIGHT_MODE_NONE); } else { AlertDialog.Builder b = new AlertDialog.Builder(activity, Theme.dialogTheme()); @@ -355,7 +355,7 @@ public boolean changeGlobalTheme (final Tdlib tdlib, final @NonNull ThemeDelegat ThemeTemporary tempTheme = new ThemeTemporary(currentTheme(true), newTheme); this._currentTheme = tempTheme; - boolean animated = UI.wasResumedRecently(1000) || UI.getUiState() == UI.STATE_RESUMED; + boolean animated = UI.wasResumedRecently(1000) || UI.getUiState() == UI.State.RESUMED; if (animated) { if (themeAnimator == null) themeAnimator = new FactorAnimator(0, this, AnimatorUtils.DECELERATE_INTERPOLATOR, Config.DEBUG_NAV_ANIM ? 1000 : 200); @@ -408,6 +408,8 @@ public void onFactorChanged (int id, float factor, float fraction, FactorAnimato @Override public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + if (finalFactor != 1f) + return; notifyThemeColorsChanged(false, null); ThemeDelegate currentTheme = currentThemeImpl(false); if (currentTheme instanceof ThemeTemporary) { diff --git a/app/src/main/java/org/thunderdog/challegram/tool/DrawAlgorithms.java b/app/src/main/java/org/thunderdog/challegram/tool/DrawAlgorithms.java index fd50e79152..bdcb12908c 100644 --- a/app/src/main/java/org/thunderdog/challegram/tool/DrawAlgorithms.java +++ b/app/src/main/java/org/thunderdog/challegram/tool/DrawAlgorithms.java @@ -33,16 +33,22 @@ import android.widget.TextView; import androidx.annotation.ColorInt; +import androidx.annotation.Dimension; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.Px; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.U; +import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TGStickerSetInfo; import org.thunderdog.challegram.loader.AvatarReceiver; +import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.loader.Receiver; +import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.mediaview.paint.PaintState; import org.thunderdog.challegram.telegram.TdlibStatusManager; import org.thunderdog.challegram.theme.ColorId; @@ -413,35 +419,37 @@ public static void drawSimplestCheckBox (Canvas c, Receiver receiver, float chec } } - public static float getCounterWidth (float textSize, boolean needBackground, CounterAnimator counter, int drawableWidth) { - return getCounterWidth(textSize, needBackground, counter.getWidth(), drawableWidth); + public static float getCounterWidth (@Dimension(unit = Dimension.DP) float textSize, boolean needBackground, CounterAnimator counter, @Px int drawableWidth, @Px int backgroundPadding) { + return getCounterWidth(textSize, needBackground, counter.getWidth(), drawableWidth, backgroundPadding); } - public static float getCounterWidth (float textSize, boolean needBackground, float counterWidth, int drawableWidth) { + public static float getCounterWidth (@Dimension(unit = Dimension.DP) float textSize, boolean needBackground, @Px float counterWidth, @Px int drawableWidth, @Px int backgroundPadding) { float contentWidth = counterWidth + drawableWidth; if (needBackground) { - return Math.max(Screen.dp(textSize - 2f) * 2, contentWidth + Screen.dp(3f) * 2); + return Math.max(Screen.dp(textSize - 2f) * 2, contentWidth + backgroundPadding * 2); } else { return contentWidth; } } - public static void drawCounter (Canvas c, float cx, float cy, int gravity, CounterAnimator counter, float textSize, boolean needBackground, TextColorSet colorSet, Drawable drawable, int drawableGravity, int drawableColorId, int drawableMargin, float alpha, float drawableAlpha, float scale) { + public static void drawCounter (Canvas c, float cx, float cy, int gravity, CounterAnimator counter, float textSize, float textAlpha, TextColorSet colorSet, float scale) { + drawCounter(c, cx, cy, gravity, counter, textSize, textAlpha, false, false, 0, colorSet, null, Gravity.LEFT, 0, 0, 0f, 0f, scale, null); + } + + public static void drawCounter (Canvas c, float cx, float cy, int gravity, CounterAnimator counter, float textSize, float textAlpha, boolean needBackground, boolean outlineAffectsBackgroundSize, @Px int backgroundPadding, TextColorSet colorSet, @Nullable Drawable drawable, int drawableGravity, int drawableColorId, @Px int drawableMargin, float backgroundAlpha, float drawableAlpha, float scale, @Nullable RectF outputDrawRect) { scale = .6f + .4f * scale; final boolean needScale = scale != 1f; - final float radius, addRadius; + final float drawRectHeight = Screen.dp(textSize - 2f); + final float radius, outlineWidth; if (needBackground) { - radius = Screen.dp(textSize - 2f); - addRadius = Screen.dp(1.5f); + radius = drawRectHeight; + outlineWidth = Screen.dp(1.5f); } else { - radius = addRadius = 0f; + radius = outlineWidth = 0f; } final float contentWidth = counter.getWidth() + (drawable != null ? drawable.getMinimumWidth() + drawableMargin : 0); - final float width = getCounterWidth(textSize, needBackground, counter, drawable != null ? drawable.getMinimumWidth() + drawableMargin : 0); - - final int backgroundColor = colorSet.backgroundColor(false); - final int outlineColor = colorSet.outlineColor(false); + final float width = getCounterWidth(textSize, needBackground, counter, drawable != null ? drawable.getMinimumWidth() + drawableMargin : 0, backgroundPadding); RectF rectF = Paints.getRectF(); switch (gravity) { @@ -463,26 +471,61 @@ public static void drawCounter (Canvas c, float cx, float cy, int gravity, Count c.scale(scale, scale, rectF.centerX(), rectF.centerY()); } - if (needBackground) { - boolean needOutline = Color.alpha(outlineColor) > 0 && addRadius > 0; + if (outputDrawRect != null) { + outputDrawRect.set(rectF.left, cy - drawRectHeight, rectF.right, cy + drawRectHeight); + // c.drawRect(outputDrawRect, Paints.strokeSmallPaint(0xFF00FF00)); + } + + if (needBackground && backgroundAlpha > 0f) { + final int outlineColor = colorSet.outlineColor(false); + final int fillingColor = colorSet.backgroundColor(false); + boolean haveFilling = Color.alpha(fillingColor) > 0; + boolean haveOutline = Color.alpha(outlineColor) > 0 && outlineWidth > 0; + float fillingRadius = outlineAffectsBackgroundSize || !haveOutline ? radius : radius - outlineWidth; + float outlineRadius = fillingRadius + outlineWidth * 0.5f; if (rectF.width() == rectF.height()) { - if (needOutline) { - c.drawCircle(cx, cy, radius + addRadius, Paints.fillingPaint(ColorUtils.alphaColor(alpha, outlineColor))); + if (haveOutline) { + if (outlineColor == fillingColor) { + c.drawCircle(cx, cy, fillingRadius + outlineWidth, Paints.fillingPaint(ColorUtils.alphaColor(backgroundAlpha, fillingColor))); + } else if (Color.alpha(outlineColor) == 0xFF && Color.alpha(fillingColor) == 0xFF && backgroundAlpha == 1f) { + c.drawCircle(cx, cy, fillingRadius + outlineWidth, Paints.fillingPaint(outlineColor)); + c.drawCircle(cx, cy, fillingRadius, Paints.fillingPaint(fillingColor)); + } else { + if (haveFilling) { + c.drawCircle(cx, cy, fillingRadius, Paints.fillingPaint(ColorUtils.alphaColor(backgroundAlpha, fillingColor))); + } + c.drawCircle(cx, cy, outlineRadius, Paints.getCounterOutlinePaint(outlineWidth, ColorUtils.alphaColor(backgroundAlpha, outlineColor))); + } + } else if (haveFilling) { + c.drawCircle(cx, cy, fillingRadius, Paints.fillingPaint(ColorUtils.alphaColor(backgroundAlpha, fillingColor))); } - c.drawCircle(cx, cy, radius, Paints.fillingPaint(ColorUtils.alphaColor(alpha, backgroundColor))); } else { - if (needOutline) { - rectF.left -= addRadius; - rectF.top -= addRadius; - rectF.right += addRadius; - rectF.bottom += addRadius; - c.drawRoundRect(rectF, radius + addRadius, radius + addRadius, Paints.fillingPaint(ColorUtils.alphaColor(alpha, outlineColor))); - rectF.left += addRadius; - rectF.top += addRadius; - rectF.right -= addRadius; - rectF.bottom -= addRadius; + if (haveOutline) { + if (outlineColor == fillingColor) { + if (outlineAffectsBackgroundSize) { + rectF.inset(-outlineWidth, -outlineWidth); + } + c.drawRoundRect(rectF, fillingRadius + outlineWidth, fillingRadius + outlineWidth, Paints.fillingPaint(ColorUtils.alphaColor(backgroundAlpha, fillingColor))); + } else if (Color.alpha(outlineColor) == 0xFF && Color.alpha(fillingColor) == 0xFF && backgroundAlpha == 1f) { + if (outlineAffectsBackgroundSize) { + rectF.inset(-outlineWidth, -outlineWidth); + } + c.drawRoundRect(rectF, fillingRadius + outlineWidth, fillingRadius + outlineWidth, Paints.fillingPaint(outlineColor)); + rectF.inset(outlineWidth, outlineWidth); + c.drawRoundRect(rectF, fillingRadius, fillingRadius, Paints.fillingPaint(fillingColor)); + } else { + if (!outlineAffectsBackgroundSize) { + rectF.inset(outlineWidth, outlineWidth); + } + if (haveFilling) { + c.drawRoundRect(rectF, fillingRadius, fillingRadius, Paints.fillingPaint(ColorUtils.alphaColor(backgroundAlpha, fillingColor))); + } + rectF.inset(-outlineWidth * 0.5f, -outlineWidth * 0.5f); + c.drawRoundRect(rectF, outlineRadius, outlineRadius, Paints.getCounterOutlinePaint(outlineWidth, ColorUtils.alphaColor(backgroundAlpha, outlineColor))); + } + } else if (haveFilling) { + c.drawRoundRect(rectF, fillingRadius, fillingRadius, Paints.fillingPaint(ColorUtils.alphaColor(backgroundAlpha, fillingColor))); } - c.drawRoundRect(rectF, radius, radius, Paints.fillingPaint(ColorUtils.alphaColor(alpha, backgroundColor))); } } @@ -510,7 +553,7 @@ public static void drawCounter (Canvas c, float cx, float cy, int gravity, Count int textStartX = Math.round(startX + entry.getRectF().left); int textEndX = textStartX + entry.item.getWidth(); int startY = Math.round(cy - entry.item.getHeight() / 2f + entry.item.getHeight() * .8f * entry.item.getVerticalPosition()); - entry.item.text.draw(c, textStartX, textEndX, 0, startY, colorSet, alpha * entry.getVisibility() * (1f - Math.abs(entry.item.getVerticalPosition()))); + entry.item.text.draw(c, textStartX, textEndX, 0, startY, colorSet, textAlpha * entry.getVisibility() * (1f - Math.abs(entry.item.getVerticalPosition()))); } if (needScale) { @@ -556,10 +599,10 @@ public static void drawScaledBitmap (View view, Canvas c, Bitmap bitmap, int rot final int viewWidth = view.getMeasuredWidth(); final int viewHeight = view.getMeasuredHeight(); - drawScaledBitmap(viewWidth, viewHeight, c, bitmap, rotation, null); + drawScaledBitmap(viewWidth, viewHeight, c, bitmap, rotation, 0f, 0f, null); } - public static void drawScaledBitmap (final int viewWidth, final int viewHeight, Canvas c, Bitmap bitmap, int rotation, @Nullable PaintState paintState) { + public static void drawScaledBitmap (final int viewWidth, final int viewHeight, Canvas c, Bitmap bitmap, int rotation, float mirrorHorizontallyFactor, float mirrorVerticallyFactor, @Nullable PaintState paintState) { if (bitmap != null && !bitmap.isRecycled()) { int bitmapWidth, bitmapHeight; @@ -570,11 +613,14 @@ public static void drawScaledBitmap (final int viewWidth, final int viewHeight, float scaleX = (float) viewHeight / (float) bitmapWidth; float scaleY = (float) viewWidth / (float) bitmapHeight; c.save(); - c.scale(scaleX, scaleY, viewWidth / 2, viewHeight / 2); - c.rotate(rotation, viewWidth / 2, viewHeight / 2); + c.scale(scaleX, scaleY, viewWidth / 2f, viewHeight / 2f); + c.rotate(rotation, viewWidth / 2f, viewHeight / 2f); int x = viewWidth / 2 - bitmapWidth / 2; int y = viewHeight / 2 - bitmapHeight / 2; + c.save(); + c.scale(MathUtils.fromTo(1f, -1f, mirrorHorizontallyFactor), MathUtils.fromTo(1f, -1f, mirrorVerticallyFactor), viewWidth / 2f, viewHeight / 2f); c.drawBitmap(bitmap, x, y, Paints.getBitmapPaint()); + c.restore(); if (paintState != null) { c.clipRect(x, y, x + bitmapWidth, y + bitmapHeight); paintState.draw(c, x, y, bitmapWidth, bitmapHeight); @@ -585,12 +631,15 @@ public static void drawScaledBitmap (final int viewWidth, final int viewHeight, if (saved) { c.save(); if (rotation != 0) { - c.rotate(rotation, viewWidth / 2, viewHeight / 2); + c.rotate(rotation, viewWidth / 2f, viewHeight / 2f); } } Rect dst = Paints.getRect(); dst.set(0, 0, viewWidth, viewHeight); + c.save(); + c.scale(MathUtils.fromTo(1f, -1f, mirrorHorizontallyFactor), MathUtils.fromTo(1f, -1f, mirrorVerticallyFactor), viewWidth / 2f, viewHeight / 2f); c.drawBitmap(bitmap, null, dst, Paints.getBitmapPaint()); + c.restore(); if (paintState != null && !paintState.isEmpty()) { c.clipRect(0, 0, viewWidth, viewHeight); paintState.draw(c, 0, 0, viewWidth, viewHeight); @@ -1383,4 +1432,24 @@ public static void drawAnimatedCross (Canvas c, float cx, float cy, float factor } } } + + public static void drawSticker (Canvas c, TGStickerSetInfo info, GifReceiver gifReceiver, ImageReceiver receiver, Path contour) { + if (info != null && info.isAnimated()) { + if (gifReceiver.needPlaceholder()) { + if (receiver.needPlaceholder()) { + receiver.drawPlaceholderContour(c, contour); + } + receiver.draw(c); + } + gifReceiver.draw(c); + } else { + if (receiver.needPlaceholder()) { + receiver.drawPlaceholderContour(c, contour); + } + receiver.draw(c); + } + if (Config.DEBUG_STICKER_OUTLINES) { + receiver.drawPlaceholderContour(c, contour); + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/tool/Emojis.kt b/app/src/main/java/org/thunderdog/challegram/tool/Emojis.kt index 9da6aeb4f0..e9677644fa 100644 --- a/app/src/main/java/org/thunderdog/challegram/tool/Emojis.kt +++ b/app/src/main/java/org/thunderdog/challegram/tool/Emojis.kt @@ -4,6 +4,8 @@ package org.thunderdog.challegram.tool import me.vkryl.annotation.Autogenerated +const val MAX_EMOJI_LENGTH = 14 + @Autogenerated fun colored1dSet () = setOf( "\uD83E\uDD32" /* 🤲 */, "\uD83D\uDC50" /* 👐 */, diff --git a/app/src/main/java/org/thunderdog/challegram/tool/Intents.java b/app/src/main/java/org/thunderdog/challegram/tool/Intents.java index 593a44f13c..b523d47811 100644 --- a/app/src/main/java/org/thunderdog/challegram/tool/Intents.java +++ b/app/src/main/java/org/thunderdog/challegram/tool/Intents.java @@ -260,23 +260,29 @@ public static void sendEmail (String emailAddress, String subject, String text, } } - public static void openLink (String url) { - if (!StringUtils.isEmpty(url)) { - Uri uri = Strings.wrapHttps(url); - if (uri != null && !openLink(uri)) { - String scheme = uri.getScheme(); - if (Strings.isValidLink(scheme) && scheme.contains("/")) { - openLink("http://" + uri); - } - } + public static boolean openLink (String url) { + if (StringUtils.isEmpty(url)) { + return false; + } + Uri uri = Strings.wrapHttps(url); + if (uri == null) { + return false; + } + if (openLink(uri)) { + return true; } + String scheme = uri.getScheme(); + if (Strings.isValidLink(scheme) && scheme.contains("/")) { + return openLink("http://" + uri); + } + return false; } private static boolean openLink (Uri uri) { if (uri != null) { try { BaseActivity context = UI.getUiContext(); - if (UI.getUiState() == UI.STATE_RESUMED && openInAppBrowser(context, uri, false)) { + if (UI.getUiState() == UI.State.RESUMED && openInAppBrowser(context, uri, false)) { return true; } diff --git a/app/src/main/java/org/thunderdog/challegram/tool/Invalidator.java b/app/src/main/java/org/thunderdog/challegram/tool/Invalidator.java index 2ffc854f89..54dacddf32 100644 --- a/app/src/main/java/org/thunderdog/challegram/tool/Invalidator.java +++ b/app/src/main/java/org/thunderdog/challegram/tool/Invalidator.java @@ -129,7 +129,7 @@ public void onFullnessStateChanged (ReferenceList list, boolean isFull) { } private void checkLooping () { - boolean isLooping = this.isFull && context.getActivityState() == UI.STATE_RESUMED; + boolean isLooping = this.isFull && context.getActivityState() == UI.State.RESUMED; if (this.isLooping != isLooping) { this.isLooping = isLooping; if (isLooping) { diff --git a/app/src/main/java/org/thunderdog/challegram/tool/Paints.java b/app/src/main/java/org/thunderdog/challegram/tool/Paints.java index 91455f2518..2c19750933 100644 --- a/app/src/main/java/org/thunderdog/challegram/tool/Paints.java +++ b/app/src/main/java/org/thunderdog/challegram/tool/Paints.java @@ -27,10 +27,12 @@ import android.os.Looper; import android.text.TextPaint; +import androidx.annotation.ColorInt; import androidx.annotation.Nullable; +import androidx.annotation.Px; +import androidx.annotation.UiThread; import androidx.collection.SparseArrayCompat; -import org.thunderdog.challegram.R; import org.thunderdog.challegram.U; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; @@ -41,6 +43,7 @@ import java.lang.ref.SoftReference; +import me.vkryl.core.MathUtils; import me.vkryl.core.util.LocalVar; public class Paints { @@ -467,6 +470,27 @@ public static Paint getInlineButtonOuterPaint () { return buttonOuterPaint; } + private static @Nullable Paint counterOutlinePaint; + private static @Px float lastCounterOutlineWidth; + private static @ColorInt int lastCounterOutlineColor; + + public static Paint getCounterOutlinePaint (@Px float width, @ColorInt int color) { + if (counterOutlinePaint == null) { + counterOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + counterOutlinePaint.setStyle(Paint.Style.STROKE); + counterOutlinePaint.setColor(lastCounterOutlineColor = color); + counterOutlinePaint.setStrokeWidth(lastCounterOutlineWidth = width); + } else { + if (lastCounterOutlineColor != color) { + counterOutlinePaint.setColor(lastCounterOutlineColor = color); + } + if (lastCounterOutlineWidth != width) { + counterOutlinePaint.setStrokeWidth(lastCounterOutlineWidth = width); + } + } + return counterOutlinePaint; + } + private static Paint unreadSeparationPaint, inlineIconPDPaint3, @@ -637,9 +661,14 @@ public static Paint getInlineBubbleIconPaint (int color) { return inlineIconPDPaint3; } + public static Paint whitePorterDuffPaint () { + return PorterDuffPaint.get(ColorId.white); + } + + @Deprecated public static Paint getPorterDuffPaint (int color) { if (color == 0xffffffff) { - return PorterDuffPaint.get(ColorId.white); + return whitePorterDuffPaint(); } PorterDuffColorFilter filter = getColorFilter(color); @@ -718,6 +747,12 @@ public static RectF getRectF () { return rectF; } + public static RectF getRectF (float left, float top, float right, float bottom) { + RectF rect = getRectF(); + rect.set(left, top, right, bottom); + return rect; + } + private static Path path; public static Path getPath () { @@ -851,7 +886,7 @@ public static Paint getSrcInPaint (int color) { return srcInPaint; } - private static Paint bitmapPaint; + private static Paint bitmapPaint, bitmapPaint2; public static Paint getBitmapPaint () { if (bitmapPaint == null) { @@ -864,6 +899,20 @@ public static Paint getBitmapPaint () { return bitmapPaint; } + @UiThread + public static Paint bitmapPaint () { + return bitmapPaint(1f); + } + + @UiThread + public static Paint bitmapPaint (float alpha) { + if (bitmapPaint2 == null) { + bitmapPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG); + } + bitmapPaint2.setAlpha(Math.round(255f * MathUtils.clamp(alpha))); + return bitmapPaint2; + } + private static TextPaint emojiPaint; public static TextPaint emojiPaint () { diff --git a/app/src/main/java/org/thunderdog/challegram/tool/Strings.java b/app/src/main/java/org/thunderdog/challegram/tool/Strings.java index bcdc507222..3fd039352e 100644 --- a/app/src/main/java/org/thunderdog/challegram/tool/Strings.java +++ b/app/src/main/java/org/thunderdog/challegram/tool/Strings.java @@ -347,14 +347,37 @@ public static String vcardEscape (String text) { return b.toString(); } + public static Uri forceProtocol (String url, String protocol) { + if (StringUtils.isEmpty(url)) + return null; + try { + Uri uri = Uri.parse(url); + String scheme = uri.getScheme(); + if (StringUtils.isEmpty(scheme)) { + return Uri.parse(protocol + "://" + url); + } else if (!scheme.equals(protocol)) { + return uri.buildUpon().scheme(protocol).build(); + } else { + return uri; + } + } catch (Throwable t) { + Log.e("Unable to parse uri: %s", t, url); + return null; + } + } + public static Uri wrapHttps (String url) { + return wrapProtocol(url, "https"); + } + + public static Uri wrapProtocol (String url, String defaultProtocol) { if (StringUtils.isEmpty(url)) return null; try { Uri uri = Uri.parse(url); String scheme = uri.getScheme(); if (StringUtils.isEmpty(scheme)) { - return Uri.parse("https://" + url); + return Uri.parse(defaultProtocol + "://" + url); } else if (!scheme.toLowerCase().equals(scheme)) { return uri.buildUpon().scheme(scheme.toLowerCase()).build(); } else { @@ -467,7 +490,7 @@ public static boolean findWord (String text, String word) { @Deprecated public static CharSequence highlightWords (String text, String highlight, int startIndex, @Nullable char[] special) { - return highlightWords(text, highlight, startIndex, special, 0); + return highlightWords(text, highlight, startIndex, special, ThemeId.NONE); } @Deprecated @@ -617,7 +640,7 @@ public static boolean isValidLink (String in) { return false; } TdApi.TextEntity[] entities = Td.findEntities(in); - return entities != null && entities.length == 1 && entities[0].offset == 0 && entities[0].length == in.length() && entities[0].type.getConstructor() == TdApi.TextEntityTypeUrl.CONSTRUCTOR; + return entities != null && entities.length == 1 && entities[0].offset == 0 && entities[0].length == in.length() && Td.isUrl(entities[0].type); } public static boolean isValidEmail (String in) { @@ -625,7 +648,7 @@ public static boolean isValidEmail (String in) { return false; } TdApi.TextEntity[] entities = Td.findEntities(in); - if (entities != null && entities.length == 1 && entities[0].offset == 0 && entities[0].length == in.length() && entities[0].type.getConstructor() == TdApi.TextEntityTypeEmailAddress.CONSTRUCTOR) { + if (entities != null && entities.length == 1 && entities[0].offset == 0 && entities[0].length == in.length() && Td.isEmailAddress(entities[0].type)) { return true; } try { @@ -683,7 +706,7 @@ public static String getNumber (String input) { } public static CharSequence replaceBoldTokens (final String input) { - return replaceBoldTokens(input, 0); + return replaceBoldTokens(input, ColorId.NONE); } public static CharSequence replaceBoldTokens (final String input, @ColorId int colorId) { @@ -1001,6 +1024,10 @@ public static CharSequence setSpanColorId (CharSequence str, @ColorId int colorI return str; } + public static CharSequence buildMarkdown (TdlibDelegate context, CharSequence text) { + return buildMarkdown(context, text, null); + } + public static CharSequence buildMarkdown (TdlibDelegate context, CharSequence text, @Nullable CustomTypefaceSpan.OnClickListener onClickListener) { if (text == null) return null; diff --git a/app/src/main/java/org/thunderdog/challegram/tool/TGMimeType.java b/app/src/main/java/org/thunderdog/challegram/tool/TGMimeType.java index 648dde02b3..2296f62a73 100644 --- a/app/src/main/java/org/thunderdog/challegram/tool/TGMimeType.java +++ b/app/src/main/java/org/thunderdog/challegram/tool/TGMimeType.java @@ -29,13 +29,12 @@ public class TGMimeType { } if (BuildConfig.THEME_FILE_EXTENSION.equals(extension)) return "text/plain"; - TdApi.Object object = Client.execute(new TdApi.GetFileMimeType("file." + extension)); - if (object != null && object.getConstructor() == TdApi.Text.CONSTRUCTOR) { - TdApi.Text text = (TdApi.Text) object; - if (!StringUtils.isEmpty(text.text)) { - return text.text; + try { + TdApi.Text mimeType = Client.execute(new TdApi.GetFileMimeType("file." + extension)); + if (!StringUtils.isEmpty(mimeType.text)) { + return mimeType.text; } - } + } catch (Client.ExecutionException ignored) { } if ("heic".equals(extension)) { return "image/heic"; } @@ -70,13 +69,12 @@ public class TGMimeType { if (StringUtils.isEmpty(mimeType)) { return null; } - TdApi.Object object = Client.execute(new TdApi.GetFileExtension(mimeType)); - if (object != null && object.getConstructor() == TdApi.Text.CONSTRUCTOR) { - TdApi.Text text = (TdApi.Text) object; - if (!StringUtils.isEmpty(text.text)) { - return text.text; + try { + TdApi.Text extension = Client.execute(new TdApi.GetFileExtension(mimeType)); + if (!StringUtils.isEmpty(extension.text)) { + return extension.text; } - } + } catch (Client.ExecutionException ignored) { } if ("image/heic".equals(mimeType)) { return "heic"; } diff --git a/app/src/main/java/org/thunderdog/challegram/tool/UI.java b/app/src/main/java/org/thunderdog/challegram/tool/UI.java index a29686fc5d..bcb2b4a404 100644 --- a/app/src/main/java/org/thunderdog/challegram/tool/UI.java +++ b/app/src/main/java/org/thunderdog/challegram/tool/UI.java @@ -23,13 +23,17 @@ import android.os.Build; import android.os.CancellationSignal; import android.os.Handler; +import android.os.LocaleList; import android.os.Looper; import android.text.format.DateFormat; import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; import android.widget.Toast; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; @@ -38,6 +42,7 @@ import org.thunderdog.challegram.BaseActivity; import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; +import org.thunderdog.challegram.U; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.config.Device; import org.thunderdog.challegram.core.Lang; @@ -57,13 +62,17 @@ import org.thunderdog.challegram.util.Unlockable; import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Set; import me.vkryl.android.DeviceUtils; +import me.vkryl.android.LocaleUtils; import me.vkryl.android.SdkVersion; import me.vkryl.android.ViewUtils; import me.vkryl.android.util.InvalidateDelegate; @@ -72,16 +81,19 @@ import me.vkryl.core.reference.ReferenceList; public class UI { - public static final int STATE_UNKNOWN = -1; - public static final int STATE_RESUMED = 0; - public static final int STATE_PAUSED = 1; - public static final int STATE_DESTROYED = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + State.UNKNOWN, State.RESUMED, State.PAUSED, State.DESTROYED + }) + public @interface State { + int UNKNOWN = -1, RESUMED = 0, PAUSED = 1, DESTROYED = 2; + } private static Context appContext; private static WeakReference uiContext; private static UIHandler _appHandler; private static Handler _progressHandler; - private static int uiState = STATE_UNKNOWN; + private static int uiState = State.UNKNOWN; private static Boolean isTablet; @@ -230,7 +242,7 @@ public static void removeStateListener (StateListener listener) { public static boolean setUiState (BaseActivity activity, int state) { WeakReference foundKey = null; - boolean foreground = state == UI.STATE_RESUMED; + boolean foreground = state == State.RESUMED; if (resumeStates == null) { if (foreground) { resumeStates = new HashMap<>(); @@ -259,12 +271,12 @@ public static boolean setUiState (BaseActivity activity, int state) { } } } - if (state == UI.STATE_DESTROYED) { + if (state == State.DESTROYED) { if (foundKey != null) { resumeStates.remove(foundKey); } } else { - Boolean value = state == UI.STATE_RESUMED; + Boolean value = state == State.RESUMED; if (foundKey != null) { resumeStates.put(foundKey, value); } else { @@ -274,18 +286,18 @@ public static boolean setUiState (BaseActivity activity, int state) { resumeStates.put(new WeakReference<>(activity), value); } } - return setUiState(foreground ? UI.STATE_RESUMED : UI.STATE_PAUSED); + return setUiState(foreground ? State.RESUMED : State.PAUSED); } private static boolean setUiState (int state) { if (uiState != state) { - if ((state == STATE_PAUSED || state == STATE_DESTROYED) && uiState == STATE_RESUMED) { + if ((state == State.PAUSED || state == State.DESTROYED) && uiState == State.RESUMED) { lastResumeTime = System.currentTimeMillis(); } boolean called = false; - if (uiState == STATE_RESUMED || state == STATE_RESUMED) { - called = TdlibManager.instance().watchDog().onBackgroundStateChanged(state != STATE_RESUMED); + if (uiState == State.RESUMED || state == State.RESUMED) { + called = TdlibManager.instance().watchDog().onBackgroundStateChanged(state != State.RESUMED); } uiState = state; @@ -299,7 +311,7 @@ private static boolean setUiState (int state) { } public static boolean wasResumedRecently (long resumeTimeLimitMs) { - return uiState == STATE_RESUMED || getResumeDiff() <= resumeTimeLimitMs; + return uiState == State.RESUMED || getResumeDiff() <= resumeTimeLimitMs; } public static UIHandler getAppHandler () { @@ -328,7 +340,7 @@ public static void clearContext (BaseActivity context) { public static boolean isValid (BaseActivity activity) { if (activity != null) { - if (activity.getActivityState() == STATE_DESTROYED) { + if (activity.getActivityState() == State.DESTROYED) { return false; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { @@ -388,7 +400,7 @@ public static int getUiState () { } public static boolean isResumed () { - return uiState == STATE_RESUMED; + return uiState == State.RESUMED; } public static void forceVibrateError (View view) { @@ -465,10 +477,6 @@ public static void showError (TdApi.Object obj) { } } - public static void showWeird (TdApi.Object response, Class> function, Class... objects) { - Log.unexpectedTdlibResponse(response, function, objects); - } - public static void showApiLevelWarning (int apiLevel) { getAppHandler().showToast(Lang.getString(R.string.AndroidVersionWarning, SdkVersion.getPrettyName(apiLevel), SdkVersion.getPrettyVersionCode(apiLevel)), Toast.LENGTH_LONG); } @@ -759,4 +767,100 @@ public static void setSoftInputMode (BaseActivity context, int inputMode) { context.getWindow().setSoftInputMode(inputMode); } } + + // todo: move to other place? + + private static String toLanguageCode (InputMethodSubtype ims) { + if (ims != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + String languageTag = ims.getLanguageTag(); + if (!StringUtils.isEmpty(languageTag)) { + return languageTag; + } + } + String locale = ims.getLocale(); + if (!StringUtils.isEmpty(locale)) { + Locale l = U.getDisplayLocaleOfSubtypeLocale(locale); + if (l != null) { + return LocaleUtils.toBcp47Language(l); + } + } + } + return null; + } + + @Nullable + public static String[] getInputLanguages () { + final List inputLanguages = new ArrayList<>(); + InputMethodManager imm = (InputMethodManager) UI.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + String inputLanguageCode = null; + try { + inputLanguageCode = toLanguageCode(imm.getCurrentInputMethodSubtype()); + } catch (Throwable ignored) { } + if (StringUtils.isEmpty(inputLanguageCode)) { + try { + inputLanguageCode = toLanguageCode(imm.getLastInputMethodSubtype()); + } catch (Throwable ignored) { } + } + if (!StringUtils.isEmpty(inputLanguageCode)) { + inputLanguages.add(inputLanguageCode); + } + + /*if (Strings.isEmpty(inputLanguageCode)) { + try { + String id = android.provider.Settings.Secure.getString( + UI.getAppContext().getContentResolver(), + android.provider.Settings.Secure.DEFAULT_INPUT_METHOD + ); + if (!Strings.isEmpty(id)) { + List list = imm.getInputMethodList(); + lookup: + for (InputMethodInfo info : list) { + if (id.equals(info.getId())) { + List subtypes = imm.getEnabledInputMethodSubtypeList(info, true); + for (InputMethodSubtype subtype : subtypes) { + String languageCode = toLanguageCode(subtype); + if (!Strings.isEmpty(languageCode)) { + inputLanguageCode = languageCode; + break lookup; + } + } + } + } + } + } catch (Throwable ignored) { } + } + if (Strings.isEmpty(inputLanguageCode) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + try { + LocaleList localeList = ((InputView) callback).getImeHintLocales(); + if (localeList != null) { + for (int i = 0; i < localeList.size(); i++) { + inputLanguageCode = U.toBcp47Language(localeList.get(i)); + if (!Strings.isEmpty(inputLanguageCode)) + break; + } + } + } catch (Throwable ignored) { } + }*/ + } + if (inputLanguages.isEmpty()) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + LocaleList locales = Resources.getSystem().getConfiguration().getLocales(); + for (int i = 0; i < locales.size(); i++) { + String code = LocaleUtils.toBcp47Language(locales.get(i)); + if (!StringUtils.isEmpty(code) && !inputLanguages.contains(code)) + inputLanguages.add(code); + } + } else { + String code = LocaleUtils.toBcp47Language(Resources.getSystem().getConfiguration().locale); + if (!StringUtils.isEmpty(code)) { + inputLanguages.add(code); + } + } + } catch (Throwable ignored) { } + } + return inputLanguages.isEmpty() ? null : inputLanguages.toArray(new String[0]); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/tool/UIHandler.java b/app/src/main/java/org/thunderdog/challegram/tool/UIHandler.java index bb4273f058..28aed93672 100644 --- a/app/src/main/java/org/thunderdog/challegram/tool/UIHandler.java +++ b/app/src/main/java/org/thunderdog/challegram/tool/UIHandler.java @@ -15,7 +15,6 @@ package org.thunderdog.challegram.tool; import android.content.Context; -import android.os.Build; import android.os.Handler; import android.os.Message; import android.view.Gravity; @@ -30,6 +29,7 @@ import org.thunderdog.challegram.MainActivity; import org.thunderdog.challegram.R; import org.thunderdog.challegram.U; +import org.thunderdog.challegram.config.Device; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TGAudio; import org.thunderdog.challegram.navigation.NavigationController; @@ -358,7 +358,7 @@ public void handleMessage (Message msg) { case COPY_TEXT: { try { U.copyText((CharSequence) msg.obj); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 && msg.arg1 != 0) { + if (!Device.HAS_BUILTIN_CLIPBOARD_TOASTS && msg.arg1 != 0) { showCustomToast(msg.arg1, Toast.LENGTH_SHORT, 0); } } catch (Throwable t) { diff --git a/app/src/main/java/org/thunderdog/challegram/tool/Views.java b/app/src/main/java/org/thunderdog/challegram/tool/Views.java index 95c835cdad..0823652113 100644 --- a/app/src/main/java/org/thunderdog/challegram/tool/Views.java +++ b/app/src/main/java/org/thunderdog/challegram/tool/Views.java @@ -25,6 +25,7 @@ import android.graphics.Typeface; import android.os.Build; import android.text.Editable; +import android.text.Layout; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; @@ -48,12 +49,14 @@ import org.drinkmore.Tracer; import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; +import org.thunderdog.challegram.U; import org.thunderdog.challegram.core.DiffMatchPatch; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.ViewTranslator; -import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.util.TextSelection; import org.thunderdog.challegram.util.WebViewHolder; import org.thunderdog.challegram.util.text.Text; @@ -862,6 +865,16 @@ public static void setRightMargin (View view, int margin) { } } + public static int getTopMargin (View view) { + if (view != null) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof ViewGroup.MarginLayoutParams) { + return ((ViewGroup.MarginLayoutParams) params).topMargin; + } + } + return 0; + } + public static int getBottomMargin (View view) { if (view != null) { ViewGroup.LayoutParams params = view.getLayoutParams(); @@ -927,4 +940,64 @@ public static void updateMediumTypeface (TextView view, CharSequence text) { view.setPaintFlags(newFlags); } } + + public static int getRecyclerFirstElementTop (RecyclerView recyclerView) { + return getRecyclerViewElementTop(recyclerView, 0, 0); + } + + public static int getRecyclerFirstElementTop (RecyclerView recyclerView, int valueIfPositionNotFound) { + return getRecyclerViewElementTop(recyclerView, 0, valueIfPositionNotFound); + } + + public static int getRecyclerViewElementTop (RecyclerView recyclerView, int position) { + return getRecyclerViewElementTop(recyclerView, position, 0); + } + + public static int getRecyclerViewElementTop (RecyclerView recyclerView, int position, int valueIfPositionNotFound) { + if (recyclerView == null || recyclerView.getLayoutManager() == null) { + return valueIfPositionNotFound; + } + + View view = recyclerView.getLayoutManager().findViewByPosition(position); + if (view != null) { + return view.getTop() + recyclerView.getTop(); + } + + return valueIfPositionNotFound; + } + + public static void getCharacterCoordinates(TextView textView, int offset, int[] coordinates) { + if (coordinates.length != 2) + throw new IllegalArgumentException(); + coordinates[0] = coordinates[1] = 0; + + Editable editable = textView.getEditableText(); + Layout layout = textView.getLayout(); + + if (layout != null) { + int line = layout.getLineForOffset(offset); + int lineStartOffset = layout.getLineStart(line); + int xPos = (int) U.measureEmojiText(editable.subSequence(lineStartOffset, offset), layout.getPaint()); + int yPos = layout.getLineBaseline(line) - textView.getScrollY(); + coordinates[0] = xPos; + coordinates[1] = yPos; + } + } + + public static int findFirstCompletelyVisibleItemPositionWithOffset (LinearLayoutManager manager, int topOffset) { + int i = manager.findFirstCompletelyVisibleItemPosition(); + if (i == -1) { + i = manager.findFirstVisibleItemPosition(); + } + + View v = manager.findViewByPosition(i); + while (v != null) { + if (v.getTop() >= topOffset) { + return i; + } + v = manager.findViewByPosition(++i); + } + + return -1; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/BottomSheetViewController.java b/app/src/main/java/org/thunderdog/challegram/ui/BottomSheetViewController.java index 4ce7bc0958..3af2d93235 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/BottomSheetViewController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/BottomSheetViewController.java @@ -4,6 +4,8 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; +import android.os.Build; +import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -28,6 +30,7 @@ import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.widget.LickView; import org.thunderdog.challegram.widget.PopupLayout; @@ -53,7 +56,7 @@ protected int getHideByScrollBorder () { } protected final int getHeaderHeight (boolean withOffset) { - return getHeaderHeight() + (withOffset ? HeaderView.getTopOffset(): 0); + return getHeaderHeight() + (withOffset ? HeaderView.getTopOffset() : 0); } protected final int getContentMinHeight () { @@ -79,7 +82,7 @@ protected View onCreateView (Context context) { @Override protected void onDraw (Canvas canvas) { if (headerView != null) { - canvas.drawRect(0, headerView.getTranslationY(), getMeasuredWidth(), getMeasuredHeight(), Paints.fillingPaint(Theme.getColor(getBackgroundColorId()))); + canvas.drawRect(0, headerTranslationY, getMeasuredWidth(), getMeasuredHeight(), Paints.fillingPaint(Theme.getColor(getBackgroundColorId()))); } super.onDraw(canvas); } @@ -88,7 +91,7 @@ protected void onDraw (Canvas canvas) { protected boolean drawChild (Canvas canvas, View child, long drawingTime) { if (child == pagerInFrameLayoutFix && headerView != null) { canvas.save(); - canvas.clipRect(0, headerView.getTranslationY() + HeaderView.getTopOffset(), getMeasuredWidth(), getMeasuredHeight()); + canvas.clipRect(0, headerTranslationY + HeaderView.getTopOffset(), getMeasuredWidth(), getMeasuredHeight()); boolean result = super.drawChild(canvas, child, drawingTime); canvas.restore(); return result; @@ -110,12 +113,14 @@ protected boolean drawChild (Canvas canvas, View child, long drawingTime) { wrapView = new FrameLayoutFix(context) { @Override public boolean onInterceptTouchEvent (MotionEvent e) { - return (e.getAction() == MotionEvent.ACTION_DOWN && headerView != null && e.getY() < headerView.getTranslationY()) || super.onInterceptTouchEvent(e); + boolean b = (e.getAction() == MotionEvent.ACTION_DOWN && headerView != null && e.getY() < (getTopEdge() + HeaderView.getTopOffset())); + return b || super.onInterceptTouchEvent(e); } @Override public boolean onTouchEvent (MotionEvent e) { - return !(e.getAction() == MotionEvent.ACTION_DOWN && headerView != null && e.getY() < headerView.getTranslationY()) && super.onTouchEvent(e); + boolean b = (e.getAction() == MotionEvent.ACTION_DOWN && headerView != null && e.getY() < (getTopEdge() + HeaderView.getTopOffset())); + return b && super.onTouchEvent(e); } private int oldHeight = -1; @@ -127,7 +132,10 @@ protected void onLayout (boolean changed, int left, int top, int right, int bott final int height = getTargetHeight(); if (height != oldHeight) { invalidateAllItemDecorations(); + boolean disallowKeyboardHide = isDisallowKeyboardHideOnPageScrolled(); + setDisallowKeyboardHideOnPageScrolled(true); onPageScrolled(currentMediaPosition, currentPositionOffset, 0); + setDisallowKeyboardHideOnPageScrolled(disallowKeyboardHide); oldHeight = height; } }); @@ -218,6 +226,9 @@ protected void invalidateCachedPosition () { protected float lastHeaderPosition; protected void checkHeaderPosition (RecyclerView recyclerView) { + lastHeaderPosition = Math.max(Views.getRecyclerFirstElementTop(recyclerView), 0) + HeaderView.getTopOffset(); + setHeaderPosition(lastHeaderPosition); + /* View view = null; if (recyclerView != null) { view = recyclerView.getLayoutManager().findViewByPosition(0); @@ -230,13 +241,18 @@ protected void checkHeaderPosition (RecyclerView recyclerView) { if (headerView != null) { setHeaderPosition(lastHeaderPosition = top); } + */ } protected int getTargetHeight () { - return Screen.currentHeight() - + (context.isKeyboardVisible() ? Keyboard.getSize() : 0) - - (Screen.needsKeyboardPadding(context) ? Screen.getNavigationBarFrameDifference() : 0) - + (context.isKeyboardVisible() && Device.NEED_ADD_KEYBOARD_SIZE ? Screen.getNavigationBarHeight() : 0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return Screen.currentHeight() + + (context.isKeyboardVisible() ? Keyboard.getSize() : 0) + - (Screen.needsKeyboardPadding(context) ? Screen.getNavigationBarFrameDifference() : 0) + + (context.isKeyboardVisible() && Device.NEED_ADD_KEYBOARD_SIZE ? Screen.getNavigationBarHeight() : 0); + } else { + return Screen.currentHeight(); + } } protected void invalidateAllItemDecorations () { @@ -252,19 +268,22 @@ protected void invalidateAllItemDecorations () { } protected float headerBackgroundFactor; + protected float headerTranslationY; protected void setHeaderPosition (float y) { y = Math.max(y, HeaderView.getTopOffset()); + headerTranslationY = y; + float realHeaderOffset = y; if (headerView != null) { - headerView.setTranslationY(y); + headerView.setTranslationY(realHeaderOffset); } - fixView.setTranslationY(y); + fixView.setTranslationY(realHeaderOffset); contentView.invalidate(); fixView.invalidate(); if (lickView != null) { final int topOffset = HeaderView.getTopOffset(); final float top = y - topOffset; - lickView.setTranslationY(top); + lickView.setTranslationY(realHeaderOffset - topOffset); float factor = top > topOffset ? 0f : 1f - ((float) top / (float) topOffset); lickView.setFactor(factor); onUpdateLickViewFactor(factor); @@ -278,12 +297,12 @@ protected void setHeaderBackgroundFactor (float headerBackgroundFactor) { } protected int getTopEdge () { - return Math.max(0, (int) ((headerView != null ? headerView.getTranslationY(): 0) - HeaderView.getTopOffset())); + return Math.max(0, (int) ((headerView != null ? headerTranslationY : 0) - HeaderView.getTopOffset())); } @Override public boolean shouldTouchOutside (float x, float y) { - return headerView != null && y < headerView.getTranslationY() - HeaderView.getSize(true); + return headerView != null && y < headerTranslationY - HeaderView.getSize(true); } @Override @@ -301,7 +320,7 @@ public int maxItemsScrollY () { public void checkContentScrollY (BottomSheetBaseControllerPage c) { int maxScrollY = maxItemsScrollYOffset(); - int scrollY = (int) (getContentOffset() - (headerView != null ? headerView.getTranslationY(): 0) + HeaderView.getTopOffset()); //(); + int scrollY = (int) (getContentOffset() - (headerView != null ? headerTranslationY : 0) + HeaderView.getTopOffset()); //(); if (c != null) { c.ensureMaxScrollY(scrollY, maxScrollY); } @@ -425,7 +444,7 @@ protected PopupLayout getPopupLayout () { private @Nullable LickView lickView; protected float getLickViewFactor () { - return lickView != null ? lickView.getFactor(): 0; + return lickView != null ? lickView.getFactor() : 0; } protected void onUpdateLickViewFactor (float factor) { @@ -517,7 +536,7 @@ public void getItemOffsets (@NonNull Rect outRect, @NonNull View view, RecyclerV } outRect.set( - 0, page.needTopDecorationOffsets(parent) ? Math.max(top, 0): 0, + 0, page.needTopDecorationOffsets(parent) ? Math.max(top, 0) : 0, 0, page.needBottomDecorationOffsets(parent) ? Math.max(0, bottom) : 0); } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/CallController.java b/app/src/main/java/org/thunderdog/challegram/ui/CallController.java index 8694a84bbc..b66eb10196 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/CallController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/CallController.java @@ -52,6 +52,7 @@ import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibCache; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.DrawAlgorithms; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Fonts; @@ -61,6 +62,7 @@ import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.util.CustomTypefaceSpan; import org.thunderdog.challegram.util.EmojiStatusHelper; +import org.thunderdog.challegram.util.RateLimiter; import org.thunderdog.challegram.util.text.TextColorSetOverride; import org.thunderdog.challegram.util.text.TextColorSets; import org.thunderdog.challegram.voip.gui.CallSettings; @@ -637,14 +639,14 @@ private void setTexts () { if (emojiStatusHelper != null) { this.emojiStatusHelper.updateEmoji(tdlib, user, new TextColorSetOverride(TextColorSets.Regular.NORMAL) { @Override - public int emojiStatusColor () { - return 0xffffffff; + public long mediaTextComplexColor () { + return Theme.newComplexColor(true, ColorId.white); } }, R.drawable.baseline_premium_star_28, 32); } if (nameView != null) { this.nameView.setText(TD.getUserName(user)); - this.nameView.setPadding(0, 0, user != null && user.isPremium ? emojiStatusHelper.getWidth(Screen.dp(7)): 0, 0); + this.nameView.setPadding(0, 0, user != null && user.isPremium ? emojiStatusHelper.getWidth(Screen.dp(7)) : 0, 0); this.nameView.requestLayout(); } if (emojiViewHint != null) @@ -777,9 +779,8 @@ public void onFactorChanged (int id, float factor, float fraction, FactorAnimato public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { switch (id) { case ANIMATOR_FLASH_ID: { - flashAnimator.forceFactor(0f); - if (isFlashing) { - flashAnimator.animateTo(1f); + if (finalFactor == 1f) { + flashLimiter.run(); } break; } @@ -891,6 +892,15 @@ public void onCallSettingsChanged (final int callId, final CallSettings settings public static final long CALL_FLASH_DURATION = 1100; public static final long CALL_FLASH_DELAY = 650l; + private final RateLimiter flashLimiter = new RateLimiter(() -> { + if (isFlashing) { + flashAnimator.forceFactor(0f); + if (isFlashing) { + flashAnimator.animateTo(1f); + } + } + }, 100l, null); + private void setFlashing (boolean isFlashing) { if (this.isFlashing != isFlashing) { this.isFlashing = isFlashing; diff --git a/app/src/main/java/org/thunderdog/challegram/ui/CallListController.java b/app/src/main/java/org/thunderdog/challegram/ui/CallListController.java index cb2f65cda1..8e54a5353a 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/CallListController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/CallListController.java @@ -18,6 +18,7 @@ import android.view.View; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -34,13 +35,14 @@ import org.thunderdog.challegram.data.TGFoundChat; import org.thunderdog.challegram.navigation.SettingsWrapBuilder; import org.thunderdog.challegram.navigation.ViewController; -import org.thunderdog.challegram.telegram.DayChangeListener; +import org.thunderdog.challegram.telegram.DateChangeListener; import org.thunderdog.challegram.telegram.MessageListener; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibOptionListener; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.util.StringList; import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.widget.BaseView; @@ -57,12 +59,13 @@ import me.vkryl.android.AnimatorUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.collection.IntList; +import me.vkryl.td.Td; public class CallListController extends RecyclerViewController implements View.OnClickListener, Client.ResultHandler, MessageListener, - DayChangeListener, + DateChangeListener, View.OnLongClickListener, BaseView.ActionListProvider, TdlibOptionListener { public CallListController (Context context, Tdlib tdlib) { @@ -74,6 +77,11 @@ public int getId () { return R.id.controller_call_list; } + @Override + public CharSequence getName () { + return Lang.getString(R.string.Calls); + } + private SettingsAdapter adapter; @Override @@ -131,17 +139,56 @@ public int getItemCount () { buildCells(); recyclerView.setAdapter(adapter); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + private float lastY; + private float lastShowY; + @Override - public void onScrolled (RecyclerView recyclerView, int dx, int dy) { + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { if (messages != null && ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition() >= adapter.getItems().size() - 5) { loadMore(); } + if (Settings.instance().chatFoldersEnabled() && getParentOrSelf() == CallListController.this) { + lastY += dy; + if (dy < 0 && lastShowY - lastY >= Screen.getTouchSlop()) { + setDoneVisible(true, true); + lastShowY = lastY; + } else if (lastY - lastShowY > Screen.getTouchSlopBig()) { + setDoneVisible(false, true); + lastShowY = lastY; + } + if (Math.abs(lastY - lastShowY) > Screen.getTouchSlopBig()) { + lastY = 0; + lastShowY = 0; + } + } } }); tdlib.client().send(new TdApi.SearchCallMessages(null, Screen.calculateLoadingItems(Screen.dp(72f), 20), false), this); tdlib.client().send(new TdApi.GetTopChats(new TdApi.TopChatCategoryCalls(), 30), this); - tdlib.listeners().subscribeForAnyUpdates(this); + tdlib.listeners().subscribeForGlobalUpdates(this); + tdlib.context().dateManager().addListener(this); + } + + @Override + public boolean needAsynchronousAnimation () { + return messages == null; + } + + @Override + public void onPrepareToShow () { + super.onPrepareToShow(); + if (Settings.instance().chatFoldersEnabled() && getParentOrSelf() == this) { + setDoneIcon(R.drawable.baseline_phone_24); + setDoneVisible(true, false); + } + } + + @Override + protected void onDoneClick () { + ContactsController c = new ContactsController(context, tdlib); + c.initWithMode(ContactsController.MODE_CALL); + navigateTo(c); } @Override @@ -500,6 +547,7 @@ private void setMessages (TdApi.FoundMessages messages) { if (StringUtils.isEmpty(nextOffset)) { endReached = true; } + executeScheduledAnimation(); } private boolean isLoadingMore; @@ -548,7 +596,8 @@ private void buildSections () { @Override public void destroy () { super.destroy(); - tdlib.listeners().unsubscribeFromAnyUpdates(this); + tdlib.listeners().unsubscribeFromGlobalUpdates(this); + tdlib.context().dateManager().removeListener(this); } @Override @@ -696,7 +745,7 @@ public void onResult (final TdApi.Object object) { } private static boolean filter (TdApi.Message message) { - return message.content.getConstructor() == TdApi.MessageCall.CONSTRUCTOR && message.sendingState == null && message.schedulingState == null; + return Td.isCall(message.content) && message.sendingState == null && message.schedulingState == null; } @Override @@ -733,7 +782,7 @@ public void __onNewMessages (final TdApi.Message[] messages) { }*/ @Override - public void onDayChanged () { + public void onDateChanged () { buildSections(); } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ChatFolderIconSelector.java b/app/src/main/java/org/thunderdog/challegram/ui/ChatFolderIconSelector.java new file mode 100644 index 0000000000..c78bb75ad5 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/ui/ChatFolderIconSelector.java @@ -0,0 +1,169 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 16/01/2023 + */ +package org.thunderdog.challegram.ui; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.support.RippleSupport; +import org.thunderdog.challegram.support.ViewSupport; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Drawables; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.widget.PopupLayout; +import org.thunderdog.challegram.widget.ShadowView; + +import java.util.ArrayList; +import java.util.List; + +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.StringUtils; + +public class ChatFolderIconSelector { + + private final Context context; + private final Delegate delegate; + private final View popupView; + private final GridLayoutManager layoutManager; + + private PopupLayout popupLayout; + + public ChatFolderIconSelector (ViewController owner, Delegate delegate) { + this.context = owner.context(); + this.delegate = delegate; + + List items = new ArrayList<>(TD.ICON_NAMES.length); + for (String iconName : TD.ICON_NAMES) { + items.add(new ListItem(ListItem.TYPE_CUSTOM_SINGLE, 0, TD.iconByName(iconName, 0), 0).setStringValue(iconName)); + } + SettingsAdapter popupAdapter = new SettingsAdapter(owner, null, owner) { + @Override + protected SettingHolder initCustom (ViewGroup parent) { + ImageView imageView = new ImageView(parent.getContext()) { + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + int size = MeasureSpec.getSize(widthMeasureSpec); + setMeasuredDimension(size, size); + } + }; + imageView.setScaleType(ImageView.ScaleType.CENTER); + imageView.setColorFilter(Theme.getColor(ColorId.icon)); + owner.addThemeFilterListener(imageView, ColorId.icon); + Views.setClickable(imageView); + imageView.setOnClickListener(v -> { + ListItem item = (ListItem) imageView.getTag(); + String iconName = item.getStringValue(); + TdApi.ChatFolderIcon icon = !StringUtils.isEmpty(iconName) ? new TdApi.ChatFolderIcon(iconName) : null; + delegate.onIconClick(icon); + hide(/* animated */ true); + }); + RippleSupport.setTransparentSelector(imageView); + return new SettingHolder(imageView); + } + + @Override + protected void setCustom (ListItem item, SettingHolder holder, int position) { + ImageView imageView = (ImageView) holder.itemView; + int iconResource = item.getIconResource(); + if (iconResource != 0) { + imageView.setImageDrawable(Drawables.get(imageView.getResources(), iconResource)); + } else { + imageView.setImageDrawable(null); + } + } + }; + + layoutManager = new GridLayoutManager(context, computeSpanCount(Screen.currentWidth())); + popupAdapter.setItems(items, false); + RecyclerView recyclerView = new RecyclerView(context); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setAdapter(popupAdapter); + ViewSupport.setThemedBackground(recyclerView, ColorId.background); + + ShadowView shadowView = new ShadowView(context); + shadowView.setSimpleTopShadow(true); + owner.addThemeInvalidateListener(shadowView); + + FrameLayoutFix popupView = new FrameLayoutFix(context); + popupView.addView(shadowView, FrameLayoutFix.newParams(MATCH_PARENT, Screen.dp(7f), Gravity.TOP)); + popupView.addView(recyclerView, FrameLayoutFix.newParams(MATCH_PARENT, WRAP_CONTENT, Gravity.TOP, 0, Screen.dp(7f), 0, 0)); + popupView.setLayoutParams(FrameLayoutFix.newParams(MATCH_PARENT, WRAP_CONTENT, Gravity.BOTTOM)); + this.popupView = popupView; + } + + public void show () { + int itemCount = layoutManager.getItemCount(); + int spanCount = layoutManager.getSpanCount(); + int rowCount = (itemCount + spanCount - 1) / spanCount; + int popupHeight = rowCount * (Screen.currentWidth() / spanCount) + Screen.dp(7f); + + popupLayout = new PopupLayout(context) { + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + int popupWidth = getDefaultSize(Screen.currentWidth(), widthMeasureSpec); + layoutManager.setSpanCount(computeSpanCount(popupWidth)); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + }; + popupLayout.init(true); + popupLayout.setShowListener(popup -> delegate.onShow()); + popupLayout.setDismissListener(popup -> delegate.onDismiss()); + popupLayout.setHideKeyboard(); + popupLayout.setNeedRootInsets(); + popupLayout.showSimplePopupView(popupView, popupHeight); + } + + public void hide (boolean animated) { + if (popupLayout != null) { + popupLayout.hideWindow(animated); + popupLayout = null; + } + } + + private int computeSpanCount (int width) { + int itemSize = Screen.dp(56f); + return Math.max(width / itemSize, 3); + } + + public interface Delegate { + void onIconClick (@Nullable TdApi.ChatFolderIcon icon); + + default void onShow () {} + + default void onDismiss () {} + } + + public static ChatFolderIconSelector show (ViewController owner, Delegate delegate) { + ChatFolderIconSelector selector = new ChatFolderIconSelector(owner, delegate); + selector.show(); + return selector; + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ChatLinkMembersController.java b/app/src/main/java/org/thunderdog/challegram/ui/ChatLinkMembersController.java index 9904250060..aac40d9b78 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/ChatLinkMembersController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/ChatLinkMembersController.java @@ -178,7 +178,7 @@ public void onScrolled (RecyclerView recyclerView, int dx, int dy) { } }); - tdlib.listeners().subscribeForAnyUpdates(this); + tdlib.listeners().subscribeForGlobalUpdates(this); } private void openRightsScreen (long userId) { @@ -201,7 +201,7 @@ private void openRightsScreen (long userId) { @Override public void destroy () { super.destroy(); - tdlib.listeners().unsubscribeFromAnyUpdates(this); + tdlib.listeners().unsubscribeFromGlobalUpdates(this); } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ChatLinksController.java b/app/src/main/java/org/thunderdog/challegram/ui/ChatLinksController.java index f49656a519..ed01acd28f 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/ChatLinksController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/ChatLinksController.java @@ -299,7 +299,7 @@ public void onRemove (RecyclerView.ViewHolder viewHolder) { inviteLinksRevoked.remove(link); smOnRevokedLinkDeleted(link); notifyParentIfPossible(); - tdlib.client().send(new TdApi.DeleteRevokedChatInviteLink(chatId, link.inviteLink), null); + tdlib.client().send(new TdApi.DeleteRevokedChatInviteLink(chatId, link.inviteLink), tdlib.okHandler()); } return true; @@ -358,7 +358,7 @@ public void onClick (View v) { inviteLinksRevoked.clear(); smOnRevokedLinksCleared(firstLink, lastLink); notifyParentIfPossible(); - tdlib.client().send(new TdApi.DeleteAllRevokedChatInviteLinks(chatId, adminUserId), null); + tdlib.client().send(new TdApi.DeleteAllRevokedChatInviteLinks(chatId, adminUserId), tdlib.okHandler()); } return true; diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ChatStatisticsController.java b/app/src/main/java/org/thunderdog/challegram/ui/ChatStatisticsController.java index 6dddef7ab8..d7391d02c3 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/ChatStatisticsController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/ChatStatisticsController.java @@ -21,7 +21,6 @@ import androidx.annotation.IdRes; import androidx.annotation.StringRes; -import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; import org.thunderdog.challegram.charts.Chart; @@ -37,6 +36,7 @@ import org.thunderdog.challegram.support.RippleSupport; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; @@ -140,6 +140,8 @@ public void onClick (View v) { } } + private TdlibMessageViewer.Viewport messageViewport; + @Override protected void onCreateView (Context context, CustomRecyclerView recyclerView) { final long chatId = getArgumentsStrict().chatId; @@ -151,6 +153,7 @@ protected void onCreateView (Context context, CustomRecyclerView recyclerView) { headerCell.setSubtitle(R.string.Stats); this.headerCell = headerCell; + this.messageViewport = tdlib.messageViewer().createViewport(new TdApi.MessageSourceSearch(), this); adapter = new SettingsAdapter(this) { @Override protected void setSeparatorOptions (ListItem item, int position, SeparatorView separatorView) { @@ -184,7 +187,7 @@ protected void setMessagePreview (ListItem item, int position, MessagePreviewVie } RippleSupport.setSimpleWhiteBackground(previewView); - previewView.setMessage(container.message, null, statString.toString(), false); + previewView.setMessage(container.message, null, statString.toString(), MessagePreviewView.Options.NONE); previewView.setContentInset(Screen.dp(8)); } @@ -218,23 +221,32 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda } }; recyclerView.setAdapter(adapter); - tdlib.client().send(new TdApi.GetChatStatistics(chatId, Theme.isDark()), result -> { - switch (result.getConstructor()) { - case TdApi.ChatStatisticsChannel.CONSTRUCTOR: - runOnUiThreadOptional(() -> { - setStatistics((TdApi.ChatStatisticsChannel) result); - }); - break; - case TdApi.ChatStatisticsSupergroup.CONSTRUCTOR: - runOnUiThreadOptional(() -> { - setStatistics((TdApi.ChatStatisticsSupergroup) result); - }); - break; - case TdApi.Error.CONSTRUCTOR: - UI.showError(result); - break; + tdlib.ui().attachViewportToRecyclerView(messageViewport, recyclerView); + tdlib.send(new TdApi.GetChatStatistics(chatId, Theme.isDark()), (chatStatistics, error) -> runOnUiThreadOptional(() -> { + if (error != null) { + UI.showError(error); + } else { + switch (chatStatistics.getConstructor()) { + case TdApi.ChatStatisticsChannel.CONSTRUCTOR: + setStatistics((TdApi.ChatStatisticsChannel) chatStatistics); + break; + case TdApi.ChatStatisticsSupergroup.CONSTRUCTOR: + setStatistics((TdApi.ChatStatisticsSupergroup) chatStatistics); + break; + default: + Td.assertChatStatistics_6744ad70(); + throw Td.unsupported(chatStatistics); + } } - }); + })); + } + + @Override + public void destroy () { + super.destroy(); + if (messageViewport != null) { + messageViewport.performDestroy(); + } } @Override @@ -407,13 +419,13 @@ private void setStatistics (TdApi.ChatStatisticsChannel statistics) { items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_members, 0, R.string.StatsMembers, false).setData(statistics.memberCount)); items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_notifications, 0, R.string.StatsNotifications, false).setDoubleValue(statistics.enabledNotificationsPercentage)); - if (!Td.isEmpty(statistics.meanViewCount)) { + if (!Td.isEmpty(statistics.meanMessageViewCount)) { items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_view, 0, R.string.StatsViews, false).setData(statistics.meanViewCount)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_view, 0, R.string.StatsViews, false).setData(statistics.meanMessageViewCount)); } - if (!Td.isEmpty(statistics.meanShareCount)) { + if (!Td.isEmpty(statistics.meanMessageShareCount)) { items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_share, 0, R.string.StatsShares, false).setData(statistics.meanShareCount)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_share, 0, R.string.StatsShares, false).setData(statistics.meanMessageShareCount)); } items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, Lang.getStringBold(R.string.StatsRange, Lang.getDateRange(statistics.period.startDate, statistics.period.endDate, TimeUnit.SECONDS, true)), false)); @@ -435,15 +447,20 @@ private void setStatistics (TdApi.ChatStatisticsChannel statistics) { ); setCharts(items, charts, () -> { - if (statistics.recentMessageInteractions.length > 0) { - setRecentMessageInteractions(statistics.period, statistics.recentMessageInteractions, R.string.StatsRecentPosts); + TdApi.ChatStatisticsInteractionInfo[] recentMessageInteractions = ArrayUtils.filter( + Arrays.asList(statistics.recentInteractions), + item -> item.objectType.getConstructor() == TdApi.ChatStatisticsObjectTypeMessage.CONSTRUCTOR + ).toArray(new TdApi.ChatStatisticsInteractionInfo[0]); + // TODO: stories + if (recentMessageInteractions.length > 0) { + setRecentMessageInteractions(statistics.period, recentMessageInteractions, R.string.StatsRecentPosts); } else { executeScheduledAnimation(); } }); } - private void setRecentMessageInteractions (TdApi.DateRange range, TdApi.ChatStatisticsMessageInteractionInfo[] interactions, @StringRes int header) { + private void setRecentMessageInteractions (TdApi.DateRange range, TdApi.ChatStatisticsInteractionInfo[] interactions, @StringRes int header) { loadInteractionMessages(interactions, () -> { int currentSize = adapter.getItems().size(); adapter.getItems().add(new ListItem(ListItem.TYPE_CHART_HEADER_DETACHED).setData(new MiniChart(header, range))); @@ -460,18 +477,20 @@ private void setRecentMessageInteractions (TdApi.DateRange range, TdApi.ChatStat }); } - private void loadInteractionMessages (TdApi.ChatStatisticsMessageInteractionInfo[] interactions, Runnable onMessagesLoaded) { + private void loadInteractionMessages (TdApi.ChatStatisticsInteractionInfo[] interactions, Runnable onMessagesLoaded) { AtomicInteger remaining = new AtomicInteger(interactions.length); - Client.ResultHandler handler = result -> { - if (result.getConstructor() == TdApi.Message.CONSTRUCTOR) { - TdApi.Message message = (TdApi.Message) result; - + Runnable after = () -> { + if (remaining.decrementAndGet() == 0) { + runOnUiThreadOptional(onMessagesLoaded); + } + }; + Tdlib.ResultHandler messageHandler = (message, error) -> { + if (message != null) { if (message.mediaAlbumId != 0) { if (!interactionMessageAlbums.containsKey(message.mediaAlbumId)) { interactionMessageAlbums.put(message.mediaAlbumId, new ArrayList<>()); } - interactionMessageAlbums.get(message.mediaAlbumId).add(message); } @@ -486,13 +505,26 @@ private void loadInteractionMessages (TdApi.ChatStatisticsMessageInteractionInfo } } - if (remaining.decrementAndGet() == 0) { - runOnUiThreadOptional(onMessagesLoaded); - } + after.run(); }; - for (TdApi.ChatStatisticsMessageInteractionInfo interaction : interactions) { - tdlib.client().send(new TdApi.GetMessageLocally(getArgumentsStrict().chatId, interaction.messageId), handler); + for (TdApi.ChatStatisticsInteractionInfo interaction : interactions) { + switch (interaction.objectType.getConstructor()) { + case TdApi.ChatStatisticsObjectTypeMessage.CONSTRUCTOR: { + TdApi.ChatStatisticsObjectTypeMessage objectType = (TdApi.ChatStatisticsObjectTypeMessage) interaction.objectType; + tdlib.send(new TdApi.GetMessageLocally(getArgumentsStrict().chatId, objectType.messageId), messageHandler); + break; + } + case TdApi.ChatStatisticsObjectTypeStory.CONSTRUCTOR: { + TdApi.ChatStatisticsObjectTypeStory objectType = (TdApi.ChatStatisticsObjectTypeStory) interaction.objectType; + // TODO: stories + after.run(); + break; + } + default: + Td.assertChatStatisticsObjectType_5cb871fe(); + throw Td.unsupported(interaction.objectType); + } } } @@ -564,9 +596,9 @@ public boolean needAsynchronousAnimation () { public static class MessageInteractionInfoContainer { public final TdApi.Message message; - public final TdApi.ChatStatisticsMessageInteractionInfo messageInteractionInfo; + public final TdApi.ChatStatisticsInteractionInfo messageInteractionInfo; - public MessageInteractionInfoContainer (TdApi.Message message, TdApi.ChatStatisticsMessageInteractionInfo messageInteractionInfo) { + public MessageInteractionInfoContainer (TdApi.Message message, TdApi.ChatStatisticsInteractionInfo messageInteractionInfo) { this.message = message; this.messageInteractionInfo = messageInteractionInfo; } @@ -623,10 +655,9 @@ private void openMemberMenu (DoubleTextWrapper content) { IntList icons = new IntList(4); StringList strings = new StringList(4); - tdlib.client().send(new TdApi.GetChatMember(getArgumentsStrict().chatId, content.getSenderId()), result -> { - if (result.getConstructor() != TdApi.ChatMember.CONSTRUCTOR) return; + tdlib.send(new TdApi.GetChatMember(getArgumentsStrict().chatId, content.getSenderId()), (member, error) -> { + if (error != null) return; - TdApi.ChatMember member = (TdApi.ChatMember) result; TdApi.ChatMemberStatus myStatus = tdlib.chatStatus(getArgumentsStrict().chatId); if (myStatus != null) { diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ChatsController.java b/app/src/main/java/org/thunderdog/challegram/ui/ChatsController.java index 2abba74a49..2002ac1fc0 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/ChatsController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/ChatsController.java @@ -33,6 +33,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.UiThread; import androidx.collection.LongSparseArray; import androidx.core.os.CancellationSignal; import androidx.recyclerview.widget.ItemTouchHelper; @@ -74,15 +75,19 @@ import org.thunderdog.challegram.telegram.ConnectionListener; import org.thunderdog.challegram.telegram.ConnectionState; import org.thunderdog.challegram.telegram.CounterChangeListener; +import org.thunderdog.challegram.telegram.DateChangeListener; import org.thunderdog.challegram.telegram.MessageEditListener; import org.thunderdog.challegram.telegram.MessageListener; import org.thunderdog.challegram.telegram.NotificationSettingsListener; import org.thunderdog.challegram.telegram.TGLegacyManager; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibCache; import org.thunderdog.challegram.telegram.TdlibChatList; import org.thunderdog.challegram.telegram.TdlibChatListSlice; import org.thunderdog.challegram.telegram.TdlibContactManager; +import org.thunderdog.challegram.telegram.TdlibCounter; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; import org.thunderdog.challegram.telegram.TdlibSettingsManager; import org.thunderdog.challegram.telegram.TdlibThread; import org.thunderdog.challegram.telegram.TdlibUi; @@ -137,7 +142,7 @@ public class ChatsController extends TelegramViewController { if (newHeight != oldHeight && adapter.hasArchive() && hideArchive && adapter.getItemCount() > 0) { @@ -519,6 +527,7 @@ public void onSwiped (@NonNull RecyclerView.ViewHolder viewHolder, int direction chatsView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); if (filter != null) chatsView.setTotalRes(filter.getTotalStringRes()); + tdlib.ui().attachViewportToRecyclerView(chatsViewport, chatsView); contentView.addView(chatsView); Views.setScrollBarPosition(chatsView); @@ -531,17 +540,28 @@ public void onSwiped (@NonNull RecyclerView.ViewHolder viewHolder, int direction updateNetworkStatus(tdlib.connectionState()); - tdlib.listeners().subscribeForAnyUpdates(this); - tdlib.cache().subscribeToAnyUpdates(this); + tdlib.listeners().subscribeForGlobalUpdates(this); + tdlib.cache().subscribeForGlobalUpdates(this); Settings.instance().addChatListModeListener(this); TGLegacyManager.instance().addEmojiListener(this); + tdlib.context().dateManager().addListener(this); + tdlib.awaitMyUserOrUnauthorizedState(() -> { + executeOnUiThreadOptional(() -> { + myUserLoaded = true; + if (!needAsynchronousAnimation()) { + executeScheduledAnimation(); + } + }); + }); list.initializeList(this, this::displayChats, chatsView.getInitialLoadCount(), () -> runOnUiThreadOptional(() -> { - this.listInitalized = true; + this.listInitialized = true; checkListState(); - executeScheduledAnimation(); + if (!needAsynchronousAnimation()) { + executeScheduledAnimation(); + } }) ); @@ -980,7 +1000,11 @@ private boolean isBaseController () { @Override protected int getMenuId () { - return isBaseController() ? R.id.menu_passcode : R.id.menu_search; + return isBaseController() ? R.id.menu_passcode : isArchiveChatList() ? R.id.menu_archive : R.id.menu_search; + } + + private boolean isArchiveChatList () { + return pickerDelegate == null && filter == null && chatList().getConstructor() == TdApi.ChatListArchive.CONSTRUCTOR; } @Override @@ -1007,6 +1031,9 @@ public void fillMenuItems (int id, HeaderView header, LinearLayout menu) { header.addLockButton(menu); } header.addSearchButton(menu, this); + } else if (id == R.id.menu_archive) { + header.addButton(menu, R.id.menu_btn_settings, R.drawable.baseline_settings_24, 49f, this, getHeaderIconColorId()); + header.addSearchButton(menu, this); } else if (id == R.id.menu_search) { header.addSearchButton(menu, this); } else if (id == R.id.menu_clear) { @@ -1110,6 +1137,11 @@ protected int getSelectMenuId () { public void onMenuItemPressed (int id, View view) { if (id == R.id.menu_btn_search) { openSearchMode(); + } else if (id == R.id.menu_btn_settings) { + if (isArchiveChatList()) { + SettingsArchiveChatListController c = new SettingsArchiveChatListController(context, tdlib); + navigateTo(c); + } } else if (id == R.id.menu_btn_clear) { clearSearchInput(); } else if (id == R.id.menu_btn_more) { @@ -1152,7 +1184,8 @@ public void onMenuItemPressed (int id, View view) { } if (tdlib.canReportChatSpam(chat)) canReportSpam++; - if (tdlib.chatBlocked(chat)) { + TdApi.BlockList blockList = tdlib.chatBlockList(chat); + if (blockList != null && blockList.getConstructor() == TdApi.BlockListMain.CONSTRUCTOR) { canUnblock++; } else { canBlock++; @@ -1171,6 +1204,18 @@ public void onMenuItemPressed (int id, View view) { icons.append(canUnarchive > 0 ? R.drawable.baseline_unarchive_24 : R.drawable.baseline_archive_24); } + if (Settings.instance().chatFoldersEnabled()) { + if (TD.isChatListMain(chatList()) || TD.isChatListArchive(chatList())) { + ids.append(R.id.more_btn_addToFolder); + strings.append(R.string.AddToFolder); + icons.append(R.drawable.templarian_baseline_folder_plus_24); + } else if (TD.isChatListFolder(chatList())) { + ids.append(R.id.more_btn_removeFromFolder); + strings.append(R.string.RemoveFromFolder); + icons.append(R.drawable.templarian_baseline_folder_remove_24); + } + } + if (canMarkAsRead > 0) { ids.append(R.id.more_btn_markAsRead); strings.append(R.string.MarkAsRead); @@ -1515,7 +1560,20 @@ private void bulkDeleteChat (boolean clearHistory) { @Override public void onMoreItemPressed (int id) { - if (id == R.id.more_btn_archiveUnarchive || + if (id == R.id.more_btn_addToFolder) { + long[] selectedChatIds = ArrayUtils.keys(selectedChats); + tdlib.ui().showAddChatsToFolderOptions(this, selectedChatIds, this::onSelectionActionComplete); + // break; + } else if (id == R.id.more_btn_removeFromFolder) { + TdApi.ChatList chatList = chatList(); + if (TD.isChatListFolder(chatList)) { + int chatFolderId = ((TdApi.ChatListFolder) chatList).chatFolderId; + long[] selectedChatIds = ArrayUtils.keys(selectedChats); + tdlib.removeChatsFromChatFolder(chatFolderId, selectedChatIds); + } + onSelectionActionComplete(); + // break; + } else if (id == R.id.more_btn_archiveUnarchive || id == R.id.more_btn_markAsRead || id == R.id.more_btn_markAsUnread || id == R.id.more_btn_report || @@ -1611,7 +1669,7 @@ public void onMoreItemPressed (int id) { if (isUnblock) { for (int i = selectedChats.size() - 1; i >= 0; i--) { long chatId = selectedChats.keyAt(i); - tdlib.blockSender(tdlib.sender(chatId), false, tdlib.okHandler()); + tdlib.unblockSender(tdlib.sender(chatId), tdlib.okHandler()); } } else { showOptions( @@ -1624,7 +1682,8 @@ public void onMoreItemPressed (int id) { if (optionId == R.id.btn_unblockSender || optionId == R.id.btn_blockSender) { for (int i = selectedChats.size() - 1; i >= 0; i--) { long chatId = selectedChats.keyAt(i); - tdlib.blockSender(tdlib.sender(chatId), optionId == R.id.btn_blockSender, tdlib.okHandler(after)); + TdApi.MessageSender sender = tdlib.sender(chatId); + tdlib.blockSender(sender, optionId == R.id.btn_blockSender ? new TdApi.BlockListMain() : null, tdlib.okHandler(after)); } } return true; @@ -1975,7 +2034,7 @@ public ForceTouchView.ActionListener onCreateActions (View v, ForceTouchView.For } context.setTdlib(tdlib); - context.setHeaderAvatar(null, new AvatarPlaceholder.Metadata(ColorId.avatarArchive, R.drawable.baseline_archive_24)); + context.setHeaderAvatar(null, new AvatarPlaceholder.Metadata(tdlib.accentColor(TdlibAccentColor.InternalId.ARCHIVE), R.drawable.baseline_archive_24)); context.setHeader(Lang.getString(R.string.ArchiveTitle), Lang.plural(R.string.xChats, tdlib.getTotalChatsCount(ChatPosition.CHAT_LIST_ARCHIVE))); context.setMaximizeListener((target1, animateToWhenReady, arg) -> { @@ -2238,14 +2297,33 @@ private void prepareNoChats () { if (parentController != null) { parentController.navigateTo(c); } + } else if (viewId == R.id.btn_editFolder) { + int chatFolderId = ((TdApi.ChatListFolder) chatList()).chatFolderId; + tdlib.send(new TdApi.GetChatFolder(chatFolderId), (chatFolder, error) -> runOnUiThreadOptional(() -> { + if (parentController == null) + return; + if (error != null) { + UI.showError(error); + } else { + EditChatFolderController c = new EditChatFolderController(context, tdlib); + c.setArguments(new EditChatFolderController.Arguments(chatFolderId, chatFolder)); + parentController.navigateTo(c); + } + })); } }, this); ArrayList items = new ArrayList<>(5); + TdApi.ChatList chatList = chatList(); if (filter != null) { items.add(new ListItem(ListItem.TYPE_EMPTY, 0, 0, filter.getEmptyStringRes())); - } else if (chatList() instanceof TdApi.ChatListArchive) { + } else if (chatList instanceof TdApi.ChatListArchive) { items.add(new ListItem(ListItem.TYPE_EMPTY, 0, 0, R.string.NoArchive)); + } else if (chatList instanceof TdApi.ChatListFolder) { + items.add(new ListItem(ListItem.TYPE_ICONIZED_EMPTY, R.id.changePhoneText, R.drawable.baseline_folder_96, Lang.getMarkdownString(this, R.string.FolderNoChatsToDisplay))); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_BUTTON, R.id.btn_editFolder, 0, R.string.EditFolder)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); } else if (archiveList != null && archiveList.totalCount() > 0) { items.add(new ListItem(ListItem.TYPE_ICONIZED_EMPTY, R.id.changePhoneText, R.drawable.baseline_archive_96, Lang.getMarkdownString(this, R.string.OpenArchiveHint), false)); items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); @@ -2398,7 +2476,7 @@ public boolean isInitialLoadFinished () { @Override public boolean needAsynchronousAnimation () { - return !listInitalized; + return !listInitialized || !myUserLoaded; } @Override @@ -2478,6 +2556,9 @@ public void destroy () { if (liveLocationHelper != null) { liveLocationHelper.destroy(); } + if (chatsViewport != null) { + chatsViewport.performDestroy(); + } if (archiveList != null) { archiveList.unsubscribeFromUpdates(archiveListListener); } @@ -2489,11 +2570,12 @@ public void destroy () { } Settings.instance().removeChatListModeListener(this); tdlib.settings().removeUserPreferenceChangeListener(this); - tdlib.listeners().unsubscribeFromAnyUpdates(this); - tdlib.cache().unsubscribeFromAnyUpdates(this); + tdlib.listeners().unsubscribeFromGlobalUpdates(this); + tdlib.cache().unsubscribeFromGlobalUpdates(this); list.unsubscribeFromUpdates(this); TGLegacyManager.instance().removeEmojiListener(this); tdlib.contacts().removeListener(this); + tdlib.context().dateManager().removeListener(this); } // Updates @@ -2701,6 +2783,14 @@ public void onMessageSendAcknowledged (final long chatId, final long messageId) }); } + @Override + @UiThread + public void onDateChanged () { + if (!isDestroyed() && chatsView != null) { + chatsView.updateRelativeDate(); + } + } + @Override public void onMessageSendSucceeded (final TdApi.Message message, final long oldMessageId) { runOnUiThreadOptional(() -> { @@ -2749,7 +2839,7 @@ public void onMessagesDeleted (long chatId, long[] messageIds) { // Counter updates @Override - public void onChatCounterChanged (@NonNull TdApi.ChatList chatList, boolean availabilityChanged, int totalCount, int unreadCount, int unreadUnmutedCount) { + public void onChatCounterChanged (@NonNull TdApi.ChatList chatList, TdlibCounter counter, boolean availabilityChanged, int totalCount, int unreadCount, int unreadUnmutedCount) { if (totalCount == 0 && chatList.getConstructor() != TdApi.ChatListMain.CONSTRUCTOR && Td.equalsTo(this.chatList, chatList)) { runOnUiThreadOptional(() -> { if (!isDestroyed() && !isBaseController()) { diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ContactsController.java b/app/src/main/java/org/thunderdog/challegram/ui/ContactsController.java index 197a4936fe..bf4b62c274 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/ContactsController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/ContactsController.java @@ -605,23 +605,13 @@ private void createChannel () { userIds[i] = pickedChats.get(i).getUserId(); } - tdlib.client().send(new TdApi.AddChatMembers(chat.id, userIds), object -> { - switch (object.getConstructor()) { - case TdApi.Ok.CONSTRUCTOR: { - UI.unlock(ContactsController.this); - tdlib.ui().openChat(ContactsController.this, chat, null); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - UI.unlock(ContactsController.this); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.AddChatMembers.class, TdApi.Ok.class); - UI.unlock(ContactsController.this); - break; - } + tdlib.send(new TdApi.AddChatMembers(chat.id, userIds), (ok, error) -> { + if (error != null) { + UI.showError(error); + UI.unlock(ContactsController.this); + } else { + UI.unlock(ContactsController.this); + tdlib.ui().openChat(ContactsController.this, chat, null); } }); } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/CreateChannelController.java b/app/src/main/java/org/thunderdog/challegram/ui/CreateChannelController.java index 62a6f57096..b7d6d6e645 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/CreateChannelController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/CreateChannelController.java @@ -14,7 +14,6 @@ */ package org.thunderdog.challegram.ui; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -30,21 +29,19 @@ import android.widget.LinearLayout; import android.widget.TextView; -import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.core.Lang; -import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.emoji.EmojiFilter; -import org.thunderdog.challegram.filegen.SimpleGenerationInfo; -import org.thunderdog.challegram.loader.ImageFile; +import org.thunderdog.challegram.filegen.PhotoGenerationInfo; +import org.thunderdog.challegram.loader.ImageGalleryFile; import org.thunderdog.challegram.navigation.ActivityResultHandler; import org.thunderdog.challegram.navigation.BackHeaderButton; import org.thunderdog.challegram.navigation.EditHeaderView; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Fonts; @@ -54,7 +51,6 @@ import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.unsorted.Size; import org.thunderdog.challegram.util.CharacterStyleFilter; -import org.thunderdog.challegram.util.OptionDelegate; import org.thunderdog.challegram.v.EditText; import org.thunderdog.challegram.widget.EmojiEditText; import org.thunderdog.challegram.widget.NoScrollTextView; @@ -64,9 +60,12 @@ import me.vkryl.core.StringUtils; import me.vkryl.td.TdConstants; -public class CreateChannelController extends ViewController implements EditHeaderView.ReadyCallback, OptionDelegate, ActivityResultHandler, Client.ResultHandler, TextView.OnEditorActionListener { +public class CreateChannelController extends ViewController implements EditHeaderView.ReadyCallback, ActivityResultHandler, TextView.OnEditorActionListener { + private final TdlibUi.AvatarPickerManager avatarPickerManager; + public CreateChannelController (Context context, Tdlib tdlib) { super(context, tdlib); + avatarPickerManager = new TdlibUi.AvatarPickerManager(this); } private EditText descView; @@ -147,6 +146,9 @@ protected View onCreateView (Context context) { headerCell = new EditHeaderView(context, this); headerCell.setInputOptions(R.string.ChannelName, InputType.TYPE_TEXT_FLAG_CAP_WORDS); + headerCell.setOnPhotoClickListener(() -> { + avatarPickerManager.showMenuForNonCreatedChat(headerCell, true); + }); headerCell.setNextField(R.id.edit_description); headerCell.setReadyCallback(this); setLockFocusView(headerCell.getInputView()); @@ -220,17 +222,9 @@ protected void onFloatingButtonPressed () { createChannel(); } - @Override - public boolean onOptionItemPressed (View optionItemView, int id) { - tdlib.ui().handlePhotoOption(context, id, null, headerCell); - return true; - } - @Override public void onActivityResult (int requestCode, int resultCode, Intent data) { - if (resultCode == Activity.RESULT_OK) { - tdlib.ui().handlePhotoChange(requestCode, data, headerCell); - } + avatarPickerManager.handleActivityResult(requestCode, resultCode, data, TdlibUi.AvatarPickerManager.MODE_NON_CREATED, null, headerCell); } @Override @@ -254,14 +248,6 @@ public String getDescription () { return descView.getText().toString(); } - public String getPhoto () { - return headerCell.getPhoto(); - } - - public ImageFile getImageFile () { - return headerCell.getImageFile(); - } - public void setDescription (String description) { if (description != null) { descView.setText(description); @@ -270,8 +256,7 @@ public void setDescription (String description) { } private boolean isCreating; - private String currentPhoto; - private ImageFile currentImageFile; + private ImageGalleryFile currentImageFile; private void toggleCreating () { isCreating = !isCreating; @@ -291,12 +276,24 @@ public void createChannel () { toggleCreating(); - currentPhoto = getPhoto(); - currentImageFile = getImageFile(); + currentImageFile = headerCell.getImageFile(); - UI.showProgress(Lang.getString(R.string.ProgressCreateChannel), null, 300l); + UI.showProgress(Lang.getString(R.string.ProgressCreateChannel), null, 300L); - tdlib.client().send(new TdApi.CreateNewSupergroupChat(title, false, true, desc, null, 0, false), this); + tdlib.send(new TdApi.CreateNewSupergroupChat(title, false, true, desc, null, 0, false), (remoteChat, error) -> { + UI.hideProgress(); + if (error != null) { + UI.showError(error); + chat = null; + } else { + long chatId = remoteChat.id; + chat = tdlib.chatStrict(chatId); + if (currentImageFile != null) { + tdlib.client().send(new TdApi.SetChatPhoto(chat.id, new TdApi.InputChatPhotoStatic(PhotoGenerationInfo.newFile(currentImageFile))), tdlib.okHandler()); + } + } + UI.post(() -> channelCreated(chat)); + }); } public void channelCreated (TdApi.Chat chat) { @@ -315,35 +312,6 @@ public void channelCreated (TdApi.Chat chat) { private TdApi.Chat chat; - @Override - public void onResult (TdApi.Object object) { - UI.hideProgress(); - switch (object.getConstructor()) { - case TdApi.Ok.CONSTRUCTOR: { - // Do nothing. Photo's been set - return; - } - case TdApi.Chat.CONSTRUCTOR: { - long chatId = TD.getChatId(object); - chat = tdlib.chatStrict(chatId); - if (currentPhoto != null) { - tdlib.client().send(new TdApi.SetChatPhoto(chat.id, new TdApi.InputChatPhotoStatic(new TdApi.InputFileGenerated(currentPhoto, SimpleGenerationInfo.makeConversion(currentPhoto), 0))), this); - } - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - chat = null; - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.CreateNewSupergroupChat.class, TdApi.Ok.class, TdApi.Chat.class, TdApi.Error.class); - return; - } - } - UI.post(() -> channelCreated(chat)); - } - @Override public void destroy () { super.destroy(); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/CreateChannelLinkController.java b/app/src/main/java/org/thunderdog/challegram/ui/CreateChannelLinkController.java index ed47f63ade..3f816cb797 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/CreateChannelLinkController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/CreateChannelLinkController.java @@ -30,9 +30,7 @@ import androidx.annotation.IdRes; import androidx.annotation.StringRes; -import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TD; @@ -63,7 +61,7 @@ import me.vkryl.td.ChatId; import me.vkryl.td.TdConstants; -public class CreateChannelLinkController extends ViewController implements View.OnClickListener, Client.ResultHandler, Unlockable { +public class CreateChannelLinkController extends ViewController implements View.OnClickListener, Unlockable { public static class Args { private TdApi.Chat chat; private ImageFile photo; @@ -312,34 +310,25 @@ private RadioView addOption (Context context, @IdRes int id, boolean isChecked, private void loadInviteLink () { if (!linkRequested) { linkRequested = true; - tdlib.getPrimaryChatInviteLink(chat.id, this); - } - } - - private String inviteLink; - - @Override - public void onResult (TdApi.Object object) { - switch (object.getConstructor()) { - case TdApi.ChatInviteLink.CONSTRUCTOR: { - inviteLink = StringUtils.urlWithoutProtocol(((TdApi.ChatInviteLink) object).inviteLink); - for (String host : TdConstants.TME_HOSTS) { - if (inviteLink.startsWith(host)) { - inviteLink = inviteLink.substring(host.length() + 1); - break; + tdlib.getPrimaryChatInviteLink(chat.id, (chatInviteLink, error) -> { + if (error != null) { + UI.showError(error); + } else { + inviteLink = StringUtils.urlWithoutProtocol(chatInviteLink.inviteLink); + for (String host : TdConstants.TME_HOSTS) { + if (inviteLink.startsWith(host)) { + inviteLink = inviteLink.substring(host.length() + 1); + break; + } } + runOnUiThreadOptional(this::updateLink); } - - UI.post(() -> updateLink()); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; - } + }); } } + private String inviteLink; + @Override public View getCustomHeaderCell () { return headerView; @@ -398,22 +387,12 @@ private void setUsername (String username) { } usernameRequested = true; setEnabled(false); - tdlib.client().send(new TdApi.SetSupergroupUsername(getSupergroupId(), username), object -> { - switch (object.getConstructor()) { - case TdApi.Ok.CONSTRUCTOR: { - UI.post(this::nextStep); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - UI.unlock(CreateChannelLinkController.this); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.SetSupergroupUsername.class, TdApi.Ok.class); - UI.unlock(CreateChannelLinkController.this); - break; - } + tdlib.send(new TdApi.SetSupergroupUsername(getSupergroupId(), username), (ok, error) -> { + if (error != null) { + UI.showError(error); + UI.unlock(CreateChannelLinkController.this); + } else { + UI.post(this::nextStep); } }); } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/CreateGroupController.java b/app/src/main/java/org/thunderdog/challegram/ui/CreateGroupController.java index 7ad4328739..26a814bf7d 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/CreateGroupController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/CreateGroupController.java @@ -14,7 +14,6 @@ */ package org.thunderdog.challegram.ui; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -35,7 +34,8 @@ import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGUser; -import org.thunderdog.challegram.filegen.SimpleGenerationInfo; +import org.thunderdog.challegram.filegen.PhotoGenerationInfo; +import org.thunderdog.challegram.loader.ImageGalleryFile; import org.thunderdog.challegram.navigation.ActivityResultHandler; import org.thunderdog.challegram.navigation.BackHeaderButton; import org.thunderdog.challegram.navigation.EditHeaderView; @@ -44,13 +44,13 @@ import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibCache; +import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Keyboard; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.unsorted.Size; -import org.thunderdog.challegram.util.OptionDelegate; import org.thunderdog.challegram.util.Unlockable; import org.thunderdog.challegram.widget.ListInfoView; @@ -59,12 +59,15 @@ import me.vkryl.android.widget.FrameLayoutFix; import me.vkryl.core.ArrayUtils; -public class CreateGroupController extends ViewController implements EditHeaderView.ReadyCallback, OptionDelegate, Client.ResultHandler, Unlockable, ActivityResultHandler, +public class CreateGroupController extends ViewController implements EditHeaderView.ReadyCallback, Client.ResultHandler, Unlockable, ActivityResultHandler, TdlibCache.UserDataChangeListener, TdlibCache.UserStatusChangeListener { + + private final TdlibUi.AvatarPickerManager avatarPickerManager; private ArrayList members; public CreateGroupController (Context context, Tdlib tdlib) { super(context, tdlib); + avatarPickerManager = new TdlibUi.AvatarPickerManager(this); } public void setMembers (ArrayList members) { @@ -79,6 +82,9 @@ public void setMembers (ArrayList members) { protected View onCreateView (Context context) { headerCell = new EditHeaderView(context, this); headerCell.setInputOptions(R.string.GroupName, InputType.TYPE_TEXT_FLAG_CAP_WORDS); + headerCell.setOnPhotoClickListener(() -> { + avatarPickerManager.showMenuForNonCreatedChat(headerCell, false); + }); headerCell.setImeOptions(EditorInfo.IME_ACTION_DONE); headerCell.setReadyCallback(this); setLockFocusView(headerCell.getInputView()); @@ -334,10 +340,9 @@ public void detachReceiver () { private void onUserClick (TGUser user) { pickedUser = user; - showOptions(null, new int[] {R.id.btn_deleteMember, R.id.btn_cancel}, new String[] {Lang.getString(R.string.GroupDontAdd), Lang.getString(R.string.Cancel)}, new int[] {OPTION_COLOR_RED, OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_remove_circle_24, R.drawable.baseline_cancel_24}); + showOptions(null, new int[] {R.id.btn_deleteMember, R.id.btn_cancel}, new String[] {Lang.getString(R.string.GroupDontAdd), Lang.getString(R.string.Cancel)}, new int[] {OPTION_COLOR_RED, OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_remove_circle_24, R.drawable.baseline_cancel_24}, this::onOptionItemPressed); } - @Override public boolean onOptionItemPressed (View optionItemView, int id) { if (id == R.id.btn_deleteMember) { if (pickedUser != null) { @@ -356,17 +361,13 @@ public boolean onOptionItemPressed (View optionItemView, int id) { } } } - } else { - tdlib.ui().handlePhotoOption(context, id, null, headerCell); } return true; } @Override public void onActivityResult (int requestCode, int resultCode, Intent data) { - if (resultCode == Activity.RESULT_OK) { - tdlib.ui().handlePhotoChange(requestCode, data, headerCell); - } + avatarPickerManager.handleActivityResult(requestCode, resultCode, data, TdlibUi.AvatarPickerManager.MODE_NON_CREATED, null, headerCell); } @Override @@ -382,7 +383,7 @@ protected void onFloatingButtonPressed () { } private boolean isCreating; - private String currentPhoto; + private ImageGalleryFile currentImageFile; private long[] currentMemberIds; private boolean currentIsChannel; @@ -409,7 +410,7 @@ public void createGroup () { headerCell.setInputEnabled(false); isCreating = true; - currentPhoto = headerCell.getPhoto(); + currentImageFile = headerCell.getImageFile(); String title = headerCell.getInput(); @@ -462,8 +463,8 @@ public void onResult (TdApi.Object object) { if (currentIsChannel) { tdlib.client().send(new TdApi.AddChatMembers(chatId, currentMemberIds), this); } - if (currentPhoto != null) { - tdlib.client().send(new TdApi.SetChatPhoto(chatId, new TdApi.InputChatPhotoStatic(new TdApi.InputFileGenerated(currentPhoto, SimpleGenerationInfo.makeConversion(currentPhoto), 0))), this); + if (currentImageFile != null) { + tdlib.client().send(new TdApi.SetChatPhoto(chatId, new TdApi.InputChatPhotoStatic(PhotoGenerationInfo.newFile(currentImageFile))), this); } tdlib.ui().post(() -> { if (groupCreationCallback == null || !groupCreationCallback.onGroupCreated(this, (TdApi.Chat) object)) { diff --git a/app/src/main/java/org/thunderdog/challegram/ui/CreatePollController.java b/app/src/main/java/org/thunderdog/challegram/ui/CreatePollController.java index affb47be90..bff3bc5cbf 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/CreatePollController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/CreatePollController.java @@ -650,6 +650,12 @@ private void send (TdApi.MessageSendOptions sendOptions, boolean disableMarkdown return; } + final CharSequence slowModeRestrictionText = tdlib.getSlowModeRestrictionText(chatId, sendOptions.schedulingState); + if (slowModeRestrictionText != null) { + context().tooltipManager().builder(getDoneButton()).controller(this).show(tdlib, slowModeRestrictionText).hideDelayed(); + return; + } + TdApi.FormattedText explanation = getExplanation(!disableMarkdown); // TODO lock texts @@ -679,7 +685,7 @@ private void send (TdApi.MessageSendOptions sendOptions, boolean disableMarkdown }; final TdApi.MessageSendOptions finalSendOptions = Td.newSendOptions(sendOptions, tdlib.chatDefaultDisableNotifications(chatId)); if (!getArgumentsStrict().callback.onSendPoll(this, chatId, messageThread != null ? messageThread.getMessageThreadId() : 0, poll, finalSendOptions, after)) { - tdlib.sendMessage(chatId, messageThread != null ? messageThread.getMessageThreadId() : 0, 0, finalSendOptions, poll, after); + tdlib.sendMessage(chatId, messageThread != null ? messageThread.getMessageThreadId() : 0, null, finalSendOptions, poll, after); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EditBaseController.java b/app/src/main/java/org/thunderdog/challegram/ui/EditBaseController.java index 5eac5f5840..520f7b85eb 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/EditBaseController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/EditBaseController.java @@ -115,7 +115,7 @@ public int getRootColorId () { } @Override - public boolean onDoneClick (View v) { + public final boolean onDoneClick (View v) { return onDoneClick(); } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EditChatFolderController.java b/app/src/main/java/org/thunderdog/challegram/ui/EditChatFolderController.java new file mode 100644 index 0000000000..33c5221a08 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/ui/EditChatFolderController.java @@ -0,0 +1,740 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 06/01/2023 + */ +package org.thunderdog.challegram.ui; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.content.Context; +import android.os.Bundle; +import android.text.InputType; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.IdRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.attach.CustomItemAnimator; +import org.thunderdog.challegram.component.base.SettingView; +import org.thunderdog.challegram.component.user.RemoveHelper; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.AvatarPlaceholder; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGFoundChat; +import org.thunderdog.challegram.navigation.HeaderView; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.support.RippleSupport; +import org.thunderdog.challegram.support.ViewSupport; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Drawables; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.util.AdapterSubListUpdateCallback; +import org.thunderdog.challegram.util.ListItemDiffUtilCallback; +import org.thunderdog.challegram.v.CustomRecyclerView; +import org.thunderdog.challegram.widget.BetterChatView; +import org.thunderdog.challegram.widget.MaterialEditTextGroup; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.ArrayUtils; +import me.vkryl.core.StringUtils; +import me.vkryl.td.Td; + +public class EditChatFolderController extends RecyclerViewController implements View.OnClickListener, SettingsAdapter.TextChangeListener, SelectChatsController.Delegate { + + private static final int NO_CHAT_FOLDER_ID = 0; + private static final int COLLAPSED_CHAT_COUNT = 3; + private static final int MAX_CHAT_FOLDER_TITLE_LENGTH = 12; + private static final TdApi.ChatFolder EMPTY_CHAT_FOLDER = TD.newChatFolder(); + private static final ArrayList TEMP_ITEM_LIST = new ArrayList<>(0); + + public static class Arguments { + private final int chatFolderId; + private final String chatFolderName; + private final @Nullable TdApi.ChatFolder chatFolder; + + public static Arguments newFolder () { + return new Arguments(NO_CHAT_FOLDER_ID, (TdApi.ChatFolder) null); + } + + public static Arguments newFolder (@Nullable TdApi.ChatFolder chatFolder) { + return new Arguments(NO_CHAT_FOLDER_ID, chatFolder); + } + + public Arguments (TdApi.ChatFolderInfo chatFolderInfo) { + this(chatFolderInfo.id, chatFolderInfo.title); + } + + public Arguments (int chatFolderId, @Nullable TdApi.ChatFolder chatFolder) { + this(chatFolderId, chatFolder != null ? chatFolder.title : "", chatFolder); + } + + public Arguments (int chatFolderId, String chatFolderName) { + this(chatFolderId, chatFolderName, null); + } + + private Arguments (int chatFolderId, String chatFolderName, @Nullable TdApi.ChatFolder chatFolder) { + this.chatFolder = chatFolder; + this.chatFolderId = chatFolderId; + this.chatFolderName = chatFolderName; + } + } + + public static EditChatFolderController newFolder (Context context, Tdlib tdlib) { + EditChatFolderController controller = new EditChatFolderController(context, tdlib); + controller.setArguments(Arguments.newFolder()); + return controller; + } + + public static EditChatFolderController newFolder (Context context, Tdlib tdlib, TdApi.ChatFolder chatFolder) { + EditChatFolderController controller = new EditChatFolderController(context, tdlib); + controller.setArguments(Arguments.newFolder(chatFolder)); + return controller; + } + + @SuppressWarnings("FieldCanBeLocal") + private final @IdRes int includedChatsPreviousItemId = R.id.btn_folderIncludeChats; + @SuppressWarnings("FieldCanBeLocal") + private final @IdRes int excludedChatsPreviousItemId = R.id.btn_folderExcludeChats; + private final @IdRes int includedChatsNextItemId = ViewCompat.generateViewId(); + private final @IdRes int excludedChatsNextItemId = ViewCompat.generateViewId(); + + private boolean showAllIncludedChats; + private boolean showAllExcludedChats; + + private SettingsAdapter adapter; + private ListItem input; + + private int chatFolderId; + private TdApi.ChatFolder originChatFolder; + private TdApi.ChatFolder editedChatFolder; + + public EditChatFolderController (Context context, Tdlib tdlib) { + super(context, tdlib); + } + + @Override + public boolean needAsynchronousAnimation () { + return originChatFolder == null && chatFolderId != NO_CHAT_FOLDER_ID; + } + + @Override + public long getAsynchronousAnimationTimeout (boolean fastAnimation) { + return 500l; + } + + @Override + public int getId () { + return R.id.controller_editChatFolders; + } + + @Override + public CharSequence getName () { + Arguments arguments = getArgumentsStrict(); + return chatFolderId != NO_CHAT_FOLDER_ID ? arguments.chatFolderName : Lang.getString(R.string.NewFolder); + } + + @Override + public void setArguments (Arguments args) { + super.setArguments(args); + this.chatFolderId = args.chatFolderId; + this.originChatFolder = args.chatFolder; + this.editedChatFolder = args.chatFolder != null ? TD.copyOf(args.chatFolder) : TD.newChatFolder(args.chatFolderName); + } + + @Override + protected void onCreateView (Context context, CustomRecyclerView recyclerView) { + ArrayList items = new ArrayList<>(); + items.add(new ListItem(ListItem.TYPE_HEADER_PADDED, 0, 0, R.string.FolderName)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(input = new ListItem(ListItem.TYPE_CUSTOM_SINGLE, R.id.input).setStringValue(editedChatFolder.title)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + + items.add(new ListItem(ListItem.TYPE_HEADER_PADDED, 0, 0, R.string.FolderIncludedChats)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_folderIncludeChats, R.drawable.baseline_add_24, R.string.FolderActionIncludeChats).setTextColorId(ColorId.inlineText)); + fillIncludedChats(editedChatFolder, items); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM, includedChatsNextItemId)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, Lang.getMarkdownString(this, R.string.FolderIncludedChatsInfo))); + + items.add(new ListItem(ListItem.TYPE_HEADER_PADDED, 0, 0, R.string.FolderExcludedChats)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_folderExcludeChats, R.drawable.baseline_add_24, R.string.FolderActionExcludeChats).setTextColorId(ColorId.inlineText)); + fillExcludedChats(editedChatFolder, items); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM, excludedChatsNextItemId)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, Lang.getMarkdownString(this, R.string.FolderExcludedChatsInfo))); + + if (chatFolderId != NO_CHAT_FOLDER_ID) { + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_removeFolder, R.drawable.baseline_delete_forever_24, R.string.RemoveFolder).setTextColorId(ColorId.textNegative)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_PADDING).setHeight(Screen.dp(12f))); + } + + adapter = new Adapter(this); + adapter.setLockFocusOn(this, /* showAlways */ StringUtils.isEmpty(editedChatFolder.title)); + adapter.setTextChangeListener(this); + adapter.setItems(items, false); + CustomItemAnimator itemAnimator = new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); + itemAnimator.setSupportsChangeAnimations(false); + recyclerView.setItemAnimator(itemAnimator); + recyclerView.setAdapter(adapter); + RemoveHelper.attach(recyclerView, new RemoveHelperCallback()); + + if (originChatFolder == null && chatFolderId != NO_CHAT_FOLDER_ID) { + loadChatFolder(); + } + } + + @Override + public boolean saveInstanceState (Bundle outState, String keyPrefix) { + super.saveInstanceState(outState, keyPrefix); + Arguments arguments = getArgumentsStrict(); + outState.putInt(keyPrefix + "_chatFolderId", arguments.chatFolderId); + outState.putString(keyPrefix + "_chatFolderName", arguments.chatFolderName); + TD.saveChatFolder(outState, keyPrefix + "_originChatFolder", originChatFolder); + TD.saveChatFolder(outState, keyPrefix + "_editedChatFolder", editedChatFolder); + outState.putBoolean(keyPrefix + "_showAllIncludedChats", showAllIncludedChats); + outState.putBoolean(keyPrefix + "_showAllExcludedChats", showAllExcludedChats); + return true; + } + + @Override + public boolean restoreInstanceState (Bundle in, String keyPrefix) { + super.restoreInstanceState(in, keyPrefix); + int chatFolderId = in.getInt(keyPrefix + "_chatFolderId", NO_CHAT_FOLDER_ID); + String chatFolderName = in.getString(keyPrefix + "_chatFolderName"); + TdApi.ChatFolder originChatFolder = TD.restoreChatFolder(in, keyPrefix + "_originChatFolder"); + TdApi.ChatFolder editedChatFolder = TD.restoreChatFolder(in, keyPrefix + "_editedChatFolder"); + if (chatFolderName != null && editedChatFolder != null) { + super.setArguments(new Arguments(chatFolderId, chatFolderName, originChatFolder)); + this.chatFolderId = chatFolderId; + this.originChatFolder = originChatFolder; + this.editedChatFolder = editedChatFolder; + this.showAllIncludedChats = in.getBoolean(keyPrefix + "_showAllIncludedChats"); + this.showAllExcludedChats = in.getBoolean(keyPrefix + "_showAllExcludedChats"); + return true; + } + return false; + } + + @Override + protected int getMenuId () { + return R.id.menu_done; + } + + @Override + public void fillMenuItems (int id, HeaderView header, LinearLayout menu) { + if (id == R.id.menu_done) { + header.addDoneButton(menu, this).setVisibility(canSaveChanges() ? View.VISIBLE : View.GONE); + } + } + + @Override + public void onMenuItemPressed (int id, View view) { + if (id == R.id.menu_btn_done) { + saveChanges(); + } + } + + @Override + public void onClick (View v) { + int id = v.getId(); + if (id == R.id.btn_folderIncludeChats) { + SelectChatsController selectChats = new SelectChatsController(context, tdlib); + selectChats.setArguments(SelectChatsController.Arguments.includedChats(this, chatFolderId, editedChatFolder)); + navigateTo(selectChats); + } else if (id == R.id.btn_folderExcludeChats) { + SelectChatsController selectChats = new SelectChatsController(context, tdlib); + selectChats.setArguments(SelectChatsController.Arguments.excludedChats(this, chatFolderId, editedChatFolder)); + navigateTo(selectChats); + } else if (id == R.id.btn_showAdvanced) { + ListItem item = (ListItem) v.getTag(); + if (item.getBoolValue()) { + if (!showAllIncludedChats) { + showAllIncludedChats = true; + updateIncludedChats(); + } + } else { + if (!showAllExcludedChats) { + showAllExcludedChats = true; + updateExcludedChats(); + } + } + } else if (id == R.id.btn_removeFolder) { + showRemoveFolderConfirm(); + } else if (id == R.id.chat || ArrayUtils.contains(TD.CHAT_TYPES, id)) { + int position = getRecyclerView().getChildAdapterPosition(v); + ListItem item = (ListItem) v.getTag(); + showRemoveConditionConfirm(position, item); + } + } + + @Override + public boolean onBackPressed (boolean fromTop) { + if (hasChanges()) { + showUnsavedChangesPromptBeforeLeaving(/* onConfirm */ null); + return true; + } + return super.onBackPressed(fromTop); + } + + @Override + public void onBlur () { + super.onBlur(); + adapter.setLockFocusOn(this, false); + setLockFocusView(getLockFocusView(), false); + } + + @Override + public void onPrepareToShow () { + super.onPrepareToShow(); + updateMenuButton(); + } + + @Override + public void onTextChanged (int id, ListItem item, MaterialEditTextGroup v, String text) { + editedChatFolder.title = text; + updateMenuButton(); + } + + private void fillIncludedChats (TdApi.ChatFolder chatFolder, List outList) { + int chatTypeCount = TD.countIncludedChatTypes(chatFolder); + int chatCount = chatFolder.pinnedChatIds.length + chatFolder.includedChatIds.length; + int visibleChatCount = showAllIncludedChats || (chatCount <= COLLAPSED_CHAT_COUNT + 1) ? chatCount : COLLAPSED_CHAT_COUNT; + int moreCount = chatCount - visibleChatCount; + int itemCount = (chatTypeCount + visibleChatCount) * 2 + (moreCount > 0 ? 2 : 0); + if (itemCount == 0) + return; + ArrayUtils.ensureCapacity(outList, itemCount); + for (int includedChatType : TD.includedChatTypes(chatFolder)) { + outList.add(new ListItem(ListItem.TYPE_SEPARATOR).setIntValue(includedChatType)); + outList.add(chatTypeItem(includedChatType)); + } + int count = 0; + for (long pinnedChatId : chatFolder.pinnedChatIds) { + if (count++ >= visibleChatCount) + break; + outList.add(new ListItem(ListItem.TYPE_SEPARATOR).setLongValue(pinnedChatId)); + outList.add(chatItem(pinnedChatId).setBoolValue(true /* included chat */)); + } + for (long includedChatId : chatFolder.includedChatIds) { + if (count++ >= visibleChatCount) + break; + outList.add(new ListItem(ListItem.TYPE_SEPARATOR).setLongValue(includedChatId)); + outList.add(chatItem(includedChatId).setBoolValue(true /* included chat */)); + } + if (moreCount > 0) { + outList.add(new ListItem(ListItem.TYPE_SEPARATOR).setIntValue(R.id.btn_showAdvanced)); + outList.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_showAdvanced, R.drawable.baseline_direction_arrow_down_24, Lang.plural(R.string.ChatsXShowMore, moreCount)).setBoolValue(true /* included chats */)); + } + } + + private void fillExcludedChats (TdApi.ChatFolder chatFolder, List outList) { + int chatTypeCount = TD.countExcludedChatTypes(chatFolder); + int chatCount = chatFolder.excludedChatIds.length; + int visibleChatCount = showAllExcludedChats || (chatCount <= COLLAPSED_CHAT_COUNT + 1) ? chatCount : COLLAPSED_CHAT_COUNT; + int moreCount = chatCount - visibleChatCount; + int itemCount = (chatTypeCount + visibleChatCount) * 2 + (moreCount > 0 ? 2 : 0); + if (itemCount == 0) + return; + ArrayUtils.ensureCapacity(outList, itemCount); + for (int excludedChatType : TD.excludedChatTypes(chatFolder)) { + outList.add(new ListItem(ListItem.TYPE_SEPARATOR).setIntValue(excludedChatType)); + outList.add(chatTypeItem(excludedChatType)); + } + for (int index = 0; index < visibleChatCount; index++) { + long excludedChatId = chatFolder.excludedChatIds[index]; + outList.add(new ListItem(ListItem.TYPE_SEPARATOR).setLongValue(excludedChatId)); + outList.add(chatItem(excludedChatId).setBoolValue(false /* excluded chat */)); + } + if (moreCount > 0) { + outList.add(new ListItem(ListItem.TYPE_SEPARATOR).setIntValue(R.id.btn_showAdvanced)); + outList.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_showAdvanced, R.drawable.baseline_direction_arrow_down_24, Lang.plural(R.string.ChatsXShowMore, moreCount)).setBoolValue(false /* excluded chats */)); + } + } + + private ListItem chatItem (long chatId) { + TGFoundChat foundChat = new TGFoundChat(tdlib, null, chatId, true); + foundChat.setNoUnread(); + return new ListItem(ListItem.TYPE_CHAT_BETTER, R.id.chat).setData(foundChat).setLongId(chatId); + } + + private ListItem chatTypeItem (@IdRes int id) { + TdlibAccentColor accentColor = tdlib.accentColor(TD.chatTypeAccentColorId(id)); + return new ListItem(ListItem.TYPE_CHAT_BETTER, id, TD.chatTypeIcon24(id), TD.chatTypeName(id)) + .setAccentColor(accentColor); + } + + private void loadChatFolder () { + tdlib.send(new TdApi.GetChatFolder(chatFolderId), (chatFolder, error) -> runOnUiThreadOptional(() -> { + if (error != null) { + UI.showError(error); + } else { + updateChatFolder(chatFolder); + } + })); + } + + private void updateChatFolder (TdApi.ChatFolder chatFolder) { + this.editedChatFolder = chatFolder; + updateMenuButton(); + updateIncludedChats(); + updateExcludedChats(); + } + + private void updateIncludedChats () { + int previousItemIndex = adapter.indexOfViewById(includedChatsPreviousItemId); + int nextItemIndex = adapter.indexOfViewById(includedChatsNextItemId); + if (previousItemIndex == -1 || nextItemIndex == -1) + return; + int firstItemIndex = previousItemIndex + 1; + TEMP_ITEM_LIST.clear(); + fillIncludedChats(editedChatFolder, TEMP_ITEM_LIST); + if (firstItemIndex < nextItemIndex) { + List oldList = adapter.getItems().subList(firstItemIndex, nextItemIndex); + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtilCallback(oldList, TEMP_ITEM_LIST)); + oldList.clear(); + oldList.addAll(TEMP_ITEM_LIST); + diffResult.dispatchUpdatesTo(new AdapterSubListUpdateCallback(adapter, firstItemIndex)); + } else if (TEMP_ITEM_LIST.size() > 0) { + adapter.addItems(firstItemIndex, TEMP_ITEM_LIST.toArray(new ListItem[0])); + } + TEMP_ITEM_LIST.clear(); + } + + private void updateExcludedChats () { + int previousItemIndex = adapter.indexOfViewById(excludedChatsPreviousItemId); + int nextItemIndex = adapter.indexOfViewById(excludedChatsNextItemId); + if (previousItemIndex == -1 || nextItemIndex == -1) + return; + int firstItemIndex = previousItemIndex + 1; + TEMP_ITEM_LIST.clear(); + fillExcludedChats(editedChatFolder, TEMP_ITEM_LIST); + if (firstItemIndex < nextItemIndex) { + List oldList = adapter.getItems().subList(firstItemIndex, nextItemIndex); + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtilCallback(oldList, TEMP_ITEM_LIST)); + oldList.clear(); + oldList.addAll(TEMP_ITEM_LIST); + diffResult.dispatchUpdatesTo(new AdapterSubListUpdateCallback(adapter, firstItemIndex)); + } else if (TEMP_ITEM_LIST.size() > 0) { + adapter.addItems(firstItemIndex, TEMP_ITEM_LIST.toArray(new ListItem[0])); + } + TEMP_ITEM_LIST.clear(); + } + + private void updateFolderName () { + if (StringUtils.isEmpty(editedChatFolder.title) && editedChatFolder.pinnedChatIds.length == 0 && editedChatFolder.includedChatIds.length == 0) { + int[] includedChatTypes = TD.includedChatTypes(editedChatFolder); + if (includedChatTypes.length == 1) { + int includedChatType = includedChatTypes[0]; + String chatTypeName = Lang.getString(TD.chatTypeName(includedChatType)); + boolean hasChanges = false; + if (input.setStringValueIfChanged(chatTypeName)) { + editedChatFolder.title = chatTypeName; + hasChanges = true; + } + if (editedChatFolder.icon == null) { + TdApi.ChatFolderIcon chatTypeIcon = TD.chatTypeIcon(includedChatType); + if (chatTypeIcon != null) { + editedChatFolder.icon = chatTypeIcon; + hasChanges = true; + } + } + if (hasChanges) { + adapter.updateSimpleItemById(input.getId()); + } + } + } + } + + @Override + public void onSelectedChatsChanged (int mode, Set chatIds, Set chatTypes) { + if (mode == SelectChatsController.MODE_FOLDER_INCLUDE_CHATS) { + TD.updateIncludedChats(editedChatFolder, originChatFolder, chatIds); + TD.updateIncludedChatTypes(editedChatFolder, chatTypes); + } else if (mode == SelectChatsController.MODE_FOLDER_EXCLUDE_CHATS) { + TD.updateExcludedChats(editedChatFolder, chatIds); + TD.updateExcludedChatTypes(editedChatFolder, chatTypes); + } else { + throw new UnsupportedOperationException(); + } + updateFolderName(); + updateChatFolder(editedChatFolder); + } + + private void showRemoveConditionConfirm (int position, ListItem item) { + boolean inclusion = item.getBoolValue(); + CharSequence title; + if (item.getId() == R.id.chat) { + title = ((TGFoundChat) item.getData()).getFullTitle(); + } else { + title = item.getString(); + } + @StringRes int stringRes; + if (item.getId() == R.id.chat) { + long chatId = item.getLongId(); + if (tdlib.isUserChat(chatId)) { + stringRes = inclusion ? R.string.FolderRemoveInclusionConfirmUser : R.string.FolderRemoveExclusionConfirmUser; + } else { + stringRes = inclusion ? R.string.FolderRemoveInclusionConfirmChat : R.string.FolderRemoveExclusionConfirmChat; + } + } else { + stringRes = inclusion ? R.string.FolderRemoveInclusionConfirmType : R.string.FolderRemoveExclusionConfirmType; + } + CharSequence info = Lang.getStringBold(stringRes, title); + showConfirm(info, Lang.getString(R.string.Remove), R.drawable.baseline_delete_24, OPTION_COLOR_RED, () -> { + int index = adapter.getItem(position) == item ? position : adapter.indexOfView(item); + if (index != RecyclerView.NO_POSITION) { + adapter.removeRange(index - 1, 2); /* separator, condition */ + } + if (item.getId() == R.id.chat) { + long chatId = item.getLongId(); + if (inclusion) { + editedChatFolder.pinnedChatIds = ArrayUtils.removeElement(editedChatFolder.pinnedChatIds, ArrayUtils.indexOf(editedChatFolder.pinnedChatIds, chatId)); + editedChatFolder.includedChatIds = ArrayUtils.removeElement(editedChatFolder.includedChatIds, ArrayUtils.indexOf(editedChatFolder.includedChatIds, chatId)); + } else { + editedChatFolder.excludedChatIds = ArrayUtils.removeElement(editedChatFolder.excludedChatIds, ArrayUtils.indexOf(editedChatFolder.excludedChatIds, chatId)); + } + } else if (item.getId() == R.id.chatType_contact) { + editedChatFolder.includeContacts = false; + } else if (item.getId() == R.id.chatType_nonContact) { + editedChatFolder.includeNonContacts = false; + } else if (item.getId() == R.id.chatType_group) { + editedChatFolder.includeGroups = false; + } else if (item.getId() == R.id.chatType_channel) { + editedChatFolder.includeChannels = false; + } else if (item.getId() == R.id.chatType_bot) { + editedChatFolder.includeBots = false; + } else if (item.getId() == R.id.chatType_muted) { + editedChatFolder.excludeMuted = false; + } else if (item.getId() == R.id.chatType_read) { + editedChatFolder.excludeRead = false; + } else if (item.getId() == R.id.chatType_archived) { + editedChatFolder.excludeArchived = false; + } + updateFolderName(); + updateMenuButton(); + }); + } + + private void showRemoveFolderConfirm () { + showConfirm(Lang.getString(R.string.RemoveFolderConfirm), Lang.getString(R.string.Remove), R.drawable.baseline_delete_24, OPTION_COLOR_RED, () -> { + deleteChatFolder(chatFolderId); + }); + } + + private boolean hasChanges () { + TdApi.ChatFolder originChatFolder = this.originChatFolder != null ? this.originChatFolder : EMPTY_CHAT_FOLDER; + TdApi.ChatFolder editedChatFolder = this.editedChatFolder != null ? this.editedChatFolder : EMPTY_CHAT_FOLDER; + return !TD.contentEquals(originChatFolder, editedChatFolder); + } + + private boolean canSaveChanges () { + String title = editedChatFolder.title.trim(); + if (StringUtils.isEmpty(title)) { + return false; + } + int codePointCount = Character.codePointCount(title, 0, title.length()); + if (codePointCount > MAX_CHAT_FOLDER_TITLE_LENGTH) { + return false; + } + return (editedChatFolder.includeContacts || editedChatFolder.includeNonContacts || editedChatFolder.includeGroups || editedChatFolder.includeChannels || editedChatFolder.includeBots || editedChatFolder.pinnedChatIds.length > 0 || editedChatFolder.includedChatIds.length > 0) && + (chatFolderId == NO_CHAT_FOLDER_ID || hasChanges()); + } + + private void saveChanges () { + if (chatFolderId != NO_CHAT_FOLDER_ID) { + editChatFolder(chatFolderId, TD.copyOf(editedChatFolder)); + } else { + createChatFolder(TD.copyOf(editedChatFolder)); + } + } + + private void createChatFolder (TdApi.ChatFolder chatFolder) { + tdlib.send(new TdApi.CreateChatFolder(chatFolder), tdlib.successHandler(this::closeSelf)); + } + + private void editChatFolder (int chatFolderId, TdApi.ChatFolder chatFolder) { + tdlib.send(new TdApi.EditChatFolder(chatFolderId, chatFolder), tdlib.successHandler(this::closeSelf)); + } + + private void deleteChatFolder (int chatFolderId) { + tdlib.send(new TdApi.DeleteChatFolder(chatFolderId, null), tdlib.typedOkHandler(this::closeSelf)); + } + + private void closeSelf () { + if (!isDestroyed()) { + navigateBack(); + } + } + + private void updateMenuButton () { + if (headerView != null) { + headerView.updateButton(getMenuId(), R.id.menu_btn_done, canSaveChanges() ? View.VISIBLE : View.GONE, 0); + } + } + + private class Adapter extends SettingsAdapter { + public Adapter (ViewController context) { + super(context); + } + @Override + protected void setChatData (ListItem item, int position, BetterChatView chatView) { + if (item.getId() == R.id.chat) { + chatView.setNoSubtitle(false); + chatView.setChat((TGFoundChat) item.getData()); + chatView.setAllowMaximizePreview(false); + } else { + chatView.setTitle(item.getString()); + chatView.setSubtitle(null); + chatView.setNoSubtitle(true); + chatView.setAvatar(null, new AvatarPlaceholder.Metadata(item.getAccentColor(), item.getIconResource())); + chatView.clearPreviewChat(); + } + } + + @Override + protected SettingHolder initCustom (ViewGroup parent) { + FrameLayoutFix frameLayout = new FrameLayoutFix(parent.getContext()); + frameLayout.setLayoutParams(new RecyclerView.LayoutParams(MATCH_PARENT, Screen.dp(57f))); + ViewSupport.setThemedBackground(frameLayout, ColorId.filling, EditChatFolderController.this); + + MaterialEditTextGroup editText = new MaterialEditTextGroup(parent.getContext(), false); + editText.setId(android.R.id.input); + editText.applyRtl(Lang.rtl()); + editText.addThemeListeners(EditChatFolderController.this); + editText.setTextListener(this); + editText.setFocusListener(this); + editText.addLengthCounter(true); + editText.setMaxLength(MAX_CHAT_FOLDER_TITLE_LENGTH); + editText.getEditText().setLineDisabled(true); + editText.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_WORDS); + + FrameLayout.LayoutParams editTextParams = new FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT); + editTextParams.leftMargin = Screen.dp(16f); + editTextParams.rightMargin = Screen.dp(57f); + editTextParams.bottomMargin = Screen.dp(8f); + frameLayout.addView(editText, editTextParams); + + ImageView imageView = new ImageView(parent.getContext()); + imageView.setId(android.R.id.icon); + imageView.setScaleType(ImageView.ScaleType.CENTER); + imageView.setColorFilter(Theme.getColor(ColorId.icon)); + addThemeFilterListener(imageView, ColorId.icon); + RippleSupport.setTransparentSelector(imageView); + Views.setClickable(imageView); + imageView.setOnClickListener(v -> showIconSelector()); + + FrameLayout.LayoutParams imageViewParams = new FrameLayout.LayoutParams(Screen.dp(57f), Screen.dp(57f), Gravity.CENTER_VERTICAL | Gravity.RIGHT); + frameLayout.addView(imageView, imageViewParams); + + setLockFocusView(editText.getEditText()); + + SettingHolder holder = new SettingHolder(frameLayout); + holder.setIsRecyclable(false); + return holder; + } + + @Override + protected void setCustom (ListItem item, SettingHolder holder, int position) { + MaterialEditTextGroup editText = holder.itemView.findViewById(android.R.id.input); + editText.applyRtl(Lang.rtl()); + editText.setEmptyHint(R.string.FolderNameHint); + editText.setText(item.getStringValue()); + + ImageView imageView = holder.itemView.findViewById(android.R.id.icon); + int iconResource = TD.findFolderIcon(editedChatFolder.icon, R.drawable.baseline_folder_24); + imageView.setImageDrawable(Drawables.get(imageView.getResources(), iconResource)); + } + + @Override + protected void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) { + if (item.getId() == R.id.btn_folderIncludeChats || item.getId() == R.id.btn_folderExcludeChats) { + view.setIconColorId(ColorId.inlineIcon); + } else if (item.getId() == R.id.btn_removeFolder) { + view.setIconColorId(ColorId.iconNegative); + } else { + view.setIconColorId(ColorId.NONE /* theme_color_icon */); + } + } + } + + private static class DiffUtilCallback extends ListItemDiffUtilCallback { + public DiffUtilCallback (List oldList, List newList) { + super(oldList, newList); + } + + @Override + public boolean areItemsTheSame (ListItem oldItem, ListItem newItem) { + if (oldItem.getViewType() != newItem.getViewType() || oldItem.getId() != newItem.getId()) + return false; + if (oldItem.getId() == R.id.chat) { + return oldItem.getLongId() == newItem.getLongId(); + } + if (oldItem.getViewType() == ListItem.TYPE_SEPARATOR) + return oldItem.getIntValue() == newItem.getIntValue() && oldItem.getLongValue() == newItem.getLongValue(); + return true; + } + + @Override + public boolean areContentsTheSame (ListItem oldItem, ListItem newItem) { + return false; + } + } + + private class RemoveHelperCallback implements RemoveHelper.Callback { + @Override + public boolean canRemove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int position) { + return viewHolder.getItemViewType() == ListItem.TYPE_CHAT_BETTER; + } + + @Override + public void onRemove (RecyclerView.ViewHolder viewHolder) { + ListItem item = (ListItem) viewHolder.itemView.getTag(); + int position = viewHolder.getAbsoluteAdapterPosition(); + showRemoveConditionConfirm(position, item); + } + } + + private void showIconSelector () { + ChatFolderIconSelector.show(this, icon -> { + if (!Td.equalsTo(editedChatFolder.icon, icon)) { + editedChatFolder.icon = icon; + adapter.updateSimpleItemById(input.getId()); + updateMenuButton(); + } + }); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EditDeleteAccountReasonController.java b/app/src/main/java/org/thunderdog/challegram/ui/EditDeleteAccountReasonController.java new file mode 100644 index 0000000000..f5802f5703 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/ui/EditDeleteAccountReasonController.java @@ -0,0 +1,117 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 25/12/2023 at 00:17 + */ +package org.thunderdog.challegram.ui; + +import android.content.Context; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccount; +import org.thunderdog.challegram.tool.Strings; +import org.thunderdog.challegram.widget.DoneButton; + +import me.vkryl.core.StringUtils; + +public class EditDeleteAccountReasonController extends EditTextController { + public EditDeleteAccountReasonController (Context context, Tdlib tdlib) { + super(context, tdlib); + setDelegate(new Delegate() { + @Override + public int getId () { + return R.id.controller_deleteAccount; + } + + @Override + public int getDoneIcon () { + return R.drawable.baseline_delete_alert_24; + } + + @Override + public CharSequence getName () { + return Lang.getString(R.string.DeleteAccount); + } + + @Override + public CharSequence getHint () { + return Lang.getString(R.string.DeleteAccountReason); + } + + @Override + public CharSequence getDescription () { + return Lang.getMarkdownString(EditDeleteAccountReasonController.this, R.string.DeleteAccountDescription); + } + + @Override + public boolean allowEmptyValue () { + return false; + } + + @Override + public boolean needFocusInput () { + return false; + } + + @Override + public boolean onDonePressed (EditTextController controller, DoneButton button, String value) { + if (isInProgress()) + return false; + if (!StringUtils.isEmptyOrBlank(value)) { + showOptions( + Lang.getMarkdownString(controller, R.string.DeleteAccountConfirmFinal), + new int[] {R.id.btn_deleteAccount, R.id.btn_cancel}, + new String[] {Lang.getString(R.string.DeleteAccountConfirmFinalBtn), Lang.getString(R.string.Cancel)}, + new int[] {OPTION_COLOR_RED, OPTION_COLOR_NORMAL}, + new int[] {R.drawable.baseline_delete_alert_24, R.drawable.baseline_cancel_24}, + (optionItemView, id) -> { + if (id == R.id.btn_deleteAccount) { + TdlibAccount account = tdlib.account(); + String name = account.getName(); + String phoneNumber = account.getPhoneNumber(); + setDoneInProgress(true); + setStackLocked(true); + tdlib.send(new TdApi.DeleteAccount(value, getArguments()), (ok, error) -> runOnUiThreadOptional(() -> { + setStackLocked(false); + setDoneInProgress(false); + if (error != null) { + context().tooltipManager() + .builder(getDoneButton()) + .icon(R.drawable.baseline_warning_24) + .show(tdlib, TD.toErrorString(error)); + } else { + tdlib.switchToNextAuthorizedAccount(); + openAlert(R.string.AccountDeleted, Lang.getMarkdownString(controller, R.string.AccountDeletedText, name, Strings.formatPhone(phoneNumber))); + } + })); + } + return true; + } + ); + return true; + } + return false; + } + }); + addOneShotFocusListener(() -> { + ViewController c = navigationController().getStack().getPrevious(); + if (c instanceof PasswordController) { // Password confirmation + destroyPreviousStackItem(); + } + }); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EditLanguageController.java b/app/src/main/java/org/thunderdog/challegram/ui/EditLanguageController.java index dc59042eb0..25466bf253 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/EditLanguageController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/EditLanguageController.java @@ -403,7 +403,7 @@ private static void copyValue (TdApi.LanguagePackStringValuePluralized to, TdApi } @Override - public boolean onDoneClick (View v) { + public final boolean onDoneClick () { if (saveString()) { if (exitOnSave) { onSaveCompleted(); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EditRightsController.java b/app/src/main/java/org/thunderdog/challegram/ui/EditRightsController.java index e4acc56bd9..d0fb7c5365 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/EditRightsController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/EditRightsController.java @@ -21,6 +21,7 @@ import android.view.ViewGroup; import android.widget.Toast; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -104,13 +105,14 @@ public EditRightsController (Context context, Tdlib tdlib) { private TdApi.ChatMemberStatusAdministrator targetAdmin; private TdApi.ChatMemberStatusRestricted targetRestrict; - private boolean canViewMessages; + private boolean canViewMessages, isForum; @Override public void setArguments (Args args) { super.setArguments(args); canViewMessages = tdlib.isChannel(args.chatId); + isForum = tdlib.isForum(args.chatId); switch (args.mode) { case MODE_RESTRICTION: { @@ -131,8 +133,27 @@ public void setArguments (Args args) { if (args.member != null) { if (args.member.status.getConstructor() == TdApi.ChatMemberStatusCreator.CONSTRUCTOR) { TdApi.ChatMemberStatusCreator creator = (TdApi.ChatMemberStatusCreator) args.member.status; - // TODO bot defaults - targetAdmin = new TdApi.ChatMemberStatusAdministrator(creator.customTitle, true, new TdApi.ChatAdministratorRights(true, true, true, true, true, true, true, true, true, true, true, creator.isAnonymous)); + targetAdmin = new TdApi.ChatMemberStatusAdministrator( + creator.customTitle, + true, + new TdApi.ChatAdministratorRights( + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + creator.isAnonymous + ) + ); } else if (args.member.status.getConstructor() == TdApi.ChatMemberStatusAdministrator.CONSTRUCTOR) { TdApi.ChatMemberStatusAdministrator admin = (TdApi.ChatMemberStatusAdministrator) args.member.status; targetAdmin = (TdApi.ChatMemberStatusAdministrator) Td.copyOf(admin); @@ -160,7 +181,28 @@ private TdApi.ChatMemberStatusAdministrator newTargetAdmin () { return new TdApi.ChatMemberStatusAdministrator(null, true, Td.copyOf(me.rights)); } } - return new TdApi.ChatMemberStatusAdministrator(null, true, new TdApi.ChatAdministratorRights(true, true, true, true, true, true, true, true, false, true, true, false)); + // TODO bot defaults + return new TdApi.ChatMemberStatusAdministrator( + null, + true, + new TdApi.ChatAdministratorRights( + true, + true, + true, + true, + true, + true, + true, + true, + isForum, + true, + true, + true, + true, + true, + false + ) + ); } @Override @@ -218,6 +260,11 @@ public void onClick (View view) { .show(this, tdlib, R.drawable.baseline_info_24, text); } } + } else if (viewId == R.id.btn_togglePermissionGroup) { + RightOption option = (RightOption) item.getData(); + if (option != null && option.groupRightIds != null) { + toggleRightsGroupVisibility(option.groupRightIds); + } } else if (viewId == R.id.btn_transferOwnership) { if (ChatId.isBasicGroup(getArgumentsStrict().chatId)) { showConfirm(Lang.getMarkdownString(this, R.string.UpgradeChatPrompt), Lang.getString(R.string.Proceed), this::onTransferOwnershipClick); @@ -276,6 +323,9 @@ public void onClick (View view) { targetAdmin.rights.canManageVideoChats = false; targetAdmin.rights.isAnonymous = false; targetAdmin.rights.canPromoteMembers = false; + targetAdmin.rights.canPostStories = false; + targetAdmin.rights.canEditStories = false; + targetAdmin.rights.canDeleteStories = false; updateValues(); setDoneInProgress(true); setDoneVisible(true); @@ -440,21 +490,39 @@ protected void modifyEditText (ListItem item, ViewGroup parent, MaterialEditText @SuppressWarnings("WrongConstant") protected void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) { final int viewId = item.getId(); + boolean needRotateIcon = false; if (viewId == R.id.btn_togglePermission) { @RightId int rightId = item.getIntValue(); boolean canEdit = hasAccessToEditRight(item); view.setIgnoreEnabled(true); view.setEnabled(canEdit || getHintForToggleUnavailability(item) != null); view.setVisuallyEnabled(canEdit, isUpdate); + boolean isToggler = item.getViewType() == ListItem.TYPE_VALUED_SETTING_COMPACT_WITH_TOGGLER; + boolean needAddData = getArgumentsStrict().mode == MODE_CHAT_PERMISSIONS; + if (isToggler) { view.getToggler().setUseNegativeState(true); } view.getToggler().setRadioEnabled(item.getBoolValue(), isUpdate); view.getToggler().setShowLock(!canEdit); - if (isToggler) { + if (needAddData) { view.setData(item.getBoolValue() ? R.string.AllMembers : (rightId == RightId.INVITE_USERS || rightId == RightId.CHANGE_CHAT_INFO || rightId == RightId.PIN_MESSAGES) ? R.string.OnlyAdminsSpecific : R.string.OnlyAdmins); } + } else if (viewId == R.id.btn_togglePermissionGroup) { + final RightOption option = (RightOption) item.getData(); + if (option != null && option.groupRightIds != null) { + final boolean canEdit = hasAccessToEditRightsGroup(option.groupRightIds); + final int count = getRightsGroupEnabledCount(option.groupRightIds); + view.setIgnoreEnabled(true); + view.setEnabled(true /*canEdit || getHintForToggleUnavailability(item) != null*/); + view.setVisuallyEnabled(canEdit, isUpdate); + view.getToggler().setUseNegativeState(true); + view.getToggler().setRadioEnabled(item.getBoolValue(), isUpdate); + view.getToggler().setShowLock(!canEdit); + view.setData(Lang.pluralBold(R.string.xPermissionsSendMediaAllowed, count, option.groupRightIds.length)); + needRotateIcon = adapter.indexOfViewByIdAndValue(R.id.btn_togglePermission, option.groupRightIds[0]) != -1; + } } else if (viewId == R.id.btn_date) { boolean canEdit = hasAccessToEditRight(item); view.setIgnoreEnabled(true); @@ -480,12 +548,40 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda view.setName(canViewMessages ? R.string.RestrictFor : R.string.BlockFor); } } + if (view.getToggler() != null) { + if (viewId == R.id.btn_togglePermissionGroup) { + view.getToggler().setOnClickListener(v -> { + ListItem i = (ListItem) view.getTag(); + if (i.getId() == R.id.btn_togglePermissionGroup) { + final RightOption option = (RightOption) item.getData(); + if (option != null && option.groupRightIds != null) { + toggleRightsGroup(option.groupRightIds); + } + } else { + view.performClick(); + } + }); + view.getToggler().setClickable(true); + } else { + view.getToggler().setOnClickListener(null); + view.getToggler().setClickable(false); + } + } + view.setIconRotated(needRotateIcon, isUpdate); } @Override protected void setChatData (ListItem item, int position, BetterChatView chatView) { chatView.setChat((TGFoundChat) item.getData()); } + + @Override + public void modifySettingView (int viewType, SettingView settingView) { + super.modifySettingView(viewType, settingView); + if (viewType == ListItem.TYPE_VALUED_SETTING_COMPACT_WITH_TOGGLER || viewType == ListItem.TYPE_RADIO_SETTING_WITH_NEGATIVE_STATE) { + settingView.forcePadding(Screen.dp(73), 0); + } + } }; buildCells(); @@ -574,6 +670,9 @@ private CharSequence getHintForToggleUnavailability (ListItem item) { if (!tdlib.canRestrictMembers(args.chatId)) { return null; // No need to explain } + if (tdlib.isBroadcastGroup(args.chatId)) { + return Lang.getMarkdownString(this, R.string.BroadcastWriteHint); + } if (currentValue) return null; TdApi.Chat chat = tdlib.chatStrict(args.chatId); @@ -646,6 +745,11 @@ private boolean hasAccessToEditRight (ListItem item) { throw new UnsupportedOperationException(); } + return hasAccessToEditRight(id); + } + + private boolean hasAccessToEditRight (int id) { + Args args = getArgumentsStrict(); if (args.mode == MODE_CHAT_PERMISSIONS) { if (tdlib.canRestrictMembers(args.chatId)) { TdApi.Chat chat = tdlib.chatStrict(args.chatId); @@ -661,7 +765,7 @@ private boolean hasAccessToEditRight (ListItem item) { } break; } - return true; + return !tdlib.isBroadcastGroup(args.chatId); } return false; } @@ -723,6 +827,14 @@ private boolean hasAccessToEditRight (ListItem item) { return me.rights.canPinMessages; case RightId.MANAGE_VIDEO_CHATS: return me.rights.canManageVideoChats; + case RightId.MANAGE_TOPICS: + return me.rights.canManageTopics; + case RightId.POST_STORIES: + return me.rights.canPostStories; + case RightId.EDIT_STORIES: + return me.rights.canEditStories; + case RightId.DELETE_STORIES: + return me.rights.canDeleteStories; case RightId.REMAIN_ANONYMOUS: return me.rights.isAnonymous; case RightId.SEND_BASIC_MESSAGES: @@ -942,98 +1054,74 @@ private void buildCells () { args.mode == MODE_RESTRICTION ? args.senderId.getConstructor() == TdApi.MessageSenderChat.CONSTRUCTOR ? (tdlib.isChannel(((TdApi.MessageSenderChat) args.senderId).chatId) ? R.string.WhatThisChannelCanDo : R.string.WhatThisGroupCanDo) : R.string.WhatThisUserCanDo : R.string.WhatThisAdminCanDo)); items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); - boolean isChannel = tdlib.isChannel(args.chatId); - - @RightId int[] rightIds; + final boolean isChannel = tdlib.isChannel(args.chatId); + final ArrayList rightIdOptions = new ArrayList<>(12); if (args.mode == MODE_CHAT_PERMISSIONS) { - rightIds = new int[] { - RightId.SEND_BASIC_MESSAGES, - RightId.SEND_PHOTOS, - RightId.SEND_VIDEOS, - RightId.SEND_AUDIO, - RightId.SEND_DOCS, - RightId.SEND_VOICE_NOTES, - RightId.SEND_VIDEO_NOTES, - RightId.SEND_OTHER_MESSAGES, - RightId.SEND_POLLS, - RightId.EMBED_LINKS, - RightId.INVITE_USERS, - RightId.PIN_MESSAGES, - RightId.CHANGE_CHAT_INFO - }; + rightIdOptions.add(new RightOption(RightId.SEND_BASIC_MESSAGES)); + rightIdOptions.add(new RightOption(R.string.RightSendMedia, SEND_MEDIA_RIGHT_IDS)); + rightIdOptions.add(new RightOption(RightId.INVITE_USERS)); + rightIdOptions.add(new RightOption(RightId.PIN_MESSAGES)); + rightIdOptions.add(new RightOption(RightId.CHANGE_CHAT_INFO)); } else if (args.mode == MODE_RESTRICTION) { if (args.senderId.getConstructor() == TdApi.MessageSenderChat.CONSTRUCTOR) { - rightIds = new int[] { - RightId.SEND_BASIC_MESSAGES, - RightId.SEND_PHOTOS, - RightId.SEND_VIDEOS, - RightId.SEND_AUDIO, - RightId.SEND_DOCS, - RightId.SEND_VOICE_NOTES, - RightId.SEND_VIDEO_NOTES, - RightId.SEND_OTHER_MESSAGES, - RightId.SEND_POLLS, - RightId.EMBED_LINKS, - RightId.INVITE_USERS, - RightId.PIN_MESSAGES, - RightId.CHANGE_CHAT_INFO - }; + rightIdOptions.add(new RightOption(RightId.SEND_BASIC_MESSAGES)); + rightIdOptions.add(new RightOption(R.string.RightSendMedia, SEND_MEDIA_RIGHT_IDS)); + rightIdOptions.add(new RightOption(RightId.INVITE_USERS)); + rightIdOptions.add(new RightOption(RightId.PIN_MESSAGES)); + rightIdOptions.add(new RightOption(RightId.CHANGE_CHAT_INFO)); } else { - rightIds = new int[] { - RightId.READ_MESSAGES, - RightId.SEND_BASIC_MESSAGES, - RightId.SEND_PHOTOS, - RightId.SEND_VIDEOS, - RightId.SEND_AUDIO, - RightId.SEND_DOCS, - RightId.SEND_VOICE_NOTES, - RightId.SEND_VIDEO_NOTES, - RightId.SEND_OTHER_MESSAGES, - RightId.SEND_POLLS, - RightId.EMBED_LINKS, - RightId.INVITE_USERS, - RightId.PIN_MESSAGES, - RightId.CHANGE_CHAT_INFO - }; + rightIdOptions.add(new RightOption(RightId.READ_MESSAGES)); + rightIdOptions.add(new RightOption(RightId.SEND_BASIC_MESSAGES)); + rightIdOptions.add(new RightOption(R.string.RightSendMedia, SEND_MEDIA_RIGHT_IDS)); + rightIdOptions.add(new RightOption(RightId.INVITE_USERS)); + rightIdOptions.add(new RightOption(RightId.PIN_MESSAGES)); + rightIdOptions.add(new RightOption(RightId.CHANGE_CHAT_INFO)); } } else if (isChannel) { - rightIds = new int[] { - RightId.CHANGE_CHAT_INFO, - RightId.SEND_BASIC_MESSAGES, - RightId.EDIT_MESSAGES, - RightId.DELETE_MESSAGES, - RightId.INVITE_USERS, - RightId.MANAGE_VIDEO_CHATS, - RightId.ADD_NEW_ADMINS - }; + rightIdOptions.add(new RightOption(RightId.CHANGE_CHAT_INFO)); + rightIdOptions.add(new RightOption(R.string.RightMessages, MANAGE_CHANNEL_POSTS_IDS)); + rightIdOptions.add(new RightOption(RightId.INVITE_USERS)); + rightIdOptions.add(new RightOption(RightId.MANAGE_VIDEO_CHATS)); + rightIdOptions.add(new RightOption(RightId.ADD_NEW_ADMINS)); + rightIdOptions.add(new RightOption(R.string.RightStories, MANAGE_STORIES_RIGHT_IDS)); + } else if (isForum) { + rightIdOptions.add(new RightOption(RightId.CHANGE_CHAT_INFO)); + rightIdOptions.add(new RightOption( RightId.DELETE_MESSAGES)); + rightIdOptions.add(new RightOption(RightId.BAN_USERS)); + rightIdOptions.add(new RightOption(RightId.INVITE_USERS)); + rightIdOptions.add(new RightOption(RightId.PIN_MESSAGES)); + rightIdOptions.add(new RightOption(RightId.MANAGE_VIDEO_CHATS)); + rightIdOptions.add(new RightOption(RightId.MANAGE_TOPICS)); + rightIdOptions.add(new RightOption(RightId.REMAIN_ANONYMOUS)); + rightIdOptions.add(new RightOption(RightId.ADD_NEW_ADMINS)); } else { - rightIds = new int[] { - RightId.CHANGE_CHAT_INFO, - RightId.DELETE_MESSAGES, - RightId.BAN_USERS, - RightId.INVITE_USERS, - RightId.PIN_MESSAGES, - RightId.MANAGE_VIDEO_CHATS, - RightId.REMAIN_ANONYMOUS, - RightId.ADD_NEW_ADMINS - }; + rightIdOptions.add(new RightOption(RightId.CHANGE_CHAT_INFO)); + rightIdOptions.add(new RightOption(RightId.DELETE_MESSAGES)); + rightIdOptions.add(new RightOption(RightId.BAN_USERS)); + rightIdOptions.add(new RightOption(RightId.INVITE_USERS)); + rightIdOptions.add(new RightOption(RightId.PIN_MESSAGES)); + rightIdOptions.add(new RightOption(RightId.MANAGE_VIDEO_CHATS)); + rightIdOptions.add(new RightOption(RightId.REMAIN_ANONYMOUS)); + rightIdOptions.add(new RightOption(RightId.ADD_NEW_ADMINS)); } boolean first = true; int viewType = args.mode == MODE_CHAT_PERMISSIONS ? ListItem.TYPE_VALUED_SETTING_COMPACT_WITH_TOGGLER : ListItem.TYPE_RADIO_SETTING_WITH_NEGATIVE_STATE; - for (@RightId int rightId : rightIds) { + for (RightOption option: rightIdOptions) { if (first) { first = false; } else { items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); } - items.add(new ListItem( - viewType, - R.id.btn_togglePermission, 0, - stringForRightId(rightId, isChannel) - ).setIntValue(rightId) - .setBoolValue(getValueForId(rightId)) - ); + if (option.groupRightIds == null) { + final @RightId int rightId = option.singleRightId; + items.add(new ListItem(viewType, R.id.btn_togglePermission, iconForRightId(rightId), stringForRightId(rightId, isChannel)) + .setIntValue(rightId).setBoolValue(getValueForId(rightId))); + } else { + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT_WITH_TOGGLER, + R.id.btn_togglePermissionGroup, R.drawable.round_keyboard_arrow_right_24, option.name + ).setIntValue(option.groupRightIds[0]).setData(option).setBoolValue(getRightsGroupEnabledCount(option.groupRightIds) > 0)); + } } if (args.mode == MODE_RESTRICTION) { @@ -1279,6 +1367,14 @@ private void setCanViewMessages (boolean value) { private void toggleValueForRightId (@RightId int id) { final boolean newValue = !getValueForId(id); + setValueForRightId(id, newValue); + } + + private void setValueForRightId (@RightId int id, boolean newValue) { + if (getValueForId(id) == newValue) { + return; + } + switch (id) { case RightId.READ_MESSAGES: { setCanViewMessages(newValue); @@ -1378,6 +1474,18 @@ private void toggleValueForRightId (@RightId int id) { case RightId.MANAGE_VIDEO_CHATS: targetAdmin.rights.canManageVideoChats = newValue; break; + case RightId.MANAGE_TOPICS: + targetAdmin.rights.canManageTopics = newValue; + break; + case RightId.POST_STORIES: + targetAdmin.rights.canPostStories = newValue; + break; + case RightId.EDIT_STORIES: + targetAdmin.rights.canEditStories = newValue; + break; + case RightId.DELETE_STORIES: + targetAdmin.rights.canDeleteStories = newValue; + break; case RightId.REMAIN_ANONYMOUS: targetAdmin.rights.isAnonymous = newValue; break; @@ -1425,9 +1533,19 @@ private void updateValues () { item.setBoolValue(value); adapter.updateValuedSettingByPosition(i); } + } else if (item.getId() == R.id.btn_togglePermissionGroup) { + final RightOption option = (RightOption) item.getData(); + if (option != null && option.groupRightIds != null) { + final boolean value = getRightsGroupEnabledCount(option.groupRightIds) > 0; + if (value != item.getBoolValue()) { + item.setBoolValue(value); + adapter.updateValuedSettingByPosition(i); + } + } } i++; } + adapter.updateAllValuedSettingsById(R.id.btn_togglePermissionGroup); } private boolean checkDefaultRight (@RightId int id) { @@ -1496,6 +1614,14 @@ private boolean getValueForId (@RightId int id) { return targetAdmin.rights.canPromoteMembers; case RightId.MANAGE_VIDEO_CHATS: return targetAdmin.rights.canManageVideoChats; + case RightId.MANAGE_TOPICS: + return targetAdmin.rights.canManageTopics; + case RightId.POST_STORIES: + return targetAdmin.rights.canPostStories; + case RightId.EDIT_STORIES: + return targetAdmin.rights.canEditStories; + case RightId.DELETE_STORIES: + return targetAdmin.rights.canDeleteStories; case RightId.REMAIN_ANONYMOUS: return targetAdmin.rights.isAnonymous; case RightId.EDIT_MESSAGES: @@ -1544,12 +1670,66 @@ private boolean getValueForId (@RightId int id) { return R.string.RightEditMessages; case RightId.MANAGE_VIDEO_CHATS: return isChannel ? R.string.RightLiveStreams : R.string.RightVoiceChats; + case RightId.MANAGE_TOPICS: + return R.string.RightTopics; + case RightId.POST_STORIES: + return R.string.RightStoriesPost; + case RightId.EDIT_STORIES: + return R.string.RightStoriesEdit; + case RightId.DELETE_STORIES: + return R.string.RightStoriesDelete; case RightId.REMAIN_ANONYMOUS: return R.string.RightAnonymous; } throw new UnsupportedOperationException(Lang.getResourceEntryName(id)); } + private @DrawableRes int iconForRightId (@RightId int id) { + switch (id) { + case RightId.READ_MESSAGES: + return R.drawable.baseline_eye_off_24; + case RightId.SEND_BASIC_MESSAGES: + return R.drawable.baseline_format_text_24; + case RightId.CHANGE_CHAT_INFO: + case RightId.EDIT_MESSAGES: + case RightId.EDIT_STORIES: + return R.drawable.baseline_edit_24; + case RightId.DELETE_STORIES: + case RightId.DELETE_MESSAGES: + return R.drawable.baseline_delete_24; + case RightId.BAN_USERS: + return R.drawable.baseline_block_24; + case RightId.INVITE_USERS: + return R.drawable.baseline_person_add_24; + case RightId.ADD_NEW_ADMINS: + return R.drawable.baseline_stars_24; + case RightId.PIN_MESSAGES: + return R.drawable.deproko_baseline_pin_24; + case RightId.REMAIN_ANONYMOUS: + return R.drawable.dot_baseline_acc_anon_24; + + case RightId.MANAGE_VIDEO_CHATS: + return R.drawable.baseline_video_chat_24; + case RightId.MANAGE_TOPICS: + return R.drawable.baseline_format_list_bulleted_type_24; + + case RightId.POST_STORIES: // todo + + + case RightId.SEND_AUDIO: + case RightId.SEND_DOCS: + case RightId.SEND_PHOTOS: + case RightId.SEND_VIDEOS: + case RightId.SEND_VOICE_NOTES: + case RightId.SEND_VIDEO_NOTES: + case RightId.SEND_OTHER_MESSAGES: + case RightId.SEND_POLLS: + case RightId.EMBED_LINKS: + default: + return 0; + } + } + private boolean canViewOrEditCustomTitle () { Args args = getArgumentsStrict(); if (tdlib.isChannel(args.chatId)) @@ -1594,4 +1774,109 @@ public CharSequence getName () { throw new AssertionError(); } + private boolean hasAccessToEditRightsGroup (int[] rights) { + for (int right : rights) { + if (hasAccessToEditRight(right)) { + return true; + } + } + return false; + } + + private void toggleRightsGroup (int[] rights) { + final int index = adapter.indexOfViewByIdAndValue(R.id.btn_togglePermissionGroup, rights[0]); + final ListItem item = adapter.getItem(index); + if (item == null) { + return; + } + + boolean newValue = !item.getBoolValue(); + for (int rightId : rights) { + boolean canEdit = hasAccessToEditRight(rightId); + if (canEdit) { + setValueForRightId(rightId, newValue); + } + } + } + + private int getRightsGroupEnabledCount (int[] rights) { + int res = 0; + for (int right : rights) { + res += getValueForId(right) ? 1 : 0; + } + + return res; + } + + private void toggleRightsGroupVisibility (int[] rights) { + final int index = adapter.indexOfViewByIdAndValue(R.id.btn_togglePermissionGroup, rights[0]); + if (index == -1) return; + + final boolean sendMediaGroupIsVisible = indexOfViewByRightId(rights[0]) != -1; + + if (!sendMediaGroupIsVisible) { + final Args args = getArgumentsStrict(); + final boolean isChannel = tdlib.isChannel(args.chatId); + final int viewType = args.mode == MODE_CHAT_PERMISSIONS ? + ListItem.TYPE_VALUED_SETTING_COMPACT_WITH_TOGGLER : + ListItem.TYPE_RADIO_SETTING_WITH_NEGATIVE_STATE; + + ArrayList items = new ArrayList<>(rights.length * 2); + for (@RightId int rightId : rights) { + items.add(new ListItem(ListItem.TYPE_SEPARATOR)); + items.add(new ListItem(viewType, + R.id.btn_togglePermission, 0, + stringForRightId(rightId, isChannel) + ).setIntValue(rightId) + .setBoolValue(getValueForId(rightId)) + ); + } + adapter.addItems(index + 1, items.toArray(new ListItem[0])); + } else { + adapter.removeRange(index + 1, rights.length * 2); + } + adapter.updateValuedSettingByPosition(index); + } + + public static final int[] SEND_MEDIA_RIGHT_IDS = { + RightId.SEND_PHOTOS, + RightId.SEND_VIDEOS, + RightId.SEND_AUDIO, + RightId.SEND_DOCS, + RightId.SEND_VOICE_NOTES, + RightId.SEND_VIDEO_NOTES, + RightId.SEND_OTHER_MESSAGES, + RightId.SEND_POLLS, + RightId.EMBED_LINKS, + }; + + private static final int[] MANAGE_CHANNEL_POSTS_IDS = { + RightId.SEND_BASIC_MESSAGES, + RightId.EDIT_MESSAGES, + RightId.DELETE_MESSAGES + }; + + private static final int[] MANAGE_STORIES_RIGHT_IDS = { + RightId.POST_STORIES, + RightId.EDIT_STORIES, + RightId.DELETE_STORIES + }; + + private static class RightOption { + public final @RightId int singleRightId; + public final @Nullable @RightId int[] groupRightIds; + public final @StringRes int name; + + public RightOption (@RightId int singleRightId) { + this.singleRightId = singleRightId; + this.groupRightIds = null; + this.name = -1; + } + + public RightOption (@StringRes int name, @NonNull @RightId int[] groupRightIds) { + this.groupRightIds = groupRightIds; + this.singleRightId = groupRightIds[0]; + this.name = name; + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EditSessionController.java b/app/src/main/java/org/thunderdog/challegram/ui/EditSessionController.java index c79546ab7c..0b8dd93695 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/EditSessionController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/EditSessionController.java @@ -22,6 +22,7 @@ import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.base.SettingView; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.navigation.DoubleHeaderView; import org.thunderdog.challegram.telegram.Tdlib; @@ -121,7 +122,7 @@ public void onClick (View v) { protected boolean onDoneClick () { setDoneInProgress(true); - List> functions = new ArrayList<>(); + List> functions = new ArrayList<>(); if (allowSecretChats != session.canAcceptSecretChats) { functions.add(new TdApi.ToggleSessionCanAcceptSecretChats(session.id, allowSecretChats)); @@ -131,16 +132,12 @@ protected boolean onDoneClick () { functions.add(new TdApi.ToggleSessionCanAcceptCalls(session.id, allowCalls)); } - tdlib.sendAll(functions.toArray(new TdApi.Function[0]), (obj) -> { - - }, () -> { - runOnUiThreadOptional(() -> { - session.canAcceptSecretChats = allowSecretChats; - session.canAcceptCalls = allowCalls; - getArgumentsStrict().sessionChangeListener.runWithData(session); - navigateBack(); - }); - }); + tdlib.sendAll(TD.toArray(functions), obj -> { }, () -> runOnUiThreadOptional(() -> { + session.canAcceptSecretChats = allowSecretChats; + session.canAcceptCalls = allowCalls; + getArgumentsStrict().sessionChangeListener.runWithData(session); + navigateBack(); + })); return true; } @@ -217,9 +214,9 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_sessionPlatform, SessionIconKt.asIcon(session), (session.platform + " " + session.systemVersion).trim(), false)); items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_sessionCountry, R.drawable.baseline_location_on_24, StringUtils.isEmpty(session.country) ? Lang.getString(R.string.SessionLocationUnknown) : session.country, false)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_sessionCountry, R.drawable.baseline_location_on_24, StringUtils.isEmpty(session.location) ? Lang.getString(R.string.SessionLocationUnknown) : session.location, false)); items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_sessionIp, R.drawable.baseline_router_24, StringUtils.isEmpty(session.ip) ? Lang.getString(R.string.SessionIpUnknown) : session.ip, false)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_sessionIp, R.drawable.baseline_router_24, StringUtils.isEmpty(session.ipAddress) ? Lang.getString(R.string.SessionIpUnknown) : session.ipAddress, false)); items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); if (!session.isPasswordPending) { diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EditTextController.java b/app/src/main/java/org/thunderdog/challegram/ui/EditTextController.java new file mode 100644 index 0000000000..eecc280d45 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/ui/EditTextController.java @@ -0,0 +1,161 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 24/12/2023 at 22:32 + */ +package org.thunderdog.challegram.ui; + +import android.content.Context; +import android.text.InputFilter; +import android.text.InputType; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.DrawableRes; +import androidx.annotation.IdRes; +import androidx.recyclerview.widget.RecyclerView; + +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.emoji.EmojiFilter; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.util.CharacterStyleFilter; +import org.thunderdog.challegram.widget.DoneButton; +import org.thunderdog.challegram.widget.MaterialEditTextGroup; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import me.vkryl.android.text.CodePointCountFilter; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.StringUtils; + +public class EditTextController extends EditBaseController { + public interface Delegate { + @IdRes int getId (); + default @DrawableRes int getDoneIcon () { + return 0; + } + CharSequence getName (); + CharSequence getHint (); + CharSequence getDescription (); + default String getCurrentValue () { + return null; + } + default void onValueChanged (EditTextController controller, String value) { + controller.setDoneVisible(allowEmptyValue() || !StringUtils.isEmptyOrBlank(value)); + } + boolean onDonePressed (EditTextController controller, DoneButton button, String value); + default int getMaxLength () { + return 0; + } + default boolean allowEmptyValue () { + return true; + } + default boolean needFocusInput () { + return true; + } + } + + private Delegate delegate; + + public final void setDelegate (Delegate delegate) { + this.delegate = delegate; + } + + public EditTextController (Context context, Tdlib tdlib) { + super(context, tdlib); + } + + @Override + public int getId () { + return delegate.getId(); + } + + @Override + public CharSequence getName () { + return delegate.getName(); + } + + private SettingsAdapter adapter; + private String currentValue; + + @Override + protected void onCreateView (Context context, FrameLayoutFix contentView, RecyclerView recyclerView) { + final int maxLength = delegate.getMaxLength(); + adapter = new SettingsAdapter(this) { + @Override + protected void modifyEditText (ListItem item, ViewGroup parent, MaterialEditTextGroup editText) { + editText.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); + Views.setSingleLine(editText.getEditText(), false); + if (maxLength > 0) { + editText.setMaxLength(maxLength); + } + } + }; + + currentValue = delegate.getCurrentValue(); + CharSequence hint = delegate.getHint(); + ListItem item = new ListItem(maxLength > 0 ? ListItem.TYPE_EDITTEXT_COUNTERED : ListItem.TYPE_EDITTEXT, R.id.input, 0, hint, false); + if (!StringUtils.isEmpty(currentValue)) { + item.setStringValue(currentValue); + } + + List inputFilters = new ArrayList<>(); + if (maxLength > 0) { + inputFilters.add(new CodePointCountFilter(maxLength)); + } + Collections.addAll(inputFilters, + new EmojiFilter(), + new CharacterStyleFilter() + ); + item.setInputFilters(inputFilters.toArray(new InputFilter[0])); + + List items = new ArrayList<>(); + items.add(item); + + CharSequence description = delegate.getDescription(); + if (!StringUtils.isEmpty(description)) { + ListItem descriptionItem = new ListItem(ListItem.TYPE_DESCRIPTION, R.id.description, 0, description, false) + .setTextColorId(ColorId.textLight); + items.add(descriptionItem); + } + + adapter.setTextChangeListener((id, item1, v, text) -> { + currentValue = text; + delegate.onValueChanged(this, text); + }); + adapter.setLockFocusOn(this, delegate.needFocusInput()); + adapter.setItems(items, false); + + recyclerView.setAdapter(adapter); + recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER); + + setDoneVisible(delegate.allowEmptyValue()); + @DrawableRes int iconRes = delegate.getDoneIcon(); + if (iconRes != 0) { + setDoneIcon(iconRes); + } + } + + @Override + protected void onProgressStateChanged (boolean inProgress) { + adapter.updateLockEditTextById(R.id.input, inProgress ? currentValue : null); + } + + @Override + protected boolean onDoneClick () { + return delegate.onDonePressed(this, getDoneButton(), currentValue); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EmojiListController.java b/app/src/main/java/org/thunderdog/challegram/ui/EmojiListController.java index dfd10306b3..e434e38f08 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/EmojiListController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/EmojiListController.java @@ -15,52 +15,72 @@ package org.thunderdog.challegram.ui; import android.content.Context; -import android.os.Build; -import android.text.TextUtils; -import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.GridLayoutManager; +import androidx.collection.LongSparseArray; import androidx.recyclerview.widget.RecyclerView; +import org.drinkless.tdlib.Client; +import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.attach.CustomItemAnimator; import org.thunderdog.challegram.component.chat.EmojiToneHelper; import org.thunderdog.challegram.component.chat.EmojiView; -import org.thunderdog.challegram.config.Config; -import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.component.emoji.MediaStickersAdapter; +import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGDefaultEmoji; +import org.thunderdog.challegram.data.TGStickerSetInfo; import org.thunderdog.challegram.emoji.Emoji; -import org.thunderdog.challegram.emoji.RecentEmoji; +import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.navigation.ViewController; -import org.thunderdog.challegram.support.RippleSupport; -import org.thunderdog.challegram.telegram.TGLegacyManager; +import org.thunderdog.challegram.telegram.StickersListener; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; -import org.thunderdog.challegram.theme.Theme; -import org.thunderdog.challegram.theme.ThemeId; import org.thunderdog.challegram.tool.EmojiData; -import org.thunderdog.challegram.tool.Fonts; -import org.thunderdog.challegram.tool.Screen; -import org.thunderdog.challegram.tool.Views; -import org.thunderdog.challegram.v.CustomRecyclerView; -import org.thunderdog.challegram.v.RtlGridLayoutManager; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.util.StickerSetsDataProvider; import org.thunderdog.challegram.widget.EmojiLayout; -import org.thunderdog.challegram.widget.NoScrollTextView; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutRecyclerController; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutSectionPager; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutTrendingController; +import org.thunderdog.challegram.widget.emoji.section.EmojiSection; import java.util.ArrayList; import me.vkryl.android.AnimatorUtils; -import me.vkryl.android.animator.FactorAnimator; import me.vkryl.android.widget.FrameLayoutFix; import me.vkryl.core.StringUtils; +import me.vkryl.core.collection.LongList; -public class EmojiListController extends ViewController implements View.OnClickListener, TGLegacyManager.EmojiLoadListener { - public EmojiListController (Context context, Tdlib tdlib) { +public class EmojiListController extends ViewController implements StickersListener { + + private static final int SECTION_STICKERS = 0; + private static final int SECTION_TRENDING = 2; + + private EmojiLayoutSectionPager contentView; + private final EmojiLayoutRecyclerController emojiController; + private final EmojiLayoutTrendingController trendingSetsController; + private final boolean onlyClassicEmoji; + + public EmojiListController (Context context, Tdlib tdlib, boolean onlyClassicEmoji) { super(context, tdlib); + this.onlyClassicEmoji = onlyClassicEmoji; + emojiController = new EmojiLayoutRecyclerController(context, tdlib, EmojiLayout.EMOJI_INSTALLED_CONTROLLER_ID); + emojiController.setItemWidth(8, 45); + emojiController.setOnlyClassicEmoji(onlyClassicEmoji); + emojiController.setStickerObjModifier(this::modifyStickerObj); + + trendingSetsController = new EmojiLayoutTrendingController(context, tdlib, EmojiLayout.EMOJI_TRENDING_CONTROLLER_ID); + trendingSetsController.setCallbacks(stickerSetsDataProvider(), new TdApi.StickerTypeCustomEmoji()); + trendingSetsController.stickerSets = new ArrayList<>(); + } + + public TGStickerObj modifyStickerObj (TGStickerObj sticker) { + sticker.setPreviewOptimizationMode(GifFile.OptimizationMode.EMOJI_PREVIEW); + return sticker; } @Override @@ -68,49 +88,38 @@ public int getId () { return R.id.controller_emoji; } - private CustomRecyclerView recyclerView; - private GridLayoutManager manager; - private EmojiAdapter adapter; - private int spanCount; + private MediaStickersAdapter adapter; + private MediaStickersAdapter trendingAdapter; private EmojiToneHelper toneHelper; private boolean useDarkMode; - private int calculateSpanCount () { - int width = 0; - if (recyclerView != null) { - width = recyclerView.getMeasuredWidth(); - } - if (width == 0) { - width = Screen.currentWidth(); - } - return Math.max(MINIMUM_EMOJI_COUNT, width / Screen.dp(48f)); - } - @Override protected void handleLanguageDirectionChange () { super.handleLanguageDirectionChange(); - if (recyclerView != null) - recyclerView.requestLayout(); + if (emojiController.recyclerView != null) + emojiController.recyclerView.requestLayout(); + if (trendingAdapter != null) + trendingAdapter.notifyDataSetChanged(); } @Override protected View onCreateView (Context context) { - manager = new RtlGridLayoutManager(context, spanCount = calculateSpanCount()).setAlignOnly(true); - toneHelper = new EmojiToneHelper(context, getArgumentsStrict().getToneDelegate(), this); - adapter = new EmojiAdapter(context, this, this); + toneHelper = new EmojiToneHelper(context, getArgumentsStrict().getToneDelegate(), tdlib, this); + toneHelper.setOnCustomEmojiSelectedListener(this::onCustomEmojiSelected); + adapter = new MediaStickersAdapter(this, emojiController, false, this, null, false, toneHelper); + adapter.setClassicEmojiClickListener(this::onClassicEmojiClick); + adapter.setRepaintingColorId(ColorId.text); this.useDarkMode = getArgumentsStrict().useDarkMode(); - recyclerView = (CustomRecyclerView) Views.inflate(context(), R.layout.recycler_custom, getArguments()); - recyclerView.setHasFixedSize(true); - recyclerView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - recyclerView.setLayoutManager(manager); - recyclerView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? View.OVER_SCROLL_IF_CONTENT_SCROLLS :View.OVER_SCROLL_NEVER); - recyclerView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 140l)); - recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + emojiController.setArguments(getArguments()); + emojiController.setAdapter(adapter); + emojiController.getValue(); + emojiController.recyclerView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 140L)); + emojiController.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onScrollStateChanged (RecyclerView recyclerView, int newState) { + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { boolean isScrolling = newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING; if (getArguments() != null && getArguments().getCurrentItem() == 0) { getArguments().setIsScrolling(isScrolling); @@ -118,77 +127,104 @@ public void onScrollStateChanged (RecyclerView recyclerView, int newState) { } @Override - public void onScrolled (RecyclerView recyclerView, int dx, int dy) { - if (getArguments() != null && getArguments().isWatchingMovements() && getArguments().getCurrentItem() == 0) { + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (emojiController.isNeedIgnoreScroll()) return; + + if (contentView.isSectionStable() && contentView.getCurrentSection() == SECTION_STICKERS && getArguments() != null && getArguments().isWatchingMovements() && getArguments().getCurrentItem() == 0) { + int y = emojiController.getStickersScrollY(false); + getArguments().moveHeader(y); + getArguments().setCurrentStickerSectionByPosition(EmojiLayout.EMOJI_INSTALLED_CONTROLLER_ID, emojiController.getStickerSetSection(EmojiLayout.getHeaderSize() / 2), true, true); + // getArguments().onSectionScroll(EmojiMediaType.STICKER, dy != 0); + } + + /*if (getArguments() != null && getArguments().isWatchingMovements() && getArguments().getCurrentItem() == 0) { getArguments().onScroll(getCurrentScrollY()); - if (lastScrollAnimator == null || !lastScrollAnimator.isAnimating()) { - getArguments().setCurrentEmojiSection(getCurrentSection()); + if (!emojiController.scrollAnimationIsActive()) { + getArguments().setCurrentEmojiSection(emojiController.getStickerSetSection()); } - } + }*/ } }); - manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + + trendingAdapter = new MediaStickersAdapter(this, trendingSetsController, false, this); + trendingAdapter.setItem(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_PROGRESS)); + trendingSetsController.setArguments(getArguments()); + trendingSetsController.setAdapter(trendingAdapter); + trendingSetsController.setSpanCount(8); + + contentView = new EmojiLayoutSectionPager(context) { @Override - public int getSpanSize (int position) { - return adapter.items.get(position).viewType == Item.TYPE_EMOJI ? 1 : spanCount; + protected View getSectionView (int section) { + if (section == SECTION_STICKERS) { + return emojiController.recyclerView; + } else if (section == SECTION_TRENDING) { + initTrending(); + return trendingSetsController.recyclerView; + /*return trendingView;*/ + } + return null; } - }); - recyclerView.setAdapter(adapter); - TGLegacyManager.instance().addEmojiListener(this); - Emoji.instance().addEmojiChangeListener(adapter); + @Override + protected void onSectionChangeStart (int prevSection, int nextSection, int stickerSetSection) { + if (getArguments() != null && stickerSetSection != -1) { + getArguments().setCurrentStickerSectionByPosition(EmojiLayout.EMOJI_INSTALLED_CONTROLLER_ID, nextSection == SECTION_STICKERS ? stickerSetSection : 0, nextSection == SECTION_STICKERS, true); + } + } - return recyclerView; - } + @Override + protected void onSectionChangeEnd (int prevSection, int currentSection) { + if (getArguments() != null) { + getArguments().resetScrollState(false); + } - public void resetRecentEmoji () { - if (adapter != null) { - adapter.resetRecents(); - } + if (prevSection == SECTION_TRENDING && currentSection != SECTION_TRENDING) { + trendingSetsController.applyScheduledFeaturedSets(); + } + } + }; + contentView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + contentView.init(SECTION_STICKERS); + + buildEmojis(); + loadStickers(); + trendingSetsController.loadTrending(0, 20, 0); + + return contentView; } - @Override - public void onEmojiUpdated (boolean isPackSwitch) { - Views.invalidateChildren(recyclerView); + public void resetRecentEmoji () { + emojiController.onResetRecentEmoji(); } @Override public void destroy () { super.destroy(); - TGLegacyManager.instance().removeEmojiListener(this); - Emoji.instance().removeEmojiChangeListener(adapter); - Views.destroyRecyclerView(recyclerView); - } - - public int getCurrentSection () { - View view = recyclerView.findChildViewUnder(0, EmojiLayout.getHeaderSize() + EmojiLayout.getHeaderPadding()); - if (view != null) { - int i = recyclerView.getChildAdapterPosition(view); - if (i != -1) { - return adapter.getSectionForIndex(i); - } + if (!onlyClassicEmoji) { + tdlib.listeners().unsubscribeFromStickerUpdates(this); } - return -1; + emojiController.destroy(); + trendingSetsController.destroy(); } public int getCurrentScrollY () { - int i = manager.findFirstVisibleItemPosition(); - if (i == -1) { - return 0; + switch (contentView.getCurrentSection()) { + case SECTION_STICKERS: { + return emojiController.getStickersScrollY(false); + } + case SECTION_TRENDING: { + return trendingSetsController.getStickersScrollY(false); + } } - - View view = manager.findViewByPosition(i); - int addition = view != null ? -view.getTop() : 0; - - return addition + adapter.measureScrollTop(i, spanCount); + return -1; } public void invalidateItems () { - final int first = manager.findFirstVisibleItemPosition(); - final int last = manager.findLastVisibleItemPosition(); + final int first = emojiController.getManager().findFirstVisibleItemPosition(); + final int last = emojiController.getManager().findLastVisibleItemPosition(); for (int i = first; i <= last; i++) { - View view = manager.findViewByPosition(i); + View view = emojiController.getManager().findViewByPosition(i); if (view != null) { view.invalidate(); } else { @@ -197,8 +233,7 @@ public void invalidateItems () { } } - @Override - public void onClick (View v) { + private void onClassicEmojiClick (View v) { if (!(v instanceof EmojiView)) { return; } @@ -219,440 +254,427 @@ public void onClick (View v) { } } - public void checkSpanCount () { - if (manager != null) { - int spanCount = calculateSpanCount(); - if (this.spanCount != spanCount) { - this.spanCount = spanCount; - manager.setSpanCount(spanCount); - } + private void onCustomEmojiSelected (TGStickerObj stickerObj) { + if (getArguments() != null) { + getArguments().onEnterCustomEmoji(stickerObj); } } - private FactorAnimator lastScrollAnimator; + public void checkSpanCount () { + emojiController.checkSpanCount(); + } public void showEmojiSection (int section) { - recyclerView.stopScroll(); - - final int scrollTop; - int sectionPosition; - - if (section == 0) { - scrollTop = 0; - sectionPosition = 0; - } else { - sectionPosition = 1; - if (adapter.recentItemCount > 0) { - for (int i = 0; i < section; i++) { - if (i == 0) { - sectionPosition += adapter.recentItemCount; - } else { - sectionPosition += EmojiData.dataColored[i - 1].length + 1; - } - } - } else { - for (int i = 0; i < section; i++) { - sectionPosition += EmojiData.dataColored[i].length + 1; - } + if (contentView.canChangeSection()) { + TGStickerSetInfo info = emojiController.getStickerSetBySectionIndex(section); + int position = section != 0 && info != null ? info.getStartIndex() : 0; + + emojiController.scrollToStickerSet(position, false, contentView.getCurrentSection() != SECTION_TRENDING); + if (contentView.getCurrentSection() == SECTION_TRENDING) { + showStickers(); } - scrollTop = adapter.measureScrollTop(sectionPosition, spanCount) - EmojiLayout.getHeaderSize() - EmojiLayout.getHeaderPadding(); } + } - final int currentSection = getCurrentSection(); + private void buildEmojis () { + ArrayList items = new ArrayList<>(1); + ArrayList emojiPacks = new ArrayList<>(8); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || Math.abs(section - currentSection) > 4) { // TODO make better smooth scroller - if (getArguments() != null) { - getArguments().setIgnoreMovement(true); - } + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_KEYBOARD_TOP)); - if (section == 0) { - manager.scrollToPositionWithOffset(0, 0); - } else { - manager.scrollToPositionWithOffset(sectionPosition, EmojiLayout.getHeaderSize() + EmojiLayout.getHeaderPadding()); - } + ArrayList recentEmojiItems = emojiController.makeRecentEmojiItems(); + if (!recentEmojiItems.isEmpty()) { + TGStickerSetInfo pack = TGStickerSetInfo.fromEmojiSection(tdlib, EmojiSection.SECTION_EMOJI_RECENT, -1, recentEmojiItems.size()); + pack.setStartIndex(items.size()); + pack.setIsRecent(); + emojiPacks.add(pack); + items.addAll(recentEmojiItems); + } - if (getArguments() != null) { - getArguments().setIgnoreMovement(false); + for (int i = 0; i < EmojiData.dataColored.length; i++) { + String[] emoji = EmojiData.dataColored[i]; + TGStickerSetInfo pack = null; + switch (i) { + case 0: pack = TGStickerSetInfo.fromEmojiSection(tdlib, EmojiSection.SECTION_EMOJI_SMILEYS, R.string.SmileysAndPeople, emoji.length); break; + case 1: pack = TGStickerSetInfo.fromEmojiSection(tdlib, EmojiSection.SECTION_EMOJI_ANIMALS, R.string.AnimalsAndNature, emoji.length); break; + case 2: pack = TGStickerSetInfo.fromEmojiSection(tdlib, EmojiSection.SECTION_EMOJI_FOOD, R.string.FoodDrink, emoji.length); break; + case 3: pack = TGStickerSetInfo.fromEmojiSection(tdlib, EmojiSection.SECTION_EMOJI_TRAVEL, R.string.TravelAndPlaces, emoji.length); break; + case 4: pack = TGStickerSetInfo.fromEmojiSection(tdlib, EmojiSection.SECTION_EMOJI_SYMBOLS, R.string.SymbolsAndObjects, emoji.length); break; + case 5: pack = TGStickerSetInfo.fromEmojiSection(tdlib, EmojiSection.SECTION_EMOJI_FLAGS, R.string.Flags, emoji.length); break; + } + if (pack != null) { + pack.setStartIndex(items.size()); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER, pack)); + emojiPacks.add(pack); + } + items.ensureCapacity(items.size() + emoji.length + 1); + for (String emojiCode : emoji) { + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_DEFAULT_EMOJI, new TGDefaultEmoji(emojiCode))); } - - return; } - final int currentScrollTop = getCurrentScrollY(); - final int scrollDiff = scrollTop - currentScrollTop; - final int[] totalScrolled = new int[1]; + emojiController.setDefaultEmojiPacks(emojiPacks, items); + } - if (lastScrollAnimator != null) { - lastScrollAnimator.cancel(); - } - recyclerView.setScrollDisabled(true); - if (getArguments() != null) { - getArguments().setIgnoreMovement(true); - getArguments().setCurrentEmojiSection(section); + /* * */ + + // private RecyclerView trendingView; + + public void showStickers () { + if (contentView.canChangeSection()) { + contentView.changeSection(SECTION_STICKERS, false, -1); } - lastScrollAnimator = new FactorAnimator(0, new FactorAnimator.Target() { - @Override - public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { - int diff = (int) ((float) scrollDiff * factor); - recyclerView.scrollBy(0, diff - totalScrolled[0]); - totalScrolled[0] = diff; - } + } - @Override - public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { - recyclerView.setScrollDisabled(false); - if (getArguments() != null) { - getArguments().setIgnoreMovement(false); + public void showTrending () { + if (contentView.canChangeSection()) { + initTrending(); + if (contentView.getCurrentSection() == SECTION_TRENDING) { + trendingSetsController.recyclerView.smoothScrollBy(0, -trendingSetsController.getStickersScrollY(false)); + } else { + contentView.changeSection(SECTION_TRENDING, true, -1); + if (getArguments() != null && getArguments().getCurrentItem() == 0) { + getArguments().setCurrentStickerSectionByPosition(EmojiLayout.EMOJI_INSTALLED_CONTROLLER_ID, 0, false, true); } } - }, AnimatorUtils.ACCELERATE_DECELERATE_INTERPOLATOR, Math.min(450, Math.max(250, Math.abs(currentSection - section) * 150))); - lastScrollAnimator.animateTo(1f); - // recyclerView.smoothScrollBy(0, scrollDiff); + } } - private static final int MINIMUM_EMOJI_COUNT = 8; - - private static class Item { - public static final int TYPE_HEADER = 0; - public static final int TYPE_EMOJI = 1; - public static final int TYPE_OFFSET = 2; - - public final int viewType; - public final int strRes; - public final String emoji; - public final int emojiColorState; + private void initTrending () { + if (trendingSetsController.recyclerView == null) { + trendingSetsController.getValue(); + trendingSetsController.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { + if (contentView.isSectionStable() && contentView.getCurrentSection() == SECTION_TRENDING && getArguments() != null && getArguments().getCurrentItem() == 0) { + boolean isScrolling = newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING; + getArguments().setIsScrolling(isScrolling); + } + } - public Item (int viewType, int strRes) { - this.viewType = viewType; - this.strRes = strRes; - this.emoji = null; - this.emojiColorState = EmojiData.STATE_NO_COLORS; + @Override + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (contentView.isSectionStable() && contentView.getCurrentSection() == SECTION_TRENDING && getArguments() != null && getArguments().getCurrentItem() == 0) { + trendingSetsController.onScrolledImpl(dy, false); + } + } + }); } + } - public Item (int viewType, String emoji) { - this.viewType = viewType; - this.emoji = emoji; - this.emojiColorState = EmojiData.instance().getEmojiColorState(emoji); - this.strRes = 0; - } + /* * */ - public boolean canBeColored () { - return emojiColorState != EmojiData.STATE_NO_COLORS; - } - } + private boolean loadingStickers; - private static class ItemHolder extends RecyclerView.ViewHolder { - public ItemHolder (View itemView) { - super(itemView); + private void loadStickers () { + if (!loadingStickers && !onlyClassicEmoji) { + loadingStickers = true; + tdlib.client().send(new TdApi.GetInstalledStickerSets(new TdApi.StickerTypeCustomEmoji()), stickerSetsHandler()); } } - private static class EmojiAdapter extends RecyclerView.Adapter implements Emoji.EmojiChangeListener { - private final Context context; - private final View.OnClickListener onClickListener; - private final ArrayList items; + private Client.ResultHandler stickerSetsHandler () { + return object -> { + switch (object.getConstructor()) { + case TdApi.StickerSets.CONSTRUCTOR: { + TdApi.StickerSetInfo[] rawStickerSets = ((TdApi.StickerSets) object).sets; + + final ArrayList stickerSets = new ArrayList<>(rawStickerSets.length); + final ArrayList items = new ArrayList<>(); + + if (rawStickerSets.length > 0) { + int startIndex = this.adapter.getItemCount(); + + for (TdApi.StickerSetInfo rawInfo : rawStickerSets) { + TGStickerSetInfo info = new TGStickerSetInfo(tdlib, rawInfo); + if (info.getSize() == 0) { + continue; + } + stickerSets.add(info); + info.setStartIndex(startIndex); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER, info)); + for (int i = 0; i < rawInfo.size; i++) { + TGStickerObj sticker = new TGStickerObj(tdlib, i < rawInfo.covers.length ? rawInfo.covers[i] : null, null, rawInfo.stickerType); + sticker.setStickerSetId(rawInfo.id, null); + sticker.setDataProvider(stickerSetsDataProvider()); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, sticker)); + } + startIndex += rawInfo.size + 1; + } + } - private int recentItemCount; + runOnUiThreadOptional(() -> { + if (getArguments() != null) { + getArguments().setEmojiPacks(stickerSets); + } + setStickers(stickerSets, items); + }); - public int getSectionForIndex (int position) { - if (position == 0) { - return 0; - } - position--; - if (position < recentItemCount) { - return 0; - } - position -= recentItemCount; - for (int i = 0; i < EmojiData.dataColored.length && position >= 0; i++) { - int itemCount = EmojiData.dataColored[i].length + 1; - if (position >= 0 && position < itemCount) { - return ++i; + break; + } + case TdApi.Error.CONSTRUCTOR: { + UI.showError(object); + break; } - position -= itemCount; - } - return 0; - } - - public int measureScrollTop (int position, int spanCount) { - if (position == 0) { - return 0; } + }; + } - position--; - - int scrollY = EmojiLayout.getHeaderSize() + EmojiLayout.getHeaderPadding(); - /*if (position >= 0 && position < spanCount) { - return scrollY; - }*/ + private void setStickers (ArrayList stickerSets, ArrayList items) { + this.emojiController.setStickers(stickerSets, items); + this.loadingStickers = false; + if (stickerSetsDataProvider != null) { + stickerSetsDataProvider.clear(); + } + if (!onlyClassicEmoji) { + tdlib.listeners().subscribeToStickerUpdates(this); + } + } - int recentRowCount = (int) Math.ceil((double) Math.min(this.recentItemCount, position) / (double) spanCount); - scrollY += recentRowCount * (Screen.currentWidth() / spanCount); - if (position >= 0 && position < recentRowCount) { - return scrollY; - } + public void applyScheduledChanges () { + trendingSetsController.applyScheduledFeaturedSets(); + } - position -= this.recentItemCount; - for (int i = 0; i < EmojiData.dataColored.length && position > 0; i++) { - scrollY += Screen.dp(32f); // header - position--; - if (position > 0) { - int rowCount = (int) Math.ceil((double) Math.min(EmojiData.dataColored[i].length, position) / (double) spanCount); - scrollY += rowCount * (Screen.currentWidth() / spanCount); - position -= EmojiData.dataColored[i].length; - } + public boolean showStickerSet (TGStickerSetInfo stickerSet) { + if (contentView.canChangeSection()) { + int i = emojiController.indexOfStickerSet(stickerSet); + if (i != -1) { + emojiController.scrollToStickerSet(i, false, contentView.getCurrentSection() == SECTION_STICKERS && contentView.isAnimationNotActive()); + return contentView.changeSection(SECTION_STICKERS, false, emojiController.indexOfStickerSetByAdapterPosition(i)); } - - return scrollY; } + return false; + } - private final EmojiListController parent; - - public EmojiAdapter (Context context, View.OnClickListener onClickListener, EmojiListController parent) { - this.context = context; - this.onClickListener = onClickListener; - this.parent = parent; - this.items = new ArrayList<>(); - this.items.add(new Item(Item.TYPE_OFFSET, 0)); + /* Sticker updates */ - setRecents(); + private boolean applyingChanges; + private ArrayList pendingChanges; - int index = 0; - for (String[] emoji : EmojiData.dataColored) { - switch (index++) { - case 0: { - items.add(new Item(Item.TYPE_HEADER, R.string.SmileysAndPeople)); - break; - } - case 1: { - items.add(new Item(Item.TYPE_HEADER, R.string.AnimalsAndNature)); - break; - } - case 2: { - items.add(new Item(Item.TYPE_HEADER, R.string.FoodDrink)); - break; - } - case 3: { - items.add(new Item(Item.TYPE_HEADER, R.string.TravelAndPlaces)); - break; - } - case 4: { - items.add(new Item(Item.TYPE_HEADER, R.string.SymbolsAndObjects)); - break; - } - case 5: { - items.add(new Item(Item.TYPE_HEADER, R.string.Flags)); - break; - } - } - items.ensureCapacity(items.size() + emoji.length + 1); - for (String emojiCode : emoji) { - items.add(new Item(Item.TYPE_EMOJI, emojiCode)); - } + private void setApplyingChanges (boolean applyingChanges) { + if (this.applyingChanges != applyingChanges) { + this.applyingChanges = applyingChanges; + if (!applyingChanges && pendingChanges != null && !pendingChanges.isEmpty()) { + do { + long[] stickerSetIds = pendingChanges.remove(0); + changeStickers(stickerSetIds); + } while (!pendingChanges.isEmpty() && !this.applyingChanges); } } + } - public int getHeaderItemCount () { - return 1; + private void reloadStickers () { + if (pendingChanges != null) { + pendingChanges.clear(); } + loadStickers(); // synonym + } - public void resetRecents () { - int oldRecentItemCount = recentItemCount; - if (recentItemCount > 0) { - for (int i = recentItemCount; i >= 1; i--) { - items.remove(i); - } - } - ArrayList recents = Emoji.instance().getRecents(); - recentItemCount = recents.size(); - items.ensureCapacity(items.size() + recentItemCount); - int i = 1; - for (RecentEmoji emoji : recents) { - items.add(i, new Item(Item.TYPE_EMOJI, emoji.emoji)); + private int getSystemSetsCount () { + int i = 0; + for (TGStickerSetInfo info : emojiController.stickerSets) { + if (info.isSystem() || info.isFakeClassicEmoji()) { i++; } + } + return i; + } - if (recentItemCount > oldRecentItemCount) { - notifyItemRangeInserted(1 + oldRecentItemCount, recentItemCount - oldRecentItemCount); - } else if (recentItemCount < oldRecentItemCount) { - notifyItemRangeRemoved(1 + recentItemCount, oldRecentItemCount - recentItemCount); + private boolean hasNoStickerSets () { + for (TGStickerSetInfo info : emojiController.stickerSets) { + if (!info.isFavorite() && !info.isRecent() && !info.isFakeClassicEmoji()) { + return false; } - notifyItemRangeChanged(1, Math.min(recentItemCount, oldRecentItemCount)); } + return true; + } - private void setRecents () { - ArrayList recents = Emoji.instance().getRecents(); - if (recents.isEmpty()) { - recentItemCount = 0; - } else { - items.ensureCapacity(recents.size()); - for (RecentEmoji recentEmoji : recents) { - items.add(new Item(Item.TYPE_EMOJI, recentEmoji.emoji)); - } - recentItemCount = recents.size(); + private void changeStickers (long[] stickerSetIds) { + trendingSetsController.updateTrendingSets(stickerSetIds); + if (applyingChanges) { + if (pendingChanges == null) { + pendingChanges = new ArrayList<>(); } + pendingChanges.add(stickerSetIds); + return; } - @Override - public void moveEmoji (int oldIndex, int newIndex) { - if (parent.getArguments() != null) { - parent.getArguments().setIgnoreMovement(true); - } - oldIndex += getHeaderItemCount(); - newIndex += getHeaderItemCount(); - Item item = items.remove(oldIndex); - items.add(newIndex, item); - notifyItemMoved(oldIndex, newIndex); - if (parent.getArguments() != null) { - parent.recyclerView.post(() -> parent.getArguments().setIgnoreMovement(false)); - } + if (hasNoStickerSets()) { + reloadStickers(); + return; } - @Override - public void addEmoji (int newIndex, RecentEmoji emoji) { - if (parent.getArguments() != null) { - parent.getArguments().setIgnoreMovement(true); - } - newIndex += getHeaderItemCount(); - recentItemCount++; - items.add(newIndex, new Item(Item.TYPE_EMOJI, emoji.emoji)); - notifyItemInserted(newIndex); - if (parent.getArguments() != null) { - parent.recyclerView.post(() -> parent.getArguments().setIgnoreMovement(false)); + // setId -> position in the current list + // LongSparseArray currentStickerSets = new LongSparseArray<>(stickersController.stickerSets.size()); + LongSparseArray removedStickerSets = new LongSparseArray<>(emojiController.stickerSets.size()); + // int currentSetIndex = 0; + for (TGStickerSetInfo stickerSet : emojiController.stickerSets) { + if (!stickerSet.isSystem() && !stickerSet.isFakeClassicEmoji()) { + removedStickerSets.put(stickerSet.getId(), stickerSet); + // currentStickerSets.put(stickerSet.getId(), currentSetIndex); + // currentSetIndex++; } } - @Override - public void replaceEmoji (int newIndex, RecentEmoji emoji) { - newIndex += getHeaderItemCount(); - items.set(newIndex, new Item(Item.TYPE_EMOJI, emoji.emoji)); - notifyItemChanged(newIndex); - } + // setId -> position in the future list + LongSparseArray positions = null; - @Override - public void onToneChanged (@Nullable String newDefaultTone) { - int firstVisiblePosition = parent.manager.findFirstVisibleItemPosition(); - int lastVisiblePosition = parent.manager.findLastVisibleItemPosition(); - if (firstVisiblePosition == -1 || lastVisiblePosition == -1) { - notifyItemRangeChanged(0, items.size()); - return; - } + // items that are not represented in the list (yet) + LongList futureItems = null; + + int setIndex = 0; + int totalIndex = 0; + int lastAddedIndex = -1; + boolean reloadAfterLocalChanges = false; + for (long setId : stickerSetIds) { + TGStickerSetInfo currentSet = removedStickerSets.get(setId); - int lastChangedPosition = -1; - int lastChangedCount = 0; - final ArrayList changes = new ArrayList<>(); - for (int i = firstVisiblePosition; i <= lastVisiblePosition; i++) { - Item item = items.get(i); - boolean changed = item.viewType == Item.TYPE_EMOJI && item.canBeColored(); - if (changed) { - if (lastChangedPosition == -1) { - lastChangedPosition = i; + if (currentSet == null) { + if (!reloadAfterLocalChanges) { + if (totalIndex != ++lastAddedIndex) { + reloadAfterLocalChanges = true; + } else { + if (futureItems == null) { + futureItems = new LongList(5); + } + futureItems.append(setId); } - lastChangedCount++; - } else if (lastChangedPosition != -1) { - changes.add(new int[] {lastChangedPosition, lastChangedCount}); - lastChangedPosition = -1; - lastChangedCount = 0; } - } - if (lastChangedPosition != -1) { - changes.add(new int[] {lastChangedPosition, lastChangedCount}); - } - for (int[] change : changes) { - if (change[1] == 1) { - notifyItemChanged(change[0]); - } else { - notifyItemRangeChanged(change[0], change[1]); + } else { + removedStickerSets.remove(setId); + + if (positions == null) { + positions = new LongSparseArray<>(5); } + + positions.put(setId, setIndex); + setIndex++; } - if (firstVisiblePosition > 0) { - notifyItemRangeChanged(0, firstVisiblePosition); - } - if (lastVisiblePosition < items.size() - 1) { - notifyItemRangeChanged(lastVisiblePosition + 1, items.size() - lastVisiblePosition); - } + totalIndex++; } - @Override - public void onCustomToneApplied (String emoji, @Nullable String newTone, @Nullable String[] newOtherTones) { - int firstVisiblePosition = parent.manager.findFirstVisibleItemPosition(); - int lastVisiblePosition = parent.manager.findLastVisibleItemPosition(); + // First, remove items + final int removedCount = removedStickerSets.size(); + for (int i = 0; i < removedCount; i++) { + TGStickerSetInfo stickerSet = removedStickerSets.valueAt(i); + emojiController.removeStickerSet(stickerSet); + } - int i = 0; - for (Item item : items) { - if (item.viewType == Item.TYPE_EMOJI && StringUtils.equalsOrBothEmpty(item.emoji, emoji)) { - View view = i >= firstVisiblePosition && i <= lastVisiblePosition ? parent.manager.findViewByPosition(i) : null; - if (!(view instanceof EmojiView) || !((EmojiView) view).applyTone(emoji, newTone, newOtherTones)) { - notifyItemChanged(i); - } + // Then, move items + if (positions != null && !emojiController.stickerSets.isEmpty() ) { + for (int j = 0; j < positions.size(); j++) { + long setId = positions.keyAt(j); + int newPosition = positions.valueAt(j); + int currentPosition = emojiController.indexOfStickerSetById(setId); + if (currentPosition == -1) { + throw new RuntimeException(); + } + if (currentPosition != newPosition) { + int systemSetsCount = getSystemSetsCount(); + emojiController.moveStickerSet(currentPosition + systemSetsCount, newPosition + systemSetsCount); } - i++; } } - @Override - @NonNull - public ItemHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { - switch (viewType) { - case Item.TYPE_EMOJI: { - EmojiView imageView = new EmojiView(context, this.parent.toneHelper); - imageView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - imageView.setOnClickListener(onClickListener); - Views.setClickable(imageView); - RippleSupport.setTransparentSelector(imageView); - return new ItemHolder(imageView); - } - case Item.TYPE_HEADER: { - TextView textView = new NoScrollTextView(context); - textView.setTypeface(Fonts.getRobotoMedium()); - if (this.parent.useDarkMode) { - textView.setTextColor(Theme.getColor(ColorId.textLight, ThemeId.NIGHT_BLACK)); + if (reloadAfterLocalChanges) { + reloadStickers(); + return; + } + + // Then, add items + if (futureItems != null) { + setApplyingChanges(true); + final long[] setIds = futureItems.get(); + final int addedCount = futureItems.size(); + final int[] index = new int[2]; + tdlib.client().send(new TdApi.GetStickerSet(setIds[index[0]]), new Client.ResultHandler() { + @Override + public void onResult (TdApi.Object object) { + if (object.getConstructor() == TdApi.StickerSet.CONSTRUCTOR) { + + TdApi.StickerSet rawStickerSet = (TdApi.StickerSet) object; + final TGStickerSetInfo stickerSet = new TGStickerSetInfo(tdlib, rawStickerSet); + final TdApi.Sticker[] stickers = ((TdApi.StickerSet) object).stickers; + + final int insertIndex = index[1]++; + + final ArrayList items; + items = new ArrayList<>(stickers.length + 1); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER, stickerSet)); + + int i = 0; + for (TdApi.Sticker sticker : stickers) { + TGStickerObj parsed = new TGStickerObj(tdlib, sticker, sticker.fullType, rawStickerSet.emojis[i].emojis); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, parsed)); + i++; + } + + runOnUiThreadOptional(() -> emojiController.addStickerSet(stickerSet, items, insertIndex + getSystemSetsCount())); + } + + if (++index[0] < addedCount) { + tdlib.client().send(new TdApi.GetStickerSet(setIds[index[0]]), this); } else { - textView.setTextColor(Theme.textDecentColor()); - this.parent.addThemeTextDecentColorListener(textView); + runOnUiThreadOptional(() -> setApplyingChanges(false)); } - textView.setGravity(Lang.gravity()); - textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15f); - textView.setSingleLine(true); - textView.setEllipsize(TextUtils.TruncateAt.END); - textView.setPadding(Screen.dp(14f), Screen.dp(5f), Screen.dp(14f), Screen.dp(5f)); - textView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(32f))); - return new ItemHolder(textView); - } - case Item.TYPE_OFFSET: { - View view = new View(context); - view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, EmojiLayout.getHeaderSize() + EmojiLayout.getHeaderPadding())); - return new ItemHolder(view); } - } - throw new RuntimeException("viewType == " + viewType); + }); } + } - @Override - public void onBindViewHolder (@NonNull ItemHolder holder, int position) { - switch (holder.getItemViewType()) { - case Item.TYPE_EMOJI: { - Item item = items.get(position); - boolean isRecent = position < getHeaderItemCount() + recentItemCount; - holder.itemView.setId(isRecent ? R.id.emoji_recent : R.id.emoji); - ((EmojiView) holder.itemView).setEmoji(item.emoji, item.emojiColorState); - break; - } - case Item.TYPE_HEADER: { - Views.setMediumText((TextView) holder.itemView, Lang.getString(items.get(position).strRes)); - break; - } + @Override + public void onTrendingStickersUpdated (final TdApi.StickerType stickerType, final TdApi.TrendingStickerSets stickerSets, int unreadCount) { + if (stickerType.getConstructor() != TdApi.StickerTypeCustomEmoji.CONSTRUCTOR) + return; + runOnUiThreadOptional(() -> { + if (getArguments() != null) { + getArguments().setHasNewHots(EmojiLayout.EMOJI_TRENDING_CONTROLLER_ID, TD.getStickerSetsUnreadCount(stickerSets.sets) > 0); } - } + trendingSetsController.scheduleFeaturedSets(stickerSets, contentView.getCurrentSection() == SECTION_TRENDING); + }); + } - @Override - public int getItemViewType (int position) { - return items.get(position).viewType; + @Override + public void onInstalledStickerSetsUpdated (final long[] stickerSetIds, TdApi.StickerType stickerType) { + if (stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR) { + runOnUiThreadOptional(() -> { + if (!loadingStickers) { + changeStickers(stickerSetIds); + } + }); } + } + + /* Data provider */ + + private StickerSetsDataProvider stickerSetsDataProvider; - @Override - public int getItemCount () { - return items.size(); + private StickerSetsDataProvider stickerSetsDataProvider() { + if (stickerSetsDataProvider != null) { + return stickerSetsDataProvider; } + + return stickerSetsDataProvider = new StickerSetsDataProvider(tdlib) { + @Override + protected boolean needIgnoreRequests (long stickerSetId, TGStickerObj stickerObj) { + return emojiController.isIgnoreRequests(stickerSetId); + } + + @Override + protected int getLoadingFlags (long stickerSetId, TGStickerObj stickerObj) { + return stickerObj.isTrending() ? FLAG_TRENDING: FLAG_REGULAR; + } + + @Override + protected void applyStickerSet (TdApi.StickerSet stickerSet, int flags) { + if ((flags & FLAG_REGULAR) != 0) { + emojiController.applyStickerSet(stickerSet, this); + } + if ((flags & FLAG_TRENDING) != 0) { + trendingSetsController.applyStickerSet(stickerSet, this); + } + } + }; } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EmojiMediaListController.java b/app/src/main/java/org/thunderdog/challegram/ui/EmojiMediaListController.java index 293c39554a..50ee8d423a 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/EmojiMediaListController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/EmojiMediaListController.java @@ -16,7 +16,6 @@ import android.content.Context; import android.graphics.Rect; -import android.os.Build; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -38,7 +37,6 @@ import org.thunderdog.challegram.component.emoji.GifView; import org.thunderdog.challegram.component.emoji.MediaGifsAdapter; import org.thunderdog.challegram.component.emoji.MediaStickersAdapter; -import org.thunderdog.challegram.component.sticker.StickerSmallView; import org.thunderdog.challegram.component.sticker.TGStickerObj; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; @@ -54,38 +52,45 @@ import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; -import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.unsorted.Settings; +import org.thunderdog.challegram.util.StickerSetsDataProvider; import org.thunderdog.challegram.util.StringList; -import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.v.NewFlowLayoutManager; -import org.thunderdog.challegram.v.RtlGridLayoutManager; import org.thunderdog.challegram.widget.EmojiLayout; import org.thunderdog.challegram.widget.ForceTouchView; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutRecyclerController; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutSectionPager; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutTrendingController; import java.util.ArrayList; import me.vkryl.android.AnimatorUtils; -import me.vkryl.android.animator.FactorAnimator; import me.vkryl.android.util.ClickHelper; import me.vkryl.android.widget.FrameLayoutFix; import me.vkryl.core.collection.IntList; import me.vkryl.core.collection.LongList; -import me.vkryl.core.collection.LongSparseIntArray; -import me.vkryl.core.lambda.CancellableRunnable; public class EmojiMediaListController extends ViewController implements - StickerSmallView.StickerMovementCallback, StickersListener, AnimationsListener, - TGStickerObj.DataProvider, - FactorAnimator.Target, MediaGifsAdapter.Callback, - TGStickerSetInfo.ViewCallback, ClickHelper.Delegate, ForceTouchView.ActionListener { + + private static final int SECTION_STICKERS = 0; + private static final int SECTION_GIFS = 1; + private static final int SECTION_TRENDING = 2; + + private EmojiLayoutSectionPager contentView; + private final EmojiLayoutRecyclerController stickersController; + private final EmojiLayoutTrendingController trendingSetsController; + public EmojiMediaListController (Context context, Tdlib tdlib) { super(context, tdlib); + stickersController = new EmojiLayoutRecyclerController(context, tdlib, EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID); + trendingSetsController = new EmojiLayoutTrendingController(context, tdlib, EmojiLayout.STICKERS_TRENDING_CONTROLLER_ID); + trendingSetsController.setCallbacks(stickerSetsDataProvider(), new TdApi.StickerTypeRegular()); + trendingSetsController.stickerSets = new ArrayList<>(); } @Override @@ -93,32 +98,14 @@ public int getId () { return R.id.controller_emojiMedia; } - private static final int SECTION_STICKERS = 0; - private static final int SECTION_GIFS = 1; - private static final int SECTION_TRENDING = 2; - - private int currentSection; - - private FrameLayoutFix contentView; private MediaStickersAdapter stickersAdapter; private MediaStickersAdapter trendingAdapter; private MediaGifsAdapter gifsAdapter; - private CustomRecyclerView stickersView; private RecyclerView gifsView; - private RecyclerView hotView; @Override protected View onCreateView (Context context) { - contentView = new FrameLayoutFix(context) { - @Override - protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - updatePositions(); - } - }; - contentView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - - stickersAdapter = new MediaStickersAdapter(this, this, false, this) { + stickersAdapter = new MediaStickersAdapter(this, stickersController, false, this) { @Override protected void onToggleCollapseRecentStickers (TextView collapseView, TGStickerSetInfo recentSet) { boolean needExpand = recentSet.isCollapsed(); @@ -145,52 +132,115 @@ protected void onToggleCollapseRecentStickers (TextView collapseView, TGStickerS Settings.instance().setNewSetting(Settings.SETTING_FLAG_EXPAND_RECENT_STICKERS, needExpand); } }; - trendingAdapter = new MediaStickersAdapter(this, this, true, this); + stickersController.setArguments(getArguments()); + stickersController.setAdapter(stickersAdapter); + stickersController.setSpanCount(spanCount); + + trendingAdapter = new MediaStickersAdapter(this, trendingSetsController, true, this); trendingAdapter.setItem(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_PROGRESS)); + trendingSetsController.setArguments(getArguments()); + trendingSetsController.setAdapter(trendingAdapter); + trendingSetsController.setSpanCount(5); + gifsAdapter = new MediaGifsAdapter(context, this); gifsAdapter.setCallback(this); + contentView = new EmojiLayoutSectionPager(context) { + + @Override + protected View getSectionView (int section) { + if (section == SECTION_STICKERS) { + initStickers(); + return stickersController.recyclerView; + } else if (section == SECTION_TRENDING) { + initHots(); + return trendingSetsController.recyclerView; + } else if (section == SECTION_GIFS) { + initGIFs(); + return gifsView; + } + return null; + } + + @Override + protected void onSectionChangeStart (int prevSection, int nextSection, int stickerSetSection) { + if (getArguments() != null) { + if (getArguments().getCurrentItem() == 1) { + if (prevSection == SECTION_GIFS && (nextSection == SECTION_STICKERS || nextSection == SECTION_TRENDING)) { + getArguments().setCircleVisible(false, false); + } else if ((prevSection == SECTION_STICKERS || prevSection == SECTION_TRENDING) && nextSection == SECTION_GIFS) { + getArguments().setCircleVisible(true, true); + } + } + getArguments().setCurrentStickerSectionByPosition(EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID, + nextSection == SECTION_STICKERS ? (stickerSetSection != -1 ? stickerSetSection : stickersController.getStickerSetSection()) : + nextSection == SECTION_TRENDING ? 2 : 1 + , nextSection == SECTION_STICKERS,true); + } + } + + @Override + protected void onSectionChangeEnd (int prevSection, int currentSection) { + if (getArguments() != null) { + if (prevSection == SECTION_GIFS && (currentSection == SECTION_STICKERS || currentSection == SECTION_TRENDING)) { + getArguments().setPreferredSection(EmojiMediaType.STICKER); + } else if (currentSection == SECTION_GIFS && (prevSection == SECTION_STICKERS || prevSection == SECTION_TRENDING)) { + getArguments().setPreferredSection(EmojiMediaType.GIF); + } + } + + if (prevSection == SECTION_TRENDING && currentSection != SECTION_TRENDING) { + trendingSetsController.applyScheduledFeaturedSets(); + } + + if (getArguments() != null) { + EmojiLayout.Listener listener = getArguments().getListener(); + if (listener != null) { + int prevSectionX = prevSection == SECTION_GIFS ? EmojiMediaType.GIF : EmojiMediaType.STICKER; + int newSection = currentSection == SECTION_GIFS ? EmojiMediaType.GIF : EmojiMediaType.STICKER; + listener.onSectionSwitched(getArguments(), newSection, prevSectionX); + } + getArguments().resetScrollState(false); + } + } + }; + contentView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + checkSpanCount(); int currentSection = Settings.instance().getEmojiMediaSection(); switch (currentSection) { case EmojiMediaType.STICKER: { - initStickers(); - this.currentSection = SECTION_STICKERS; - this.currentSectionView = stickersView; - this.contentView.addView(stickersView); + this.contentView.init(SECTION_STICKERS); break; } case EmojiMediaType.GIF: { - initGIFs(); if (getArguments() != null) { - getArguments().setCurrentStickerSectionByPosition(1, false, false); + getArguments().setCurrentStickerSectionByPosition(EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID, 1, false, false); getArguments().setMediaSection(true); } - this.currentSection = SECTION_GIFS; - this.currentSectionView = gifsView; - this.contentView.addView(gifsView); + this.contentView.init(SECTION_GIFS); break; } } loadGIFs(); // to show or hide GIF section loadStickers(); // to show sections - loadTrending(0, 20, 0); // to show blue badge? + trendingSetsController.loadTrending(0, 20, 0); // to show blue badge? return contentView; } public int getCurrentScrollY () { - switch (currentSection) { + switch (contentView.getCurrentSection()) { case SECTION_GIFS: { return getGIFsScrollY(); } case SECTION_STICKERS: { - return getStickersScrollY(); + return stickersController.getStickersScrollY(showRecentTitle); } case SECTION_TRENDING: { - return getTrendingScrollY(); + return trendingSetsController.getStickersScrollY(showRecentTitle); } } return -1; @@ -202,88 +252,42 @@ public void removeRecentStickers () { setRecentStickers(null, null); } - private int getStickerSetSection () { - if (stickersView == null || spanCount == 0) { - return -1; - } - int i = ((LinearLayoutManager) stickersView.getLayoutManager()).findFirstCompletelyVisibleItemPosition(); - if (i == -1) { - i = ((LinearLayoutManager) stickersView.getLayoutManager()).findFirstVisibleItemPosition(); - } - if (i != -1) { - return indexOfStickerSetByAdapterPosition(i); - } - return 0; - } - - private int getStickersScrollY () { - if (stickersView == null || spanCount == 0) { - return 0; - } - int i = ((LinearLayoutManager) stickersView.getLayoutManager()).findFirstVisibleItemPosition(); - if (i != -1) { - View v = stickersView.getLayoutManager().findViewByPosition(i); - int additional = v != null ? -v.getTop() : 0; - int stickerSet = indexOfStickerSetByAdapterPosition(i); - return additional + stickersAdapter.measureScrollTop(i, spanCount, stickerSet, stickerSets, showRecentTitle); - } - return 0; - } - @Override protected void handleLanguageDirectionChange () { super.handleLanguageDirectionChange(); - if (stickersView != null) - stickersView.requestLayout(); + if (stickersController.recyclerView != null) + stickersController.recyclerView.requestLayout(); if (trendingAdapter != null) trendingAdapter.notifyDataSetChanged(); } private void initStickers () { - if (stickersView == null) { - GridLayoutManager manager = new RtlGridLayoutManager(context(), spanCount).setAlignOnly(true); - manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { - @Override - public int getSpanSize (int position) { - return stickersAdapter.getItemViewType(position) == MediaStickersAdapter.StickerHolder.TYPE_STICKER ? 1 : spanCount; - } - }); - - stickersView = (CustomRecyclerView) Views.inflate(context(), R.layout.recycler_custom, contentView); - stickersView.setHasFixedSize(true); - stickersView.setLayoutManager(manager); - stickersView.setAdapter(stickersAdapter); - // stickersView.setItemAnimator(null); - stickersView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180)); - stickersView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? View.OVER_SCROLL_IF_CONTENT_SCROLLS :View.OVER_SCROLL_NEVER); - stickersView.addOnScrollListener(new RecyclerView.OnScrollListener() { + if (stickersController.recyclerView == null) { + stickersController.getValue(); + stickersController.recyclerView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180)); + stickersController.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onScrollStateChanged (RecyclerView recyclerView, int newState) { - if ((sectionAnimator == null || sectionAnimator.getFactor() == 0f) && currentSection == SECTION_STICKERS && getArguments() != null && getArguments().getCurrentItem() == 1) { + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { + if (contentView.isSectionStable() && contentView.getCurrentSection() == SECTION_STICKERS && getArguments() != null && getArguments().getCurrentItem() == 1) { boolean isScrolling = newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING; getArguments().setIsScrolling(isScrolling); } } @Override - public void onScrolled (RecyclerView recyclerView, int dx, int dy) { - onStickersScroll(false, dy); + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (!stickersController.isNeedIgnoreScroll()) { + if (contentView.isSectionStable() && contentView.getCurrentSection() == SECTION_STICKERS && getArguments() != null && getArguments().isWatchingMovements() && getArguments().getCurrentItem() == 1) { + getArguments().moveHeader(stickersController.getStickersScrollY(showRecentTitle)); + getArguments().setCurrentStickerSectionByPosition(EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID, stickersController.getStickerSetSection(), true, true); + getArguments().onSectionInteractedScroll(EmojiMediaType.STICKER, dy != 0); + } + } } }); } } - private void onStickersScroll (boolean force, int movedDy) { - if (ignoreStickersScroll == 0) { - if ((sectionAnimator == null || sectionAnimator.getFactor() == 0f) && currentSection == SECTION_STICKERS && getArguments() != null && getArguments().isWatchingMovements() && getArguments().getCurrentItem() == 1) { - int y = getStickersScrollY(); - getArguments().onScroll(y); - getArguments().setCurrentStickerSectionByPosition(getStickerSetSection(), true, true); - getArguments().onSectionScroll(EmojiMediaType.STICKER, movedDy != 0); - } - } - } - // GIFs private int getGIFsScrollY () { @@ -303,7 +307,7 @@ private int getGIFsScrollY () { private void initGIFs () { if (gifsView == null) { final NewFlowLayoutManager manager = new NewFlowLayoutManager(context(), 100) { - private Size size = new Size(); + private final Size size = new Size(); @Override protected Size getSizeForItem (int i) { @@ -334,24 +338,24 @@ public int getSpanSize(int position) { gifsView.setAdapter(gifsAdapter); gifsView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onScrollStateChanged (RecyclerView recyclerView, int newState) { - if ((sectionAnimator == null || sectionAnimator.getFactor() == 0f) && currentSection == SECTION_GIFS && getArguments() != null && getArguments().getCurrentItem() == 1) { + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { + if (contentView.isSectionStable() && contentView.getCurrentSection() == SECTION_GIFS && getArguments() != null && getArguments().getCurrentItem() == 1) { boolean isScrolling = newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING; getArguments().setIsScrolling(isScrolling); } } @Override - public void onScrolled (RecyclerView recyclerView, int dx, int dy) { - if ((sectionAnimator == null || sectionAnimator.getFactor() == 0f) && currentSection == SECTION_GIFS && getArguments() != null && getArguments().getCurrentItem() == 1) { + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (contentView.isSectionStable() && contentView.getCurrentSection() == SECTION_GIFS && getArguments() != null && getArguments().getCurrentItem() == 1) { getArguments().moveHeaderFull(getGIFsScrollY()); - getArguments().onSectionScroll(EmojiMediaType.GIF, dy != 0); + getArguments().onSectionInteractedScroll(EmojiMediaType.GIF, dy != 0); } } }); gifsView.addItemDecoration(new RecyclerView.ItemDecoration() { @Override - public void getItemOffsets (Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + public void getItemOffsets (@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { int position = parent.getChildAdapterPosition(view); outRect.top = manager.isFirstRow(position) ? Screen.dp(4f) + EmojiLayout.getHeaderSize() : 0; outRect.right = manager.isLastInRow(position) ? 0 : Screen.dp(3f); @@ -363,7 +367,7 @@ public void getItemOffsets (Rect outRect, View view, RecyclerView parent, Recycl } public float getDesiredHeaderHideFactor () { - return currentSection == SECTION_GIFS ? Math.min(1f, Math.max(0f, (float) getGIFsScrollY() / (float) EmojiLayout.getHeaderSize())) : 0f; + return contentView.getCurrentSection() == SECTION_GIFS ? Math.min(1f, Math.max(0f, (float) getGIFsScrollY() / (float) EmojiLayout.getHeaderSize())) : 0f; } @Override @@ -495,54 +499,7 @@ public void onAfterForceTouchAction (ForceTouchView.ForceTouchContext context, i // Trending stickers - private final ArrayList trendingSets = new ArrayList<>(); - private boolean trendingLoading, canLoadMoreTrending; - - private void updateTrendingSets (long[] stickerSetIds) { - LongSparseArray installedStickerSets = new LongSparseArray<>(stickerSetIds.length); - for (long stickerSetId : stickerSetIds) { - installedStickerSets.put(stickerSetId, null); - } - for (TGStickerSetInfo stickerSet : trendingSets) { - int i = installedStickerSets.indexOfKey(stickerSet.getId()); - if (i >= 0) { - stickerSet.setIsInstalled(); - trendingAdapter.updateDone(stickerSet); - } else { - stickerSet.setIsNotInstalled(); - trendingAdapter.updateDone(stickerSet); - } - } - } - - private void addTrendingStickers (ArrayList trendingSets, ArrayList items, boolean hasUnread, int offset) { - if (offset != 0 && (!trendingLoading || offset != this.trendingSets.size())) - return; - - if (trendingSets != null) { - if (offset == 0) { - this.lastTrendingStickerSet = null; - this.trendingSets.clear(); - } - this.trendingSets.addAll(trendingSets); - } - this.canLoadMoreTrending = trendingSets != null && !trendingSets.isEmpty(); - if (getArguments() != null && (hasUnread || offset == 0)) { - getArguments().setHasNewHots(hasUnread); - } - if (offset == 0) { - if (hotView != null) { - hotView.stopScroll(); - ((LinearLayoutManager) hotView.getLayoutManager()).scrollToPositionWithOffset(0, 0); - } - trendingAdapter.setItems(items); - } else { - trendingAdapter.addItems(items); - } - this.trendingLoading = false; - } - - public static int parseTrending (Tdlib tdlib, ArrayList parsedStickerSets, ArrayList items, int offset, TdApi.StickerSetInfo[] stickerSets, TGStickerObj.DataProvider dataProvider, @Nullable TGStickerSetInfo.ViewCallback viewCallback, boolean needSeparators, boolean isEmojiStatuses) { + public static int parseTrending (Tdlib tdlib, ArrayList parsedStickerSets, ArrayList items, int offset, TdApi.StickerSetInfo[] stickerSets, TGStickerObj.DataProvider dataProvider, @Nullable TGStickerSetInfo.ViewCallback viewCallback, boolean needSeparators, boolean isEmojiStatuses, String highlight) { int unreadItemCount = 0; parsedStickerSets.ensureCapacity(stickerSets.length); items.ensureCapacity(items.size() + stickerSets.length * 2 + 1); @@ -570,11 +527,11 @@ public static int parseTrending (Tdlib tdlib, ArrayList parsed parsedStickerSets.add(stickerSet); stickerSet.setStartIndex(startIndex); if (isEmojiStatuses) { - items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER, stickerSet)); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER, stickerSet).setHighlightValue(highlight)); } else { - items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER_TRENDING, stickerSet)); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER_TRENDING, stickerSet).setHighlightValue(highlight)); } - int itemCount = isEmojiStatuses ? stickerSetInfo.size :5; + int itemCount = isEmojiStatuses ? stickerSetInfo.size : (stickerSetInfo.stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR ? 16 : 5); for (int i = 0; i < itemCount; i++) { TGStickerObj stickerObj = new TGStickerObj(tdlib, i < stickerSetInfo.covers.length ? stickerSetInfo.covers[i] : null, null, stickerSetInfo.stickerType); stickerObj.setStickerSetId(stickerSetInfo.id, null); @@ -587,84 +544,22 @@ public static int parseTrending (Tdlib tdlib, ArrayList parsed return unreadItemCount; } - private void loadTrending (int offset, int limit, int cellCount) { - if (!trendingLoading) { - trendingLoading = true; - tdlib.client().send(new TdApi.GetTrendingStickerSets(new TdApi.StickerTypeRegular(), offset, limit), object -> { - final ArrayList parsedStickerSets = new ArrayList<>(); - final ArrayList items = new ArrayList<>(); - final int unreadItemCount; - - if (object.getConstructor() == TdApi.TrendingStickerSets.CONSTRUCTOR) { - TdApi.TrendingStickerSets trendingStickerSets = (TdApi.TrendingStickerSets) object; - if (offset == 0) - items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_KEYBOARD_TOP)); - unreadItemCount = parseTrending(tdlib, parsedStickerSets, items, cellCount, trendingStickerSets.sets, EmojiMediaListController.this, EmojiMediaListController.this, false, false); - } else { - if (offset == 0) - items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_COME_AGAIN_LATER)); - unreadItemCount = 0; - } - - runOnUiThreadOptional(() -> { - addTrendingStickers(parsedStickerSets, items, unreadItemCount > 0, offset); - }); - }); - } - } - - public int getTrendingScrollY () { - if (hotView == null) { - return 0; - } - int i = ((LinearLayoutManager) hotView.getLayoutManager()).findFirstVisibleItemPosition(); - if (i != -1) { - View v = hotView.getLayoutManager().findViewByPosition(i); - int additional = v != null ? -v.getTop() : 0; - return additional + trendingAdapter.measureScrollTop(i, 5, indexOfTrendingStickerSetByAdapterPosition(i), trendingSets, showRecentTitle); - } - return 0; - } - private void initHots () { - if (hotView == null) { - GridLayoutManager manager = new GridLayoutManager(context(), 5); - manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { - @Override - public int getSpanSize (int position) { - return trendingAdapter.getItemViewType(position) == MediaStickersAdapter.StickerHolder.TYPE_STICKER ? 1 : 5; - } - }); - trendingAdapter.setManager(manager); - - hotView = (RecyclerView) Views.inflate(context(), R.layout.recycler, contentView); - hotView.setHasFixedSize(true); - hotView.setAdapter(trendingAdapter); - hotView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? View.OVER_SCROLL_IF_CONTENT_SCROLLS :View.OVER_SCROLL_NEVER); - hotView.setLayoutManager(manager); - hotView.addOnScrollListener(new RecyclerView.OnScrollListener() { + if (trendingSetsController.recyclerView == null) { + trendingSetsController.getValue(); + trendingSetsController.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onScrollStateChanged (RecyclerView recyclerView, int newState) { - if ((sectionAnimator == null || sectionAnimator.getFactor() == 0f) && currentSection == SECTION_TRENDING && getArguments() != null && getArguments().getCurrentItem() == 1) { + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { + if (contentView.isSectionStable() && contentView.getCurrentSection() == SECTION_TRENDING && getArguments() != null && getArguments().getCurrentItem() == 1) { boolean isScrolling = newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING; getArguments().setIsScrolling(isScrolling); } } @Override - public void onScrolled (RecyclerView recyclerView, int dx, int dy) { - if ((sectionAnimator == null || sectionAnimator.getFactor() == 0f) && currentSection == SECTION_TRENDING && getArguments() != null && getArguments().getCurrentItem() == 1) { - getArguments().onScroll(getTrendingScrollY()); - getArguments().onSectionScroll(EmojiMediaType.STICKER, dy != 0); - if (!trendingLoading && canLoadMoreTrending) { - int lastVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition(); - if (lastVisiblePosition != -1) { - int index = trendingSets.indexOf(trendingAdapter.getItem(lastVisiblePosition).stickerSet); - if (index != -1 && index + 5 >= trendingSets.size()) { - loadTrending(trendingSets.size(), 25, trendingAdapter.getItemCount()); - } - } - } + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (contentView.isSectionStable() && contentView.getCurrentSection() == SECTION_TRENDING && getArguments() != null && getArguments().getCurrentItem() == 1) { + trendingSetsController.onScrolledImpl(dy, showRecentTitle); } } }); @@ -676,7 +571,9 @@ public void onScrolled (RecyclerView recyclerView, int dx, int dy) { @Override public void destroy () { super.destroy(); - tdlib.listeners().unsubscribeFromAnyUpdates(this); + tdlib.listeners().unsubscribeFromGlobalUpdates(this); + stickersController.destroy(); + trendingSetsController.destroy(); } // When rotation, etc @@ -687,9 +584,7 @@ public void checkSpanCount () { int spanCount = calculateSpanCount(Screen.currentWidth(), Screen.currentHeight()); if (this.spanCount != spanCount) { this.spanCount = spanCount; - if (stickersView != null) { - ((GridLayoutManager) stickersView.getLayoutManager()).setSpanCount(spanCount); - } + stickersController.setSpanCount(spanCount); if (gifsView != null) { gifsView.invalidateItemDecorations(); } @@ -707,217 +602,9 @@ public static int getEstimateColumnResolution () { return Screen.currentWidth() / spanCount; } - // Sticker movements - - private @Nullable TGStickerSetInfo lastStickerSetInfo; - private int lastStickerSetIndex; - - private void setLastStickerSetInfo (TGStickerSetInfo info, int index) { - lastStickerSetInfo = info; - lastStickerSetIndex = index; - } - - private int indexOfStickerSetByAdapterPosition (int position) { - if (position == 0) { - return 0; - } - if (stickerSets != null) { - if (lastStickerSetInfo != null) { - if (position >= lastStickerSetInfo.getStartIndex() && position < lastStickerSetInfo.getEndIndex()) { - return lastStickerSetIndex; - } else if (position >= lastStickerSetInfo.getEndIndex()) { - for (int i = lastStickerSetIndex + 1; i < stickerSets.size(); i++) { - TGStickerSetInfo oldStickerSet = stickerSets.get(i); - if (position >= oldStickerSet.getStartIndex() && position < oldStickerSet.getEndIndex()) { - setLastStickerSetInfo(oldStickerSet, i); - return lastStickerSetIndex; - } - } - } else if (position < lastStickerSetInfo.getStartIndex()) { - for (int i = lastStickerSetIndex - 1; i >= 0; i--) { - TGStickerSetInfo oldStickerSet = stickerSets.get(i); - if (position >= oldStickerSet.getStartIndex() && position < oldStickerSet.getEndIndex()) { - setLastStickerSetInfo(oldStickerSet, i); - return lastStickerSetIndex; - } - } - } - } - int i = 0; - for (TGStickerSetInfo oldStickerSet : stickerSets) { - if (position >= oldStickerSet.getStartIndex() && position < oldStickerSet.getEndIndex()) { - setLastStickerSetInfo(oldStickerSet, i); - return lastStickerSetIndex; - } - i++; - } - } - return -1; - } - - private @Nullable TGStickerSetInfo lastTrendingStickerSet; - private int lastTrendingStickerSetIndex; - - private int indexOfTrendingStickerSetByAdapterPosition (int position) { - if (position == 0) { - return 0; - } - if (lastTrendingStickerSet != null) { - if (position >= lastTrendingStickerSet.getStartIndex() && position < lastTrendingStickerSet.getEndIndex()) { - return lastTrendingStickerSetIndex; - } else if (position >= lastTrendingStickerSet.getEndIndex()) { - for (int i = lastTrendingStickerSetIndex + 1; i < trendingSets.size(); i++) { - TGStickerSetInfo oldStickerSet = trendingSets.get(i); - if (position >= oldStickerSet.getStartIndex() && position < oldStickerSet.getEndIndex()) { - lastTrendingStickerSet = oldStickerSet; - lastTrendingStickerSetIndex = i; - return i; - } - } - } else if (position < lastTrendingStickerSet.getStartIndex()) { - for (int i = Math.min(trendingSets.size() - 1, lastTrendingStickerSetIndex - 1); i >= 0; i--) { - TGStickerSetInfo oldStickerSet = trendingSets.get(i); - if (position >= oldStickerSet.getStartIndex() && position < oldStickerSet.getEndIndex()) { - lastTrendingStickerSet = oldStickerSet; - lastTrendingStickerSetIndex = i; - return i; - } - } - } - } - int i = 0; - for (TGStickerSetInfo oldStickerSet : trendingSets) { - if (position >= oldStickerSet.getStartIndex() && position < oldStickerSet.getEndIndex()) { - lastTrendingStickerSet = oldStickerSet; - lastTrendingStickerSetIndex = i; - return i; - } - i++; - } - return -1; - } - - private int indexOfStickerSet (TGStickerSetInfo stickerSet) { - if (stickerSets != null) { - for (TGStickerSetInfo oldStickerSet : stickerSets) { - if (stickerSet.getId() == oldStickerSet.getId()) { - return stickerSet.getStartIndex(); - } - } - } - return -1; - } - - private int indexOfSticker (TGStickerObj sticker) { - if (stickerSets != null) { - for (TGStickerSetInfo stickerSet : stickerSets) { - boolean isFavorite = stickerSet.isFavorite(); - boolean isRecent = stickerSet.isRecent(); - boolean stickerFavorite = sticker.isFavorite(); - boolean stickerRecent = sticker.isRecent(); - if ((isFavorite && stickerFavorite) || (isRecent && stickerRecent) || (isFavorite == stickerFavorite && isRecent == stickerRecent && stickerSet.getId() == sticker.getStickerSetId())) { - return stickersAdapter.indexOfSticker(sticker, stickerSet.getStartIndex()); - } - } - } - return -1; - } - - private int indexOfTrendingSticker (TGStickerObj sticker) { - if (trendingSets != null) { - for (TGStickerSetInfo stickerSet : trendingSets) { - if (stickerSet.getId() == sticker.getStickerSetId()) { - return trendingAdapter.indexOfSticker(sticker, stickerSet.getStartIndex()); - } - } - } - return -1; - } - - @Override - public void setStickerPressed (StickerSmallView view, TGStickerObj sticker, boolean isPressed) { - if (sticker.isTrending()) { - int i = indexOfTrendingSticker(sticker); - if (i != -1) { - trendingAdapter.setStickerPressed(i, isPressed, hotView != null ? hotView.getLayoutManager() : null); - } - } else { - int i = indexOfSticker(sticker); - if (i != -1) { - stickersAdapter.setStickerPressed(i, isPressed, stickersView != null ? stickersView.getLayoutManager() : null); - } - } - } - - @Override - public boolean canFindChildViewUnder (StickerSmallView view, int recyclerX, int recyclerY) { - EmojiLayout parent = getArgumentsStrict(); - return recyclerY > parent.getHeaderBottom(); - } - - @Override - public void onStickerPreviewOpened (StickerSmallView view, TGStickerObj sticker) { - if (getArguments() != null) { - getArguments().onSectionInteracted(EmojiMediaType.STICKER, false); - } - } - - @Override - public void onStickerPreviewChanged (StickerSmallView view, TGStickerObj otherOrThisSticker) { - if (getArguments() != null) { - getArguments().onSectionInteracted(EmojiMediaType.STICKER, false); - } - } - - @Override - public void onStickerPreviewClosed (StickerSmallView view, TGStickerObj thisSticker) { - if (getArguments() != null) { - getArguments().onSectionInteracted(EmojiMediaType.STICKER, true); - } - } - - @Override - public boolean needsLongDelay (StickerSmallView view) { - return false; - } - - @Override - public int getViewportHeight () { - return -1; - } - - @Override - public boolean onStickerClick (StickerSmallView view, View clickView, TGStickerObj sticker, boolean isMenuClick, TdApi.MessageSendOptions sendOptions) { - if (sticker.isTrending() && !isMenuClick) { - int i = indexOfTrendingStickerSetById(sticker.getStickerSetId()); - if (i != -1) { - trendingSets.get(i).show(this); - return true; - } - return false; - } - - if (getArguments() != null) { - if (getArguments().sendSticker(clickView, sticker, sendOptions)) { - return true; - } - } - return false; - } - - @Override - public long getStickerOutputChatId () { - return getArguments() != null ? getArguments().findOutputChatId() : 0; - } - - @Override - public int getStickersListTop () { - return Views.getLocationInWindow(currentSection == SECTION_TRENDING ? hotView : stickersView)[1]; - } - @EmojiMediaType public int getMediaSection () { - int currentSection = this.nextSection != -1 ? this.nextSection : this.currentSection; + int currentSection = this.contentView.getNextSection() != -1 ? this.contentView.getNextSection() : this.contentView.getCurrentSection(); if (currentSection == SECTION_GIFS) { return EmojiMediaType.GIF; } else { @@ -964,8 +651,8 @@ private void reloadStickers () { private boolean loadingRecentStickers, loadingFavoriteStickers; private void shiftStickerSets (int startPosition, int startIndex) { - for (int i = startPosition; i < stickerSets.size(); i++) { - TGStickerSetInfo stickerSet = stickerSets.get(i); + for (int i = startPosition; i < stickersController.stickerSets.size(); i++) { + TGStickerSetInfo stickerSet = stickersController.stickerSets.get(i); stickerSet.setStartIndex(startIndex); startIndex = stickerSet.getEndIndex(); } @@ -974,9 +661,9 @@ private void shiftStickerSets (int startPosition, int startIndex) { private static final int MAX_HEADER_COUNT = 2; private int findRecentStickerSet () { - if (stickerSets != null && !stickerSets.isEmpty()) { + if (stickersController.stickerSets != null && !stickersController.stickerSets.isEmpty()) { int i = 0; - for (TGStickerSetInfo set : stickerSets) { + for (TGStickerSetInfo set : stickersController.stickerSets) { if (set.isRecent()) { return i; } @@ -990,9 +677,9 @@ private int findRecentStickerSet () { } private int findFavoriteStickerSet () { - if (stickerSets != null && !stickerSets.isEmpty()) { + if (stickersController.stickerSets != null && !stickersController.stickerSets.isEmpty()) { int i = 0; - for (TGStickerSetInfo set : stickerSets) { + for (TGStickerSetInfo set : stickersController.stickerSets) { if (set.isFavorite()) { return i; } @@ -1008,7 +695,7 @@ private int findFavoriteStickerSet () { private int getRecentStartIndex () { int i = findFavoriteStickerSet(); if (i != -1) { - return stickerSets.get(i).getEndIndex(); + return stickersController.stickerSets.get(i).getEndIndex(); } return 1; } @@ -1021,10 +708,10 @@ private boolean smartUpdateStickerPack ( int stickerSetIndex, TGStickerSetInfo stickerSet, TdApi.Sticker[] newStickers, @NonNull ArrayList newItems, int visibleItemCount ) { - if (stickersView == null || stickersAdapter == null) { + if (stickersController == null || stickersAdapter == null) { return false; } - LinearLayoutManager manager = (LinearLayoutManager) stickersView.getLayoutManager(); + LinearLayoutManager manager = (LinearLayoutManager) stickersController.getManager(); if (manager == null) { return false; } @@ -1132,14 +819,14 @@ private void setRecentStickers (@Nullable TdApi.Sticker[] recentStickers, @Nulla if (existingIndex != -1) { if (haveRecentStickers) { - recentSet = stickerSets.get(existingIndex); + recentSet = stickersController.stickerSets.get(existingIndex); TdApi.Sticker[] prevRecentStickers = recentSet.getAllStickers(); if (prevRecentStickers != null && prevRecentStickers.length > 0 && items != null && !items.isEmpty()) { if (smartUpdateStickerPack(existingIndex, recentSet, recentStickers, items, visibleRecentCount)) { setShowRecentTitle(showRecentTitle, allowCollapseRecent); - if (showRecentTitle && allowCollapseRecent && stickersView != null && recentSet.isCollapsed()) { + if (showRecentTitle && allowCollapseRecent && stickersController != null && recentSet.isCollapsed()) { // direct update of show X more - View view = stickersView.getLayoutManager().findViewByPosition(recentSet.getStartIndex()); + View view = stickersController.getManager().findViewByPosition(recentSet.getStartIndex()); if (view instanceof ViewGroup) { stickersAdapter.updateCollapseView((ViewGroup) view, recentSet); } @@ -1150,7 +837,7 @@ private void setRecentStickers (@Nullable TdApi.Sticker[] recentStickers, @Nulla } // Too many changes for simple animation update - recentSet = stickerSets.remove(existingIndex); + recentSet = stickersController.stickerSets.remove(existingIndex); shiftStickerSets(existingIndex, recentSet.getStartIndex()); stickersAdapter.removeRange(recentSet.getStartIndex(), recentSet.getItemCount()); } else if (visibleRecentCount > 0) { @@ -1166,7 +853,7 @@ private void setRecentStickers (@Nullable TdApi.Sticker[] recentStickers, @Nulla recentSet.setStickers(recentStickers, visibleRecentCount); recentSet.setStartIndex(startIndex); int stickerSetIndex = haveFavorites ? 1 : 0; - stickerSets.add(stickerSetIndex, recentSet); + stickersController.stickerSets.add(stickerSetIndex, recentSet); shiftStickerSets(stickerSetIndex + 1, recentSet.getEndIndex()); stickersAdapter.insertRange(startIndex, items); } @@ -1184,7 +871,7 @@ private void setShowRecentTitle (boolean show, boolean allowToggleCollapse) { this.allowCollapseRecent = allowToggleCollapse; int i = findRecentStickerSet(); if (i != -1) { - int index = stickerSets.get(i).getStartIndex(); + int index = stickersController.stickerSets.get(i).getStartIndex(); if (stickersAdapter.getItem(index).setViewType(getRecentTitleViewType()) || prevAllowCollapseRecent != allowToggleCollapse) { stickersAdapter.notifyItemChanged(index); } @@ -1199,7 +886,7 @@ private void setFavoriteStickers (@Nullable TdApi.Sticker[] favoriteStickers, @N int recentStickerSetIndex = findRecentStickerSet(); if (recentStickerSetIndex != -1) { - TGStickerSetInfo recentSet = stickerSets.get(recentStickerSetIndex); + TGStickerSetInfo recentSet = stickersController.stickerSets.get(recentStickerSetIndex); setShowRecentTitle( Config.FORCE_SHOW_RECENTS_STICKERS_TITLE || recentSet.getFullSize() > Config.DEFAULT_SHOW_RECENT_STICKERS_COUNT || @@ -1212,14 +899,14 @@ private void setFavoriteStickers (@Nullable TdApi.Sticker[] favoriteStickers, @N int existingIndex = findFavoriteStickerSet(); if (existingIndex != -1) { - favoriteSet = stickerSets.get(existingIndex); + favoriteSet = stickersController.stickerSets.get(existingIndex); if (haveFavoriteStickers && items != null && !items.isEmpty()) { if (smartUpdateStickerPack(existingIndex, favoriteSet, null, items, items.size())) { return; } } - favoriteSet = stickerSets.remove(existingIndex); + favoriteSet = stickersController.stickerSets.remove(existingIndex); shiftStickerSets(existingIndex, favoriteSet.getStartIndex()); stickersAdapter.removeRange(favoriteSet.getStartIndex(), favoriteSet.getItemCount()); } else if (favoriteStickers != null && favoriteStickers.length > 0) { @@ -1233,7 +920,7 @@ private void setFavoriteStickers (@Nullable TdApi.Sticker[] favoriteStickers, @N favoriteSet.setSize(favoriteStickers.length); favoriteSet.setStartIndex(startIndex); int stickerSetIndex = 0; // findFavoriteStickerSet() != -1 ? 1 : 0; - stickerSets.add(stickerSetIndex, favoriteSet); + stickersController.stickerSets.add(stickerSetIndex, favoriteSet); shiftStickerSets(stickerSetIndex + 1, favoriteSet.getEndIndex()); stickersAdapter.insertRange(startIndex, items); } @@ -1305,7 +992,7 @@ private void applyFavoriteStickers (int[] stickerIds) { } private boolean hasNoStickerSets () { - for (TGStickerSetInfo info : stickerSets) { + for (TGStickerSetInfo info : stickersController.stickerSets) { if (!info.isFavorite() && !info.isRecent()) { return false; } @@ -1315,7 +1002,7 @@ private boolean hasNoStickerSets () { private int getSystemSetsCount () { int i = 0; - for (TGStickerSetInfo info : stickerSets) { + for (TGStickerSetInfo info : stickersController.stickerSets) { if (info.isSystem()) { i++; } @@ -1324,9 +1011,7 @@ private int getSystemSetsCount () { } private void changeStickers (long[] stickerSetIds) { - if (trendingSets != null) { - updateTrendingSets(stickerSetIds); - } + trendingSetsController.updateTrendingSets(stickerSetIds); if (applyingChanges) { if (pendingChanges == null) { pendingChanges = new ArrayList<>(); @@ -1341,10 +1026,10 @@ private void changeStickers (long[] stickerSetIds) { } // setId -> position in the current list - // LongSparseArray currentStickerSets = new LongSparseArray<>(this.stickerSets.size()); - LongSparseArray removedStickerSets = new LongSparseArray<>(this.stickerSets.size()); + // LongSparseArray currentStickerSets = new LongSparseArray<>(stickersController.stickerSets.size()); + LongSparseArray removedStickerSets = new LongSparseArray<>(stickersController.stickerSets.size()); // int currentSetIndex = 0; - for (TGStickerSetInfo stickerSet : this.stickerSets) { + for (TGStickerSetInfo stickerSet : stickersController.stickerSets) { if (!stickerSet.isSystem()) { removedStickerSets.put(stickerSet.getId(), stickerSet); // currentStickerSets.put(stickerSet.getId(), currentSetIndex); @@ -1393,21 +1078,21 @@ private void changeStickers (long[] stickerSetIds) { final int removedCount = removedStickerSets.size(); for (int i = 0; i < removedCount; i++) { TGStickerSetInfo stickerSet = removedStickerSets.valueAt(i); - removeStickerSet(stickerSet); + stickersController.removeStickerSet(stickerSet); } // Then, move items - if (positions != null && !stickerSets.isEmpty() ) { + if (positions != null && !stickersController.stickerSets.isEmpty() ) { for (int j = 0; j < positions.size(); j++) { long setId = positions.keyAt(j); int newPosition = positions.valueAt(j); - int currentPosition = indexOfStickerSetById(setId); + int currentPosition = stickersController.indexOfStickerSetById(setId); if (currentPosition == -1) { throw new RuntimeException(); } if (currentPosition != newPosition) { int systemSetsCount = getSystemSetsCount(); - moveStickerSet(currentPosition + systemSetsCount, newPosition + systemSetsCount); + stickersController.moveStickerSet(currentPosition + systemSetsCount, newPosition + systemSetsCount); } } } @@ -1445,9 +1130,7 @@ public void onResult (TdApi.Object object) { i++; } - runOnUiThreadOptional(() -> { - addStickerSet(stickerSet, items, insertIndex + getSystemSetsCount()); - }); + runOnUiThreadOptional(() -> stickersController.addStickerSet(stickerSet, items, insertIndex + getSystemSetsCount())); } if (++index[0] < addedCount) { @@ -1462,126 +1145,6 @@ public void onResult (TdApi.Object object) { } } - private int indexOfTrendingStickerSetById (long setId) { - int index = 0; - for (TGStickerSetInfo setInfo : trendingSets) { - if (setInfo.getId() == setId) { - return index; - } - index++; - } - return -1; - } - - private int indexOfStickerSetById (long setId) { - int index = 0; - for (TGStickerSetInfo setInfo : stickerSets) { - if (!setInfo.isSystem()) { - if (setInfo.getId() == setId) { - return index; - } - index++; - } - } - return -1; - } - - private int ignoreStickersScroll; - - private void moveStickerSet (int oldPosition, int newPosition) { - beforeStickerChanges(); - - if (getArguments() != null) { - getArguments().moveStickerSection(oldPosition, newPosition); - } - - TGStickerSetInfo stickerSet = stickerSets.remove(oldPosition); - - final int startIndex = stickerSet.getStartIndex(); - final int itemCount = stickerSet.getSize() + 1; - - int startPosition; - if (oldPosition < newPosition) { - startPosition = startIndex; - } else { - startPosition = stickerSets.get(newPosition).getStartIndex(); - } - - stickerSets.add(newPosition, stickerSet); - - for (int i = Math.min(oldPosition, newPosition); i < stickerSets.size(); i++) { - TGStickerSetInfo nextSet = stickerSets.get(i); - nextSet.setStartIndex(startPosition); - startPosition += nextSet.getSize() + 1; - } - - stickersAdapter.moveRange(startIndex, itemCount, stickerSet.getStartIndex()); - resetScrollCache(); - } - - private void beforeStickerChanges () { - ignoreStickersScroll++; - } - - private void resetScrollCache () { - // lastStickerSetInfo = null; // FIXME removing current sticker set does not update selection - // ignoreStickersScroll--; - - if (getArguments() != null) { - getArguments().resetScrollState(true); // FIXME upd: ... fixme what? - } - UI.post(() -> { - if (getArguments() != null && currentSection == SECTION_STICKERS) { - getArguments().setCurrentStickerSectionByPosition(getStickerSetSection(), true, true); - getArguments().resetScrollState(true); - } - ignoreStickersScroll--; - }, 400); - } - - private void addStickerSet (TGStickerSetInfo stickerSet, ArrayList items, int index) { - if (index < 0 || index >= stickerSets.size()) { - return; - } - - beforeStickerChanges(); - - if (getArguments() != null) { - getArguments().addStickerSection(index, stickerSet); - } - - int startIndex = stickerSets.get(index).getStartIndex(); - stickerSets.add(index, stickerSet); - for (int i = index; i < stickerSets.size(); i++) { - TGStickerSetInfo nextStickerSet = stickerSets.get(i); - nextStickerSet.setStartIndex(startIndex); - startIndex += nextStickerSet.getSize() + 1; - } - - stickersAdapter.addRange(stickerSet.getStartIndex(), items); - resetScrollCache(); - } - - private int removeStickerSet (TGStickerSetInfo stickerSet) { - int i = stickerSets.indexOf(stickerSet); - if (i != -1) { - beforeStickerChanges(); - stickerSets.remove(i); - if (getArguments() != null) { - getArguments().removeStickerSection(i); - } - int startIndex = stickerSet.getStartIndex(); - stickersAdapter.removeRange(startIndex, stickerSet.getSize() + 1); - for (int j = i; j < stickerSets.size(); j++) { - TGStickerSetInfo nextStickerSet = stickerSets.get(j); - nextStickerSet.setStartIndex(startIndex); - startIndex += nextStickerSet.getSize() + 1; - } - resetScrollCache(); - } - return i; - } - @Override public void onInstalledStickerSetsUpdated (final long[] stickerSetIds, TdApi.StickerType stickerType) { if (stickerType.getConstructor() == TdApi.StickerTypeRegular.CONSTRUCTOR) { @@ -1594,10 +1157,7 @@ public void onInstalledStickerSetsUpdated (final long[] stickerSetIds, TdApi.Sti } public void applyScheduledChanges () { - if (scheduledFeaturedSets != null) { - applyScheduledFeaturedSets(scheduledFeaturedSets); - scheduledFeaturedSets = null; - } + trendingSetsController.applyScheduledFeaturedSets(); } @Override @@ -1628,148 +1188,18 @@ public void onFavoriteStickersUpdated (final int[] stickerIds) { }); } - private TdApi.TrendingStickerSets scheduledFeaturedSets; - @Override public void onTrendingStickersUpdated (final TdApi.StickerType stickerType, final TdApi.TrendingStickerSets stickerSets, int unreadCount) { if (stickerType.getConstructor() != TdApi.StickerTypeRegular.CONSTRUCTOR) return; runOnUiThreadOptional(() -> { if (getArguments() != null) { - getArguments().setHasNewHots(getUnreadCount(stickerSets.sets) > 0); + getArguments().setHasNewHots(EmojiLayout.STICKERS_TRENDING_CONTROLLER_ID, TD.getStickerSetsUnreadCount(stickerSets.sets) > 0); } - scheduleFeaturedSets(stickerSets); + trendingSetsController.scheduleFeaturedSets(stickerSets, contentView.getCurrentSection() == SECTION_TRENDING); }); } - public static int getUnreadCount (TdApi.StickerSetInfo[] stickerSets) { - int unreadCount = 0; - for (TdApi.StickerSetInfo stickerSet : stickerSets) { - if (!stickerSet.isViewed) { - unreadCount++; - } - } - return unreadCount; - } - - private void scheduleFeaturedSets (TdApi.TrendingStickerSets stickerSets) { - if (currentSection == SECTION_TRENDING) { - scheduledFeaturedSets = stickerSets; - } else { - scheduledFeaturedSets = null; - applyScheduledFeaturedSets(stickerSets); - } - } - - private void applyScheduledFeaturedSets () { - if (scheduledFeaturedSets != null) { - applyScheduledFeaturedSets(scheduledFeaturedSets); - scheduledFeaturedSets = null; - } - } - - private void applyScheduledFeaturedSets (TdApi.TrendingStickerSets sets) { - if (sets != null && !isDestroyed() && !trendingLoading) { - if (trendingSets != null && trendingSets.size() == sets.sets.length && !trendingSets.isEmpty()) { - boolean equal = true; - int i = 0; - for (TGStickerSetInfo stickerSetInfo : trendingSets) { - if (stickerSetInfo.getId() != sets.sets[i].id) { - equal = false; - break; - } - boolean visuallyChanged = stickerSetInfo.isViewed() != sets.sets[i].isViewed; - stickerSetInfo.updateState(sets.sets[i]); - if (visuallyChanged) { - trendingAdapter.updateState(stickerSetInfo); - } - i++; - } - if (equal) { - return; - } - } - - final ArrayList stickerItems = new ArrayList<>(sets.sets.length * 2 + 1); - final ArrayList stickerSetInfos = new ArrayList<>(sets.sets.length); - stickerItems.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_KEYBOARD_TOP)); - final int unreadItemCount = parseTrending(tdlib, stickerSetInfos, stickerItems, 0, sets.sets, this, this, false, false); - addTrendingStickers(stickerSetInfos, stickerItems, unreadItemCount > 0, 0); - } - } - - private static final int FLAG_TRENDING = 0x01; - private static final int FLAG_REGULAR = 0x02; - private LongSparseIntArray loadingStickerSets; - - private boolean ignoreRequests; - private long ignoreException; - - private void setIgnoreRequests (boolean ignoreRequests, long exceptSetId) { - if (this.ignoreRequests != ignoreRequests) { - this.ignoreRequests = ignoreRequests; - this.ignoreException = exceptSetId; - if (!ignoreRequests && stickersView != null) { - final int firstVisiblePosition = ((LinearLayoutManager) stickersView.getLayoutManager()).findFirstVisibleItemPosition(); - final int lastVisiblePosition = ((LinearLayoutManager) stickersView.getLayoutManager()).findLastVisibleItemPosition(); - - for (int i = lastVisiblePosition; i >= firstVisiblePosition; i--) { - MediaStickersAdapter.StickerItem item = stickersAdapter.getItem(i); - if (item != null && item.viewType == MediaStickersAdapter.StickerHolder.TYPE_STICKER && item.sticker != null) { - item.sticker.requestRequiredInformation(); - } - } - } - } - } - - @Override - public void viewStickerSet (TGStickerSetInfo stickerSetInfo) { - viewStickerSetInternal(stickerSetInfo.getId()); - } - - @Override - public void requestStickerData (TGStickerObj stickerObj, long stickerSetId) { - if (ignoreRequests && stickerSetId != ignoreException) { // avoiding huge data load while scrolling to section - return; - } - int currentFlags; - if (loadingStickerSets == null) { - loadingStickerSets = new LongSparseIntArray(); - currentFlags = 0; - } else { - currentFlags = loadingStickerSets.get(stickerSetId, 0); - } - if (currentFlags == 0) { - loadingStickerSets.put(stickerSetId, stickerObj.isTrending() ? FLAG_TRENDING : FLAG_REGULAR); - tdlib.client().send(new TdApi.GetStickerSet(stickerSetId), singleStickerSetHandler()); - } else if ((currentFlags & FLAG_TRENDING) == 0 && stickerObj.isTrending()) { - currentFlags |= FLAG_TRENDING; - loadingStickerSets.put(stickerSetId, currentFlags); - } else if ((currentFlags & FLAG_REGULAR) == 0 && !stickerObj.isTrending()) { - currentFlags |= FLAG_REGULAR; - loadingStickerSets.put(stickerSetId, currentFlags); - } - } - - private Client.ResultHandler singleStickerSetHandler () { - return object -> { - switch (object.getConstructor()) { - case TdApi.StickerSet.CONSTRUCTOR: { - final TdApi.StickerSet stickerSet = (TdApi.StickerSet) object; - runOnUiThreadOptional(() -> { - applyStickerSet(stickerSet); - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; - } - } - }; - } - private Client.ResultHandler stickerSetsHandler () { return object -> { switch (object.getConstructor()) { @@ -1847,7 +1277,7 @@ private Client.ResultHandler stickerSetsHandler () { for (int i = 0; i < rawInfo.size; i++) { TGStickerObj sticker = new TGStickerObj(tdlib, i < rawInfo.covers.length ? rawInfo.covers[i] : null, null, rawInfo.stickerType); sticker.setStickerSetId(rawInfo.id, null); - sticker.setDataProvider(this); + sticker.setDataProvider(stickerSetsDataProvider()); items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, sticker)); } startIndex += rawInfo.size + 1; @@ -1910,248 +1340,15 @@ private void scrollToSystemStickers (boolean animated) { if (i == -1) i = findRecentStickerSet(); if (i != -1) - scrollToStickerSet(i == 0 ? 0 : stickerSets.get(i).getStartIndex(), animated); - } - - private FactorAnimator lastScrollAnimator; - private static final int SCROLLBY_SECTION_LIMIT = 8; - - private void scrollToStickerSet (int stickerSetIndex, boolean animated) { - final int futureSection = indexOfStickerSetByAdapterPosition(stickerSetIndex); - if (futureSection == -1) { - return; - } - - stickersView.stopScroll(); - - final int currentSection = getStickerSetSection(); - - if (!animated || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getArguments() == null || Math.abs(futureSection - currentSection) > SCROLLBY_SECTION_LIMIT) { - if (getArguments() != null) { - getArguments().setIgnoreMovement(true); - } - ((LinearLayoutManager) stickersView.getLayoutManager()).scrollToPositionWithOffset(stickerSetIndex, stickerSetIndex == 0 ? 0 : EmojiLayout.getHeaderSize() + EmojiLayout.getHeaderPadding()); - if (getArguments() != null) { - getArguments().setIgnoreMovement(false); - } - } else { - final int scrollTop; - - if (stickerSetIndex == 0) { - scrollTop = 0; - } else { - scrollTop = Math.max(0, stickersAdapter.measureScrollTop(stickerSetIndex, spanCount, futureSection, stickerSets, showRecentTitle) - EmojiLayout.getHeaderSize() - EmojiLayout.getHeaderPadding()); - } - - final int currentScrollTop = getStickersScrollY(); - final int scrollDiff = scrollTop - currentScrollTop; - final int[] totalScrolled = new int[1]; - - if (lastScrollAnimator != null) { - lastScrollAnimator.cancel(); - } - stickersView.setScrollDisabled(true); - setIgnoreRequests(true, stickerSets.get(futureSection).getId()); - if (getArguments() != null) { - getArguments().setIgnoreMovement(true); - getArguments().setCurrentStickerSectionByPosition(futureSection, true, true); - } - - lastScrollAnimator = new FactorAnimator(0, new FactorAnimator.Target() { - @Override - public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { - int diff = (int) ((float) scrollDiff * factor); - stickersView.scrollBy(0, diff - totalScrolled[0]); - totalScrolled[0] = diff; - } - - @Override - public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { - stickersView.setScrollDisabled(false); - setIgnoreRequests(false, 0); - if (getArguments() != null) { - getArguments().setIgnoreMovement(false); - } - } - }, AnimatorUtils.DECELERATE_INTERPOLATOR, Math.min(450, Math.max(250, Math.abs(currentSection - futureSection) * 150))); - lastScrollAnimator.animateTo(1f); - - // stickersView.smoothScrollBy(0, scrollTop - currentScrollTop); - } - } - - private LongSparseArray pendingViewStickerSets; - private CancellableRunnable viewSets; - - private void viewStickerSetInternal (long stickerSetId) { - if (pendingViewStickerSets == null) { - pendingViewStickerSets = new LongSparseArray<>(); - } else if (pendingViewStickerSets.indexOfKey(stickerSetId) >= 0) { - return; - } - pendingViewStickerSets.put(stickerSetId, true); - if (viewSets != null) { - viewSets.cancel(); - } - viewSets = new CancellableRunnable() { - @Override - public void act () { - if (pendingViewStickerSets != null && pendingViewStickerSets.size() > 0) { - final int size = pendingViewStickerSets.size(); - long[] setIds = new long[size]; - for (int i = 0; i < size; i++) { - setIds[i] = pendingViewStickerSets.keyAt(i); - } - pendingViewStickerSets.clear(); - tdlib.client().send(new TdApi.ViewTrendingStickerSets(setIds), tdlib.okHandler()); - } - } - }; - UI.post(viewSets, 750l); - } - - private void applyStickerSet (TdApi.StickerSet stickerSet) { - int flags = loadingStickerSets.get(stickerSet.id); - loadingStickerSets.delete(stickerSet.id); - - if (flags == 0) { - return; - } - - final int actualSize = stickerSet.stickers.length; - - if ((flags & FLAG_TRENDING) != 0) { - if (trendingSets == null || trendingSets.isEmpty()) { - return; - } - for (TGStickerSetInfo oldStickerSet : trendingSets) { - if (oldStickerSet.getId() == stickerSet.id) { - oldStickerSet.setStickerSet(stickerSet); - for (int stickerIndex = oldStickerSet.getCoverCount(), j = oldStickerSet.getStartIndex() + 1 + oldStickerSet.getCoverCount(); stickerIndex < Math.min(stickerSet.stickers.length - oldStickerSet.getCoverCount(), oldStickerSet.getCoverCount() + 4); stickerIndex++, j++) { - MediaStickersAdapter.StickerItem item = trendingAdapter.getItem(j); - if (item.sticker != null) { - TdApi.Sticker sticker = stickerSet.stickers[stickerIndex]; - item.sticker.set(tdlib, sticker, sticker.fullType, stickerSet.emojis[stickerIndex].emojis); - } - - View view = hotView != null ? hotView.getLayoutManager().findViewByPosition(j) : null; - if (view != null && view instanceof StickerSmallView && view.getTag() == item) { - ((StickerSmallView) view).refreshSticker(); - } else { - trendingAdapter.notifyItemChanged(j); - } - } - break; - } - } - } - - if ((flags & FLAG_REGULAR) != 0) { - if (stickerSets == null || stickerSets.isEmpty()) { - return; - } - int i = 0; - for (TGStickerSetInfo oldStickerSet : stickerSets) { - if (oldStickerSet.isSystem()) { - i++; - continue; - } - if (oldStickerSet.getId() == stickerSet.id) { - oldStickerSet.setStickerSet(stickerSet); - final int oldSize = oldStickerSet.getSize(); - // If something has suddenly changed with this sticker set - if (oldSize != actualSize) { - if (actualSize == 0) { - if (getArguments() != null) { - getArguments().setIgnoreMovement(true); - } - stickerSets.remove(i); - if (stickerSets.isEmpty()) { - stickersAdapter.setItem(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_NO_STICKERSETS)); - } else { - // Shifting next sticker sets bounds - int startIndex; - if (i != 0) { - TGStickerSetInfo prevStickerSet = stickerSets.get(i - 1); - startIndex = prevStickerSet.getStartIndex() + prevStickerSet.getSize() + 1; - } else { - startIndex = 1; - } - for (int j = i; j < stickerSets.size(); j++) { - TGStickerSetInfo nextStickerSet = stickerSets.get(j); - nextStickerSet.setStartIndex(startIndex); - startIndex += nextStickerSet.getSize() + 1; - } - stickersAdapter.removeRange(oldStickerSet.getStartIndex(), oldStickerSet.getSize() + 1); - } - - if (getArguments() != null) { - getArguments().setIgnoreMovement(false); - } - - return; - } else { - oldStickerSet.setSize(actualSize); - - // Shifting next sticker sets bounds - int startIndex = oldStickerSet.getStartIndex() + actualSize + 1; - for (int j = i + 1; j < stickerSets.size(); j++) { - TGStickerSetInfo nextStickerSet = stickerSets.get(j); - nextStickerSet.setStartIndex(startIndex); - startIndex += nextStickerSet.getSize() + 1; - } - - if (actualSize < oldSize) { - stickersAdapter.removeRange(oldStickerSet.getStartIndex() + 1 + actualSize, oldSize - actualSize); - } else { - ArrayList items = new ArrayList<>(actualSize - oldSize); - for (int j = oldSize; j < actualSize; j++) { - TdApi.Sticker sticker = stickerSet.stickers[j]; - TGStickerObj obj = new TGStickerObj(tdlib, sticker, sticker.fullType, stickerSet.emojis[j].emojis); - obj.setStickerSetId(stickerSet.id, stickerSet.emojis[j].emojis); - obj.setDataProvider(this); - items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, obj)); - } - stickersAdapter.insertRange(oldStickerSet.getStartIndex() + 1 + oldSize, items); - } - } - - if (getArguments() != null) { - getArguments().setIgnoreMovement(false); - } - } - - for (int stickerIndex = oldStickerSet.getCoverCount(), j = oldStickerSet.getStartIndex() + 1 + oldStickerSet.getCoverCount(); stickerIndex < stickerSet.stickers.length; stickerIndex++, j++) { - MediaStickersAdapter.StickerItem item = stickersAdapter.getItem(j); - TdApi.Sticker sticker = stickerSet.stickers[stickerIndex]; - item.sticker.set(tdlib, sticker, sticker.fullType, stickerSet.emojis[stickerIndex].emojis); - - View view = stickersView != null ? stickersView.getLayoutManager().findViewByPosition(j) : null; - if (view != null && view instanceof StickerSmallView) { - ((StickerSmallView) view).refreshSticker(); - } else { - stickersAdapter.notifyItemChanged(j); - } - } - - break; - } - i++; - } - } + stickersController.scrollToStickerSet(i == 0 ? 0 : stickersController.stickerSets.get(i).getStartIndex(), showRecentTitle, animated); } - private ArrayList stickerSets; - private void setStickers (ArrayList stickerSets, ArrayList items) { - this.stickerSets = stickerSets; + this.stickersController.setStickers(stickerSets, items); this.loadingStickers = false; - this.lastStickerSetInfo = null; - if (loadingStickerSets != null) { - this.loadingStickerSets.clear(); + if (stickerSetsDataProvider != null) { + this.stickerSetsDataProvider.clear(); } - stickersAdapter.setItems(items); - tdlib.listeners().subscribeToStickerUpdates(this); } @@ -2200,159 +1397,35 @@ private void setGIFs (ArrayList gifs) { this.loadingGIFs = false; this.gifs = gifs; gifsAdapter.setGIFs(gifs); - if (gifs.isEmpty() && currentSection == SECTION_GIFS) { + if (gifs.isEmpty() && contentView.getCurrentSection() == SECTION_GIFS) { showStickers(); } tdlib.listeners().subscribeForAnimationsUpdates(this); } - // Section changer - - private View currentSectionView; - private int nextSection = -1; - private View nextSectionView; - private boolean sectionIsLeft; - - private static final int CHANGE_SECTION_ANIMATOR = 0; - private FactorAnimator sectionAnimator; - private float sectionChangeFactor; - - private boolean changeSection (int sectionId, View sectionView, boolean fromLeft, int stickerSetSection) { - if (currentSection == sectionId || !canChangeSection()) { - return false; - } - - this.nextSection = sectionId; - this.nextSectionView = sectionView; - this.sectionIsLeft = fromLeft; - - this.contentView.addView(sectionView); - - if (this.sectionAnimator == null) { - this.sectionAnimator = new FactorAnimator(CHANGE_SECTION_ANIMATOR, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); - } - - this.sectionAnimator.animateTo(1f); - - if (getArguments() != null) { - if (getArguments().getCurrentItem() == 1) { - if (currentSection == SECTION_GIFS && (nextSection == SECTION_STICKERS || nextSection == SECTION_TRENDING)) { - getArguments().setCircleVisible(false, false); - } else if ((currentSection == SECTION_STICKERS || currentSection == SECTION_TRENDING) && nextSection == SECTION_GIFS) { - getArguments().setCircleVisible(true, true); - } - } - getArguments().setCurrentStickerSectionByPosition( - nextSection == SECTION_STICKERS ? (stickerSetSection != -1 ? stickerSetSection : getStickerSetSection()) : - nextSection == SECTION_TRENDING ? 2 : 1 - , nextSection == SECTION_STICKERS,true); - } - - return true; - } - - private void updatePositions () { - if (sectionIsLeft != Lang.rtl()) { - currentSectionView.setTranslationX((float) currentSectionView.getMeasuredWidth() * sectionChangeFactor); - if (nextSectionView != null) { - nextSectionView.setTranslationX((float) (-nextSectionView.getMeasuredWidth()) * (1f - sectionChangeFactor)); - } - } else { - currentSectionView.setTranslationX((float) (-currentSectionView.getMeasuredWidth()) * sectionChangeFactor); - if (nextSectionView != null) { - nextSectionView.setTranslationX((float) nextSectionView.getMeasuredWidth() * (1f - sectionChangeFactor)); - } - } - } - - private boolean canChangeSection () { - return sectionAnimator == null || (!sectionAnimator.isAnimating() && sectionAnimator.getFactor() == 0f && sectionChangeFactor == 0f); - } - - private void applySection () { - contentView.removeView(currentSectionView); - - int oldSection = this.currentSection; - - if (getArguments() != null) { - if (currentSection == SECTION_GIFS && (nextSection == SECTION_STICKERS || nextSection == SECTION_TRENDING)) { - getArguments().setPreferredSection(EmojiMediaType.STICKER); - } else if (nextSection == SECTION_GIFS && (currentSection == SECTION_STICKERS || currentSection == SECTION_TRENDING)) { - getArguments().setPreferredSection(EmojiMediaType.GIF); - } - } - - if (currentSection == SECTION_TRENDING && nextSection != SECTION_TRENDING) { - applyScheduledFeaturedSets(); - } - - currentSection = nextSection; - nextSection = -1; - currentSectionView = nextSectionView; - nextSectionView = null; - sectionAnimator.forceFactor(0f); - sectionChangeFactor = 0f; - - if (getArguments() != null) { - EmojiLayout.Listener listener = getArguments().getListener(); - if (listener != null) { - int prevSection = oldSection == SECTION_GIFS ? EmojiMediaType.GIF : EmojiMediaType.STICKER; - int newSection = currentSection == SECTION_GIFS ? EmojiMediaType.GIF : EmojiMediaType.STICKER; - listener.onSectionSwitched(getArguments(), newSection, prevSection); - } - } - - if (getArguments() != null) { - getArguments().resetScrollState(); - } - } - - @Override - public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { - switch (id) { - case CHANGE_SECTION_ANIMATOR: { - this.sectionChangeFactor = factor; - updatePositions(); - break; - } - } - } - - @Override - public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { - switch (id) { - case CHANGE_SECTION_ANIMATOR: { - if (finalFactor == 1f) { - applySection(); - } - break; - } - } - } - // When user clicked public void showHot () { - if (canChangeSection()) { + if (contentView.canChangeSection()) { initHots(); - if (currentSection == SECTION_TRENDING && canChangeSection()) { - hotView.smoothScrollBy(0, -getTrendingScrollY()); + if (contentView.getCurrentSection() == SECTION_TRENDING && contentView.canChangeSection()) { + trendingSetsController.recyclerView.smoothScrollBy(0, -trendingSetsController.getStickersScrollY(showRecentTitle)); } else { - changeSection(SECTION_TRENDING, hotView, currentSection != SECTION_GIFS, -1); + contentView.changeSection(SECTION_TRENDING, contentView.getCurrentSection() != SECTION_GIFS, -1); } } } public boolean showGIFs () { - if (canChangeSection() && gifs != null && !gifs.isEmpty()) { + if (contentView.canChangeSection() && gifs != null && !gifs.isEmpty()) { initGIFs(); - return changeSection(SECTION_GIFS, gifsView, true, -1); + return contentView.changeSection(SECTION_GIFS, true, -1); } return false; } public boolean needSearchButton () { - switch (currentSection) { + switch (contentView.getCurrentSection()) { case SECTION_GIFS: return getGIFsScrollY() == 0; /*case SECTION_STICKERS: @@ -2362,29 +1435,62 @@ public boolean needSearchButton () { } public void showStickers () { - if (canChangeSection()) { + if (contentView.canChangeSection()) { initStickers(); - changeSection(SECTION_STICKERS, stickersView, false, -1); + contentView.changeSection(SECTION_STICKERS, false, -1); } } public void showSystemStickers () { - if (canChangeSection()) { + if (contentView.canChangeSection()) { initStickers(); - scrollToSystemStickers(currentSection == SECTION_STICKERS && (sectionAnimator == null || !sectionAnimator.isAnimating())); - changeSection(SECTION_STICKERS, stickersView, false, 0); + scrollToSystemStickers(contentView.getCurrentSection() == SECTION_STICKERS && contentView.isAnimationNotActive()); + contentView.changeSection(SECTION_STICKERS, false, 0); } } public boolean showStickerSet (TGStickerSetInfo stickerSet) { - if (canChangeSection()) { - int i = indexOfStickerSet(stickerSet); + if (contentView.canChangeSection()) { + int i = stickersController.indexOfStickerSet(stickerSet); if (i != -1) { initStickers(); - scrollToStickerSet(i, currentSection == SECTION_STICKERS && (sectionAnimator == null || !sectionAnimator.isAnimating())); - return changeSection(SECTION_STICKERS, stickersView, false, indexOfStickerSetByAdapterPosition(i)); + stickersController.scrollToStickerSet(i, showRecentTitle, contentView.getCurrentSection() == SECTION_STICKERS && contentView.isAnimationNotActive()); + return contentView.changeSection(SECTION_STICKERS, false, stickersController.indexOfStickerSetByAdapterPosition(i)); } } return false; } + + + /* Data provider */ + + private StickerSetsDataProvider stickerSetsDataProvider; + + private StickerSetsDataProvider stickerSetsDataProvider() { + if (stickerSetsDataProvider != null) { + return stickerSetsDataProvider; + } + + return stickerSetsDataProvider = new StickerSetsDataProvider(tdlib) { + @Override + protected boolean needIgnoreRequests (long stickerSetId, TGStickerObj stickerObj) { + return stickersController.isIgnoreRequests(stickerSetId); + } + + @Override + protected int getLoadingFlags (long stickerSetId, TGStickerObj stickerObj) { + return stickerObj.isTrending() ? FLAG_TRENDING: FLAG_REGULAR; + } + + @Override + protected void applyStickerSet (TdApi.StickerSet stickerSet, int flags) { + if ((flags & FLAG_REGULAR) != 0) { + stickersController.applyStickerSet(stickerSet, this); + } + if ((flags & FLAG_TRENDING) != 0) { + trendingSetsController.applyStickerSet(stickerSet, this); + } + } + }; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EmojiStatusListController.java b/app/src/main/java/org/thunderdog/challegram/ui/EmojiStatusListController.java index 7f196621ab..cf3fe88f43 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/EmojiStatusListController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/EmojiStatusListController.java @@ -33,17 +33,20 @@ import org.thunderdog.challegram.component.attach.CustomItemAnimator; import org.thunderdog.challegram.component.emoji.GifView; import org.thunderdog.challegram.component.emoji.MediaStickersAdapter; +import org.thunderdog.challegram.component.sticker.StickerPreviewView; import org.thunderdog.challegram.component.sticker.StickerSmallView; import org.thunderdog.challegram.component.sticker.TGStickerObj; import org.thunderdog.challegram.config.Config; +import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGStickerSetInfo; +import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.mediaview.MediaCellView; import org.thunderdog.challegram.mediaview.data.MediaItem; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.telegram.AnimationsListener; -import org.thunderdog.challegram.telegram.EmojiMediaType; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; @@ -52,6 +55,7 @@ import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.v.RtlGridLayoutManager; import org.thunderdog.challegram.widget.EmojiLayout; +import org.thunderdog.challegram.util.StickerSetsDataProvider; import org.thunderdog.challegram.widget.ForceTouchView; import java.util.ArrayList; @@ -60,15 +64,14 @@ import me.vkryl.android.animator.FactorAnimator; import me.vkryl.android.util.ClickHelper; import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.BitwiseUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.collection.IntList; import me.vkryl.core.collection.LongList; -import me.vkryl.core.collection.LongSparseIntArray; public class EmojiStatusListController extends ViewController implements - StickerSmallView.StickerMovementCallback, + StickerSmallView.StickerMovementCallback, StickerPreviewView.MenuStickerPreviewCallback, AnimationsListener, - TGStickerObj.DataProvider, ClickHelper.Delegate, ForceTouchView.ActionListener { @@ -97,6 +100,7 @@ protected View onCreateView (Context context) { stickersAdapter = new MediaStickersAdapter(this, this, false, this); stickersAdapter.setItem(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_PROGRESS)); + stickersAdapter.setMenuStickerPreviewCallback(this); checkSpanCount(); @@ -115,10 +119,7 @@ private int getStickerSetSection () { if (stickersView == null || spanCount == 0) { return -1; } - int i = ((LinearLayoutManager) stickersView.getLayoutManager()).findFirstCompletelyVisibleItemPosition(); - if (i == -1) { - i = ((LinearLayoutManager) stickersView.getLayoutManager()).findFirstVisibleItemPosition(); - } + int i = Views.findFirstCompletelyVisibleItemPositionWithOffset((LinearLayoutManager) stickersView.getLayoutManager(), EmojiLayout.getHeaderSize() / 2); if (i != -1) { return indexOfStickerSetByAdapterPosition(i); } @@ -134,7 +135,7 @@ private int getStickersScrollY () { View v = stickersView.getLayoutManager().findViewByPosition(i); int additional = v != null ? -v.getTop() : 0; int stickerSet = indexOfStickerSetByAdapterPosition(i); - return additional + stickersAdapter.measureScrollTop(i, spanCount, stickerSet, stickerSets, false); + return additional + stickersAdapter.measureScrollTop(i, spanCount, stickerSet, stickerSets, null, false); } return 0; } @@ -159,7 +160,7 @@ public int getSpanSize (int position) { } }); - stickersView = stickersViewToSet != null ? stickersViewToSet: + stickersView = stickersViewToSet != null ? stickersViewToSet : (CustomRecyclerView) Views.inflate(context(), R.layout.recycler_custom, contentView); stickersView.setHasFixedSize(true); @@ -189,9 +190,8 @@ private void onStickersScroll (int movedDy) { if (ignoreStickersScroll == 0) { if (getArguments() != null && getArguments().isWatchingMovements() && getArguments().getCurrentItem() == 0) { int y = getStickersScrollY(); - getArguments().onScroll(y); - getArguments().setCurrentStickerSectionByPosition(getStickerSetSection(), true, true); - getArguments().onSectionScroll(EmojiMediaType.STICKER, movedDy != 0); + getArguments().moveHeader(y); + getArguments().setCurrentStickerSectionByPosition(EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID, getStickerSetSection(), true, true); } } } @@ -403,27 +403,6 @@ public boolean canFindChildViewUnder (StickerSmallView view, int recyclerX, int return parent != null && recyclerY > parent.getHeaderBottom(); } - @Override - public void onStickerPreviewOpened (StickerSmallView view, TGStickerObj sticker) { - if (getArguments() != null) { - getArguments().onSectionInteracted(EmojiMediaType.STICKER, false); - } - } - - @Override - public void onStickerPreviewChanged (StickerSmallView view, TGStickerObj otherOrThisSticker) { - if (getArguments() != null) { - getArguments().onSectionInteracted(EmojiMediaType.STICKER, false); - } - } - - @Override - public void onStickerPreviewClosed (StickerSmallView view, TGStickerObj thisSticker) { - if (getArguments() != null) { - getArguments().onSectionInteracted(EmojiMediaType.STICKER, true); - } - } - @Override public boolean needsLongDelay (StickerSmallView view) { return false; @@ -676,6 +655,7 @@ public void onResult (TdApi.Object object) { int i = 0; for (TdApi.Sticker sticker : stickers) { TGStickerObj parsed = new TGStickerObj(tdlib, sticker, sticker.fullType, rawStickerSet.emojis[i].emojis); + parsed.setPreviewOptimizationMode(GifFile.OptimizationMode.EMOJI_PREVIEW); items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, parsed)); i++; } @@ -712,7 +692,7 @@ private void moveStickerSet (int oldPosition, int newPosition) { beforeStickerChanges(); if (getArguments() != null) { - getArguments().moveStickerSection(oldPosition, newPosition); + getArguments().onMoveStickerSection(EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID, oldPosition, newPosition); } TGStickerSetInfo stickerSet = stickerSets.remove(oldPosition); @@ -748,12 +728,12 @@ private void resetScrollCache () { // ignoreStickersScroll--; if (getArguments() != null) { - getArguments().resetScrollState(true); // FIXME upd: ... fixme what? + getArguments().resetScrollState(false); // FIXME upd: ... fixme what? } UI.post(() -> { if (getArguments() != null) { - getArguments().setCurrentStickerSectionByPosition(getStickerSetSection(), true, true); - getArguments().resetScrollState(true); + getArguments().setCurrentStickerSectionByPosition(EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID, getStickerSetSection(), true, true); + getArguments().resetScrollState(false); } ignoreStickersScroll--; }, 400); @@ -767,7 +747,7 @@ private void addStickerSet (TGStickerSetInfo stickerSet, ArrayList { - switch (object.getConstructor()) { - case TdApi.StickerSet.CONSTRUCTOR: { - final TdApi.StickerSet stickerSet = (TdApi.StickerSet) object; - runOnUiThreadOptional(() -> applyStickerSet(stickerSet)); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; - } - } - }; - } - private Client.ResultHandler stickerSetsHandler (boolean needAddDefaultPremiumStar) { return object -> { switch (object.getConstructor()) { @@ -891,7 +829,7 @@ private Client.ResultHandler stickerSetsHandler (boolean needAddDefaultPremiumSt items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_KEYBOARD_TOP)); int startIndex = 1; - final int totalRecentCount = recentStickers != null ? recentStickers.length: 0; + final int totalRecentCount = recentStickers != null ? recentStickers.length : 0; final int totalTrendingCount = trendingStickers != null ? trendingStickers.length : 0; if (totalRecentCount > 0) { TGStickerSetInfo info = new TGStickerSetInfo(tdlib, recentStickers, false, totalRecentCount); @@ -901,6 +839,7 @@ private Client.ResultHandler stickerSetsHandler (boolean needAddDefaultPremiumSt int remainingCount = totalRecentCount; for (TdApi.Sticker recentSticker : recentStickers) { TGStickerObj sticker = new TGStickerObj(tdlib, recentSticker, null, recentSticker.fullType); + sticker.setPreviewOptimizationMode(GifFile.OptimizationMode.EMOJI_PREVIEW); sticker.setIsRecent(); items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, sticker)); if (--remainingCount == 0) { @@ -922,13 +861,14 @@ private Client.ResultHandler stickerSetsHandler (boolean needAddDefaultPremiumSt int remainingCount = totalTrendingCount; for (TdApi.Sticker trendingSticker : trendingStickers) { TGStickerObj sticker = new TGStickerObj(tdlib, trendingSticker, null, trendingSticker.fullType); + sticker.setPreviewOptimizationMode(GifFile.OptimizationMode.EMOJI_PREVIEW); sticker.setIsRecent(); items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, sticker)); if (--remainingCount == 0) { break; } } - startIndex += totalTrendingCount + (needAddDefaultPremiumStar ? 2: 1); + startIndex += totalTrendingCount + (needAddDefaultPremiumStar ? 2 : 1); } for (TdApi.StickerSetInfo rawInfo : rawStickerSets) { @@ -941,8 +881,9 @@ private Client.ResultHandler stickerSetsHandler (boolean needAddDefaultPremiumSt items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER, info)); for (int i = 0; i < rawInfo.size; i++) { TGStickerObj sticker = new TGStickerObj(tdlib, i < rawInfo.covers.length ? rawInfo.covers[i] : null, null, rawInfo.stickerType); + sticker.setPreviewOptimizationMode(GifFile.OptimizationMode.EMOJI_PREVIEW); sticker.setStickerSetId(rawInfo.id, null); - sticker.setDataProvider(this); + sticker.setDataProvider(stickerSetsDataProvider()); items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, sticker)); } startIndex += rawInfo.size + 1; @@ -1034,10 +975,10 @@ private void getCustomEmojiStatusList (TdApi.Function req, tdlib.client().send(req, object2 -> { switch (object2.getConstructor()) { case TdApi.EmojiStatuses.CONSTRUCTOR: { - TdApi.EmojiStatus[] emojiStatuses2 = ((TdApi.EmojiStatuses) object2).emojiStatuses; - for (TdApi.EmojiStatus emojiStatus : emojiStatuses2) { + long[] customEmojiIds = ((TdApi.EmojiStatuses) object2).customEmojiIds; + for (long customEmojiId : customEmojiIds) { if (longList.size() >= 200) break; - longList.append(emojiStatus.customEmojiId); + longList.append(customEmojiId); } onReceive.run(); break; @@ -1131,7 +1072,7 @@ private void scrollToStickerSet (int stickerSetIndex, boolean animated) { if (stickerSetIndex == 0) { scrollTop = 0; } else { - scrollTop = Math.max(0, stickersAdapter.measureScrollTop(stickerSetIndex, spanCount, futureSection, stickerSets, false) - EmojiLayout.getHeaderSize() - EmojiLayout.getHeaderPadding()); + scrollTop = Math.max(0, stickersAdapter.measureScrollTop(stickerSetIndex, spanCount, futureSection, stickerSets, null, false) - EmojiLayout.getHeaderSize() - EmojiLayout.getHeaderPadding()); } final int currentScrollTop = getStickersScrollY(); @@ -1145,7 +1086,7 @@ private void scrollToStickerSet (int stickerSetIndex, boolean animated) { setIgnoreRequests(true, stickerSets.get(futureSection).getId()); if (getArguments() != null) { getArguments().setIgnoreMovement(true); - getArguments().setCurrentStickerSectionByPosition(futureSection, true, true); + getArguments().setCurrentStickerSectionByPosition(EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID, futureSection, true, true); } lastScrollAnimator = new FactorAnimator(0, new FactorAnimator.Target() { @@ -1172,106 +1113,98 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca } private void applyStickerSet (TdApi.StickerSet stickerSet) { - int flags = loadingStickerSets.get(stickerSet.id); - loadingStickerSets.delete(stickerSet.id); - - if (flags == 0) { + final int actualSize = stickerSet.stickers.length; + if (stickerSets == null || stickerSets.isEmpty()) { return; } - - final int actualSize = stickerSet.stickers.length; - if ((flags & FLAG_REGULAR) != 0) { - if (stickerSets == null || stickerSets.isEmpty()) { - return; + int i = 0; + for (TGStickerSetInfo oldStickerSet : stickerSets) { + if (oldStickerSet.isSystem()) { + i++; + continue; } - int i = 0; - for (TGStickerSetInfo oldStickerSet : stickerSets) { - if (oldStickerSet.isSystem()) { - i++; - continue; - } - if (oldStickerSet.getId() == stickerSet.id) { - oldStickerSet.setStickerSet(stickerSet); - final int oldSize = oldStickerSet.getSize(); - // If something has suddenly changed with this sticker set - if (oldSize != actualSize) { - if (actualSize == 0) { - if (getArguments() != null) { - getArguments().setIgnoreMovement(true); - } - stickerSets.remove(i); - if (stickerSets.isEmpty()) { - stickersAdapter.setItem(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_NO_EMOJISETS)); - } else { - // Shifting next sticker sets bounds - int startIndex; - if (i != 0) { - TGStickerSetInfo prevStickerSet = stickerSets.get(i - 1); - startIndex = prevStickerSet.getStartIndex() + prevStickerSet.getSize() + 1; - } else { - startIndex = 1; - } - for (int j = i; j < stickerSets.size(); j++) { - TGStickerSetInfo nextStickerSet = stickerSets.get(j); - nextStickerSet.setStartIndex(startIndex); - startIndex += nextStickerSet.getSize() + 1; - } - stickersAdapter.removeRange(oldStickerSet.getStartIndex(), oldStickerSet.getSize() + 1); - } - - if (getArguments() != null) { - getArguments().setIgnoreMovement(false); - } - - return; + if (oldStickerSet.getId() == stickerSet.id) { + oldStickerSet.setStickerSet(stickerSet); + final int oldSize = oldStickerSet.getSize(); + // If something has suddenly changed with this sticker set + if (oldSize != actualSize) { + if (actualSize == 0) { + if (getArguments() != null) { + getArguments().setIgnoreMovement(true); + } + stickerSets.remove(i); + if (stickerSets.isEmpty()) { + stickersAdapter.setItem(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_NO_EMOJISETS)); } else { - oldStickerSet.setSize(actualSize); - // Shifting next sticker sets bounds - int startIndex = oldStickerSet.getStartIndex() + actualSize + 1; - for (int j = i + 1; j < stickerSets.size(); j++) { + int startIndex; + if (i != 0) { + TGStickerSetInfo prevStickerSet = stickerSets.get(i - 1); + startIndex = prevStickerSet.getStartIndex() + prevStickerSet.getSize() + 1; + } else { + startIndex = 1; + } + for (int j = i; j < stickerSets.size(); j++) { TGStickerSetInfo nextStickerSet = stickerSets.get(j); nextStickerSet.setStartIndex(startIndex); startIndex += nextStickerSet.getSize() + 1; } - - if (actualSize < oldSize) { - stickersAdapter.removeRange(oldStickerSet.getStartIndex() + 1 + actualSize, oldSize - actualSize); - } else { - ArrayList items = new ArrayList<>(actualSize - oldSize); - for (int j = oldSize; j < actualSize; j++) { - TdApi.Sticker sticker = stickerSet.stickers[j]; - TGStickerObj obj = new TGStickerObj(tdlib, sticker, sticker.fullType, stickerSet.emojis[j].emojis); - obj.setStickerSetId(stickerSet.id, stickerSet.emojis[j].emojis); - obj.setDataProvider(this); - items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, obj)); - } - stickersAdapter.insertRange(oldStickerSet.getStartIndex() + 1 + oldSize, items); - } + stickersAdapter.removeRange(oldStickerSet.getStartIndex(), oldStickerSet.getSize() + 1); } if (getArguments() != null) { getArguments().setIgnoreMovement(false); } - } - for (int stickerIndex = oldStickerSet.getCoverCount(), j = oldStickerSet.getStartIndex() + 1 + oldStickerSet.getCoverCount(); stickerIndex < stickerSet.stickers.length; stickerIndex++, j++) { - MediaStickersAdapter.StickerItem item = stickersAdapter.getItem(j); - TdApi.Sticker sticker = stickerSet.stickers[stickerIndex]; - item.sticker.set(tdlib, sticker, sticker.fullType, stickerSet.emojis[stickerIndex].emojis); + return; + } else { + oldStickerSet.setSize(actualSize); + + // Shifting next sticker sets bounds + int startIndex = oldStickerSet.getStartIndex() + actualSize + 1; + for (int j = i + 1; j < stickerSets.size(); j++) { + TGStickerSetInfo nextStickerSet = stickerSets.get(j); + nextStickerSet.setStartIndex(startIndex); + startIndex += nextStickerSet.getSize() + 1; + } - View view = stickersView != null ? stickersView.getLayoutManager().findViewByPosition(j) : null; - if (view instanceof StickerSmallView) { - ((StickerSmallView) view).refreshSticker(); + if (actualSize < oldSize) { + stickersAdapter.removeRange(oldStickerSet.getStartIndex() + 1 + actualSize, oldSize - actualSize); } else { - stickersAdapter.notifyItemChanged(j); + ArrayList items = new ArrayList<>(actualSize - oldSize); + for (int j = oldSize; j < actualSize; j++) { + TdApi.Sticker sticker = stickerSet.stickers[j]; + TGStickerObj obj = new TGStickerObj(tdlib, sticker, sticker.fullType, stickerSet.emojis[j].emojis); + obj.setPreviewOptimizationMode(GifFile.OptimizationMode.EMOJI_PREVIEW); + obj.setStickerSetId(stickerSet.id, stickerSet.emojis[j].emojis); + obj.setDataProvider(stickerSetsDataProvider()); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, obj)); + } + stickersAdapter.insertRange(oldStickerSet.getStartIndex() + 1 + oldSize, items); } } - break; + if (getArguments() != null) { + getArguments().setIgnoreMovement(false); + } } - i++; + + for (int stickerIndex = oldStickerSet.getCoverCount(), j = oldStickerSet.getStartIndex() + 1 + oldStickerSet.getCoverCount(); stickerIndex < stickerSet.stickers.length; stickerIndex++, j++) { + MediaStickersAdapter.StickerItem item = stickersAdapter.getItem(j); + TdApi.Sticker sticker = stickerSet.stickers[stickerIndex]; + item.sticker.set(tdlib, sticker, sticker.fullType, stickerSet.emojis[stickerIndex].emojis); + + View view = stickersView != null ? stickersView.getLayoutManager().findViewByPosition(j) : null; + if (view instanceof StickerSmallView) { + ((StickerSmallView) view).refreshSticker(); + } else { + stickersAdapter.notifyItemChanged(j); + } + } + + break; } + i++; } } @@ -1281,8 +1214,8 @@ private void setStickers (ArrayList stickerSets, ArrayList filtered = new ArrayList<>(); - for (TdApi.StickerSetInfo set: trendingStickerSets.sets) { + for (TdApi.StickerSetInfo set : trendingStickerSets.sets) { if (!isContainStickerSet(set)) { filtered.add(set); } else { @@ -1358,7 +1286,7 @@ private void loadNextTrending () { } } - EmojiMediaListController.parseTrending(tdlib, parsedStickerSets, items, stickersAdapter.getItemCount(), filtered.toArray(new TdApi.StickerSetInfo[0]), this, null, false, true); + EmojiMediaListController.parseTrending(tdlib, parsedStickerSets, items, stickersAdapter.getItemCount(), filtered.toArray(new TdApi.StickerSetInfo[0]), stickerSetsDataProvider(), null, false, true, null); } runOnUiThreadOptional(() -> { @@ -1378,8 +1306,8 @@ private void addTrendingStickers (ArrayList trendingSets, Arra this.trendingSets.clear(); } this.trendingSets.addAll(trendingSets); - for (TGStickerSetInfo info: trendingSets) { - getArguments().addStickerSection(stickerSets.size(), info); + for (TGStickerSetInfo info : trendingSets) { + getArguments().onAddStickerSection(EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID, stickerSets.size(), info); stickerSets.add(info); } } @@ -1392,4 +1320,119 @@ private void addTrendingStickers (ArrayList trendingSets, Arra loadNextTrending(); } + + /* Data provider */ + + private StickerSetsDataProvider stickerSetsDataProvider; + + private StickerSetsDataProvider stickerSetsDataProvider() { + if (stickerSetsDataProvider != null) { + return stickerSetsDataProvider; + } + + return stickerSetsDataProvider = new StickerSetsDataProvider(tdlib) { + @Override + protected boolean needIgnoreRequests (long stickerSetId, TGStickerObj stickerObj) { + return ignoreRequests && stickerSetId != ignoreException; + } + + @Override + protected int getLoadingFlags (long stickerSetId, TGStickerObj stickerObj) { + return FLAG_REGULAR; + } + + @Override + protected void applyStickerSet (TdApi.StickerSet stickerSet, int flags) { + if (BitwiseUtils.hasFlag(flags, FLAG_REGULAR)) { + EmojiStatusListController.this.applyStickerSet(stickerSet); + } + } + }; + } + + + + /* Preview Sticker Menu */ + + @Override + public void buildMenuStickerPreview (ArrayList menuItems, @NonNull TGStickerObj sticker, @NonNull StickerSmallView stickerSmallView) { + menuItems.add(new StickerPreviewView.MenuItem(StickerPreviewView.MenuItem.MENU_ITEM_TEXT, + Lang.getString(R.string.SetEmojiAsStatus).toUpperCase(), R.id.btn_setEmojiStatus, ColorId.textNeutral)); + + menuItems.add(new StickerPreviewView.MenuItem(StickerPreviewView.MenuItem.MENU_ITEM_TEXT, + Lang.getString(R.string.SetEmojiAsStatusTimed).toUpperCase(), R.id.btn_setEmojiStatusTimed, ColorId.textNeutral)); + } + + @Override + public void onMenuStickerPreviewClick (View v, ViewController context, @NonNull TGStickerObj sticker, @NonNull StickerSmallView stickerSmallView) { + final long emojiId = sticker.getCustomEmojiId(); + final int viewId = v.getId(); + if (viewId == R.id.btn_setEmojiStatus) { + tdlib.client().send(new TdApi.SetEmojiStatus(new TdApi.EmojiStatus(emojiId, 0)), tdlib.okHandler()); + stickerSmallView.onSetEmojiStatus(v, sticker, emojiId, 0); + stickerSmallView.closePreviewIfNeeded(); + } else if (viewId == R.id.btn_setEmojiStatusTimed) { + if (context != null) { + context.showOptions(null, new int[] { + R.id.btn_setEmojiStatusTimed1Hour, + R.id.btn_setEmojiStatusTimed2Hours, + R.id.btn_setEmojiStatusTimed8Hours, + R.id.btn_setEmojiStatusTimed2Days, + R.id.btn_setEmojiStatusTimedCustom, + }, new String[] { + Lang.getString(R.string.SetEmojiAsStatusTimed1Hour), + Lang.getString(R.string.SetEmojiAsStatusTimed2Hours), + Lang.getString(R.string.SetEmojiAsStatusTimed8Hours), + Lang.getString(R.string.SetEmojiAsStatusTimed2Days), + Lang.getString(R.string.SetEmojiAsStatusTimedCustom) + }, new int[] { + ViewController.OPTION_COLOR_NORMAL, + ViewController.OPTION_COLOR_NORMAL, + ViewController.OPTION_COLOR_NORMAL, + ViewController.OPTION_COLOR_NORMAL, + ViewController.OPTION_COLOR_NORMAL, + }, new int[] { + R.drawable.baseline_access_time_24, + R.drawable.baseline_access_time_24, + R.drawable.baseline_access_time_24, + R.drawable.baseline_access_time_24, + R.drawable.baseline_date_range_24 + }, (optionItemView, id) -> { + if (id == R.id.btn_setEmojiStatusTimedCustom) { + int titleRes, todayRes, tomorrowRes, futureRes; + titleRes = R.string.SetEmojiAsStatus; + todayRes = R.string.SetTodayAt; + tomorrowRes = R.string.SetTomorrowAt; + futureRes = R.string.SetDateAt; + + context.showDateTimePicker(tdlib, Lang.getString(titleRes), todayRes, tomorrowRes, futureRes, millis -> { + long expirationDate = millis / 1000L; + stickerSmallView.onSetEmojiStatus(v, sticker, emojiId, expirationDate); + tdlib.client().send(new TdApi.SetEmojiStatus(new TdApi.EmojiStatus(emojiId, (int) expirationDate)), tdlib.okHandler()); + stickerSmallView.closePreviewIfNeeded(); + }, null); + return true; + } + + final int duration; + if (id == R.id.btn_setEmojiStatusTimed1Hour) { + duration = 60 * 60; + } else if (id == R.id.btn_setEmojiStatusTimed2Hours) { + duration = 2 * 60 * 60; + } else if (id == R.id.btn_setEmojiStatusTimed8Hours) { + duration = 8 * 60 * 60; + } else if (id == R.id.btn_setEmojiStatusTimed2Days) { + duration = 2 * 24 * 60 * 60; + } else { + duration = 0; + } + long expirationDate = System.currentTimeMillis() / 1000L + duration; + stickerSmallView.onSetEmojiStatus(v, sticker, emojiId, expirationDate); + tdlib.client().send(new TdApi.SetEmojiStatus(new TdApi.EmojiStatus(emojiId, (int) expirationDate)), tdlib.okHandler()); + stickerSmallView.closePreviewIfNeeded(); + return true; + }); + } + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/EmojiStatusSelectorEmojiPage.java b/app/src/main/java/org/thunderdog/challegram/ui/EmojiStatusSelectorEmojiPage.java index db1609c384..c1680103be 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/EmojiStatusSelectorEmojiPage.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/EmojiStatusSelectorEmojiPage.java @@ -36,8 +36,6 @@ import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.dialogs.SearchManager; -import org.thunderdog.challegram.component.emoji.AnimatedEmojiDrawable; -import org.thunderdog.challegram.component.emoji.AnimatedEmojiEffect; import org.thunderdog.challegram.component.sticker.StickerSmallView; import org.thunderdog.challegram.component.sticker.TGStickerObj; import org.thunderdog.challegram.core.Lang; @@ -55,12 +53,12 @@ import org.thunderdog.challegram.tool.Keyboard; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; -import org.thunderdog.challegram.util.ScrollJumpCompensator; import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.widget.ClearButton; import org.thunderdog.challegram.widget.EmojiLayout; import org.thunderdog.challegram.widget.PopupLayout; import org.thunderdog.challegram.widget.ViewPager; +import org.thunderdog.challegram.widget.decoration.ItemDecorationFirstViewTop; import me.vkryl.android.AnimatorUtils; import me.vkryl.android.animator.BoolAnimator; @@ -89,6 +87,7 @@ public class EmojiStatusSelectorEmojiPage extends BottomSheetViewController.Bott private EmojiLayout emojiCustomListLayout; private EmojiStatusListController emojiCustomListController; private CustomRecyclerView customRecyclerView; + private ItemDecorationFirstViewTop emojiStatusPickerTopDecoration; private ForegroundSearchByEmojiView foregroundEmojiLayout; private HeaderButtons headerButtons; @@ -121,7 +120,7 @@ public boolean onTouchEvent (MotionEvent e) { emojiCustomListController = new EmojiStatusListController(context, tdlib) { @Override - public void onSetEmojiStatusFromPreview (StickerSmallView view, View clickView, TGStickerObj sticker, long emojiId, int duration) { + public void onSetEmojiStatusFromPreview (StickerSmallView view, View clickView, TGStickerObj sticker, long emojiId, long expirationDate) { context.replaceReactionPreviewCords(parent.animationDelegate.getDestX(), parent.animationDelegate.getDestY()); parent.hidePopupWindow(true); scheduleClickAnimation(sticker.getCustomEmojiId()); @@ -136,13 +135,14 @@ public void onSetEmojiStatusFromPreview (StickerSmallView view, View clickView, emojiCustomListController.getValue(); emojiCustomListController.setOnStickersLoadListener(parent::launchOpenAnimation); + emojiStatusPickerTopDecoration = ItemDecorationFirstViewTop.attach(customRecyclerView, parent::getContentOffset); customRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { @Override public void getItemOffsets (@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { final int position = parent.getChildAdapterPosition(view); final int itemCount = parent.getAdapter().getItemCount(); if (position == itemCount - 1) { - int bottom = getKeyboardState() ? Keyboard.getSize(Keyboard.getSize()): 0; + int bottom = getKeyboardState() ? Keyboard.getSize(Keyboard.getSize()) : 0; outRect.set(0, 0, 0, bottom); } } @@ -166,9 +166,7 @@ public boolean onTouchEvent (MotionEvent event) { return super.onTouchEvent(event); } }; - foregroundEmojiLayout.setOnClickListener(v -> { - closeEmojiSelectMode(); - }); + foregroundEmojiLayout.setOnClickListener(v -> closeEmojiSelectMode()); parent.foregroundView.addView(foregroundEmojiLayout); UI.post(parent::launchOpenAnimation, 150); @@ -177,8 +175,8 @@ public boolean onTouchEvent (MotionEvent event) { private int destX, destY; @Override - public boolean onSetEmojiStatus (@Nullable View view, TGStickerObj sticker, int duration) { - tdlib.client().send(new TdApi.SetEmojiStatus(new TdApi.EmojiStatus(sticker.getCustomEmojiId()), duration), tdlib.okHandler()); + public boolean onSetEmojiStatus (@Nullable View view, TGStickerObj sticker, TdApi.EmojiStatus emojiStatus) { + tdlib.client().send(new TdApi.SetEmojiStatus(emojiStatus), tdlib.okHandler()); parent.hidePopupWindow(true); if (view == null) return true; @@ -191,7 +189,7 @@ public boolean onSetEmojiStatus (@Nullable View view, TGStickerObj sticker, int context().reactionsOverlayManager().addOverlay( new ReactionsOverlayView.ReactionInfo(context().reactionsOverlayManager()) .setSticker(sticker, false) - .setRepaintingColors(Theme.getColor(ColorId.iconActive), Theme.getColor(ColorId.white)) + .setRepaintingColorIds(ColorId.iconActive, ColorId.white) .setAnimationEndListener(this::onSetStatusAnimationFinish) .setAnimatedPosition( new Point(startX, startY), @@ -215,7 +213,7 @@ public void onSetStatusAnimationFinish () { context().reactionsOverlayManager().addOverlay( new ReactionsOverlayView.ReactionInfo(context().reactionsOverlayManager()) .setSticker(scheduledClickSticker, true) - .setRepaintingColors(Theme.getColor(ColorId.iconActive), Theme.getColor(ColorId.white)) + .setRepaintingColorIds(ColorId.iconActive, ColorId.white) .setEmojiStatusEffect(scheduledClickEffectSticker) .setUseDefaultSprayAnimation(true) .setPosition(new Point(destX, destY), Screen.dp(90)) @@ -318,10 +316,7 @@ public void onMenuItemPressed (int id, View view) { @Override public boolean needTopDecorationOffsets (RecyclerView parent) { - if (isScrollOffsetDisabled() && this.parent.getLickViewFactor() == 1f) { - return false; - } - return super.needTopDecorationOffsets(parent); + return false; } @Override @@ -339,18 +334,6 @@ public boolean onKeyboardStateChanged (boolean visible) { - - - private void smoothScrollBy (int y) { - if (y == 0) { - customRecyclerView.stopScroll(); - } - customRecyclerView.smoothScrollBy(0, y); - } - - - - /* * */ private String emojiSearchRequest; @@ -359,7 +342,7 @@ private void smoothScrollBy (int y) { public void onEnterEmoji (String emoji) { closeEmojiSelectMode(); setSearchInput(emojiSearchRequest = emoji); - getSearchHeaderView(headerView).setEnabled(false); + getSearchHeaderView(headerView).editView().setEnabled(false); } private boolean inEmojiSelectMode; @@ -383,8 +366,8 @@ private void closeEmojiSelectMode () { protected void onEnterSearchMode () { super.onEnterSearchMode(); searchModeVisibility.setValue(true, true); - getSearchHeaderView(headerView).setEnabled(true); - scheduleScrollOffsetDisable(); + getSearchHeaderView(headerView).editView().setEnabled(true); + emojiStatusPickerTopDecoration.scheduleDisableDecorationOffset(); } @Override @@ -392,7 +375,7 @@ protected void onAfterLeaveSearchMode () { super.onAfterLeaveSearchMode(); if (!inEmojiSelectMode) { emojiCustomListController.search(currentSearchInput = null, emojiSearchRequest = null); - UI.post(() -> setScrollOffsetDisabled(false), 250); + UI.post(emojiStatusPickerTopDecoration::enableDecorationOffset, 250); searchModeVisibility.setValue(false, true); } } @@ -402,8 +385,8 @@ protected void onSearchInputChanged (String input) { super.onSearchInputChanged(currentSearchInput = input); if (StringUtils.isEmpty(input)) { searchModeVisibility.setValue(true, true); - getSearchHeaderView(headerView).setEnabled(true); - Keyboard.show(getSearchHeaderView(headerView)); + getSearchHeaderView(headerView).editView().setEnabled(true); + Keyboard.show(getSearchHeaderView(headerView).editView()); if (!StringUtils.isEmpty(emojiSearchRequest)) { emojiSearchRequest = null; } @@ -414,46 +397,6 @@ protected void onSearchInputChanged (String input) { - - - /* Scroll offset lock */ - - private boolean isScrollOffsetDisabled; - private boolean isScrollOffsetDisableScheduled; - - public void scheduleScrollOffsetDisable () { - isScrollOffsetDisableScheduled = true; - final int top = parent.getTopEdge(); - if (top > 0) { - runOnUiThread(() -> smoothScrollBy(top), 50); - } else { - setScrollOffsetDisabled(true); - } - } - - public boolean isScrollOffsetDisableScheduled () { - return isScrollOffsetDisableScheduled; - } - - public boolean isScrollOffsetDisabled () { - return isScrollOffsetDisabled; - } - - private void setScrollOffsetDisabled (boolean offsetDisabled) { - if (isScrollOffsetDisabled == offsetDisabled) return; - isScrollOffsetDisabled = offsetDisabled; - isScrollOffsetDisableScheduled &= offsetDisabled; - - LinearLayoutManager manager = (LinearLayoutManager) customRecyclerView.getLayoutManager(); - int firstVisiblePosition = manager != null ? manager.findFirstVisibleItemPosition(): -1; - customRecyclerView.invalidateItemDecorations(); - if (firstVisiblePosition == 0) { - ScrollJumpCompensator.compensate(customRecyclerView, parent.getContentOffset() * (offsetDisabled ? -1: 1)); - } - } - - - /* Animations */ @Override @@ -484,7 +427,7 @@ public int getId () { @Override public CharSequence getName () { - return Lang.getString(inEmojiSelectMode ? R.string.FilterByEmoji: R.string.SelectEmojiStatus); + return Lang.getString(inEmojiSelectMode ? R.string.FilterByEmoji : R.string.SelectEmojiStatus); } @Override @@ -595,10 +538,10 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { private void setFactors (float clearVisibility, float searchVisibility, float emojiVisibility, float keyboardVisibility, boolean updateViewVisibility) { if (updateViewVisibility) { - clearButton.setVisibility(clearVisibility > 0f ? VISIBLE: GONE); - searchButton.setVisibility(searchVisibility > 0f ? VISIBLE: GONE); - emojiButton.setVisibility(emojiVisibility > 0f ? VISIBLE: GONE); - keyboardButton.setVisibility(keyboardVisibility > 0f ? VISIBLE: GONE); + clearButton.setVisibility(clearVisibility > 0f ? VISIBLE : GONE); + searchButton.setVisibility(searchVisibility > 0f ? VISIBLE : GONE); + emojiButton.setVisibility(emojiVisibility > 0f ? VISIBLE : GONE); + keyboardButton.setVisibility(keyboardVisibility > 0f ? VISIBLE : GONE); } clearButton.setAlpha(clearVisibility); searchButton.setAlpha(searchVisibility); @@ -629,7 +572,7 @@ public ForegroundSearchByEmojiView (@NonNull Context context, EmojiStatusSelecto super(context); foregroundEmojiLayout = new EmojiLayout(context); - foregroundEmojiLayout.initWithMediasEnabled(controller, false, controller, controller, false); + foregroundEmojiLayout.initWithMediasEnabled(controller, false, false, controller, controller, false, true); foregroundEmojiLayout.setCircleVisible(false, false); foregroundEmojiLayout.setBackgroundColor(Theme.fillingColor()); @@ -782,30 +725,9 @@ protected boolean canHideByScroll () { return false; } - @Override - protected void setDefaultListenersAndDecorators (BottomSheetBaseControllerPage controller) { - controller.getRecyclerView().addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - if (newState == RecyclerView.SCROLL_STATE_IDLE) { - if (fragment.isScrollOffsetDisableScheduled()) { - if (getLickViewFactor() == 1f) { - fragment.setScrollOffsetDisabled(true); - } else { - fragment.onScrollToTopRequested(); - } - } - } - } - }); - super.setDefaultListenersAndDecorators(controller); - } - @Override public boolean needsTempUpdates () { return true; } } - } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/GameController.java b/app/src/main/java/org/thunderdog/challegram/ui/GameController.java index f5881c774e..ee2211d1cc 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/GameController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/GameController.java @@ -102,7 +102,7 @@ public void onActivityResume () { private void checkPlaying () { if (getArgumentsStrict().ownerController != null) { - getArgumentsStrict().ownerController.setBroadcastAction(isFocused() && !isDestroyed() && context.getActivityState() == UI.STATE_RESUMED ? TdApi.ChatActionStartPlayingGame.CONSTRUCTOR : TdApi.ChatActionCancel.CONSTRUCTOR); + getArgumentsStrict().ownerController.setBroadcastAction(isFocused() && !isDestroyed() && context.getActivityState() == UI.State.RESUMED ? TdApi.ChatActionStartPlayingGame.CONSTRUCTOR : TdApi.ChatActionCancel.CONSTRUCTOR); } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/InstantViewController.java b/app/src/main/java/org/thunderdog/challegram/ui/InstantViewController.java index fc50162c6c..844a7626f5 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/InstantViewController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/InstantViewController.java @@ -29,7 +29,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; @@ -75,7 +74,7 @@ import me.vkryl.core.StringUtils; import me.vkryl.td.Td; -public class InstantViewController extends ViewController implements Menu, Client.ResultHandler, TGLegacyManager.EmojiLoadListener, Text.ClickCallback, View.OnClickListener, View.OnLongClickListener, TGPlayerController.PlayListBuilder { +public class InstantViewController extends ViewController implements Menu, TGLegacyManager.EmojiLoadListener, Text.ClickCallback, View.OnClickListener, View.OnLongClickListener, TGPlayerController.PlayListBuilder { public static class Args { public final TdApi.WebPage webPage; public TdApi.WebPageInstantView instantView; @@ -490,25 +489,11 @@ private void buildCells (ArrayList blocks, boolean isReplace) { // recyclerView.setItemAnimator(new CustomItemAnimator(Anim.DECELERATE_INTERPOLATOR, 180l)); if (!isReplace) { - tdlib.client().send(new TdApi.GetWebPageInstantView(getUrl(), true), this); - } - } - - public String getUrl () { - return getArgumentsStrict().webPage.url; - } - - public String getDisplayUrl () { - return getArgumentsStrict().webPage.displayUrl; - } - - @Override - public void onResult (TdApi.Object object) { - switch (object.getConstructor()) { - case TdApi.WebPageInstantView.CONSTRUCTOR: { - final TdApi.WebPageInstantView instantView = (TdApi.WebPageInstantView) object; - tdlib.ui().post(() -> { - if (!isDestroyed()) { + tdlib.send(new TdApi.GetWebPageInstantView(getUrl(), true), (webPageInstantView, error) -> { + if (error != null) { + UI.showError(error); + } else { + runOnUiThreadOptional(() -> { if (!TD.hasInstantView(instantView.version)) { UI.showToast(R.string.InstantViewUnsupported, Toast.LENGTH_SHORT); UI.openUrl(getUrl()); @@ -522,27 +507,27 @@ public void onResult (TdApi.Object object) { getArgumentsStrict().instantView = instantView; buildCells(pageBlocks, true); } - } - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.GetWebPageInstantView.class, TdApi.WebPageInstantView.class); - break; - } + }); + } + }); } } + public String getUrl () { + return getArgumentsStrict().webPage.url; + } + + public String getDisplayUrl () { + return getArgumentsStrict().webPage.displayUrl; + } + @Nullable @Override public TGPlayerController.PlayList buildPlayList (TdApi.Message fromMessage) { ArrayList out = new ArrayList<>(); int foundIndex = -1; int desiredType; + //noinspection SwitchIntDef switch (fromMessage.content.getConstructor()) { case TdApi.MessageAudio.CONSTRUCTOR: desiredType = InlineResult.TYPE_AUDIO; diff --git a/app/src/main/java/org/thunderdog/challegram/ui/IntroController.java b/app/src/main/java/org/thunderdog/challegram/ui/IntroController.java index 9f8d380eb9..eda4411061 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/IntroController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/IntroController.java @@ -540,7 +540,7 @@ public void clearPopup () { */ public void setPopup (@Nullable PopupLayout popupLayout, int windowType) { if (this.popupLayout != null && !this.popupLayout.isWindowHidden()) { - this.popupLayout.hideWindow(UI.getContext(this.popupLayout.getContext()).getActivityState() == UI.STATE_RESUMED); + this.popupLayout.hideWindow(UI.getContext(this.popupLayout.getContext()).getActivityState() == UI.State.RESUMED); } this.popupLayout = popupLayout; if (popupLayout != null) { diff --git a/app/src/main/java/org/thunderdog/challegram/ui/InviteLinkController.java b/app/src/main/java/org/thunderdog/challegram/ui/InviteLinkController.java index 878df94264..fe53d0c0c7 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/InviteLinkController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/InviteLinkController.java @@ -24,12 +24,11 @@ import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.UiThread; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.base.SettingView; import org.thunderdog.challegram.core.Lang; @@ -51,7 +50,7 @@ import me.vkryl.android.widget.FrameLayoutFix; -public class InviteLinkController extends ViewController implements View.OnClickListener, Client.ResultHandler, Unlockable { +public class InviteLinkController extends ViewController implements View.OnClickListener, Unlockable { private static final int FLAG_GETTING_LINK = 0x01; private long chatId; @@ -104,7 +103,9 @@ protected View onCreateView (Context context) { contentView.setAdapter(adapter = new LinkAdapter(context, this)); if (inviteLink == null) { - tdlib.getPrimaryChatInviteLink(chatId, this); + tdlib.getPrimaryChatInviteLink(chatId, (inviteLink, error) -> runOnUiThread(() -> + processInviteLink(inviteLink, error) + )); } FrameLayoutFix wrapper = new FrameLayoutFix(context); @@ -114,6 +115,25 @@ protected View onCreateView (Context context) { return wrapper; } + @UiThread + private void processInviteLink (TdApi.ChatInviteLink newInviteLink, @Nullable TdApi.Error error) { + if (error != null) { + UI.showError(error); + if (!isDestroyed()) { + UI.unlock(this); + } + return; + } + if (callback != null) { + callback.onInviteLinkChanged(newInviteLink); + } + if (!isDestroyed()) { + inviteLink = newInviteLink; + adapter.notifyItemChanged(0); + flags &= ~FLAG_GETTING_LINK; + } + } + @Override protected int getBackButton () { return BackHeaderButton.TYPE_BACK; @@ -122,7 +142,9 @@ protected int getBackButton () { private void exportLink () { if ((flags & FLAG_GETTING_LINK) == 0) { flags |= FLAG_GETTING_LINK; - tdlib.client().send(new TdApi.ReplacePrimaryChatInviteLink(chatId), this); + tdlib.send(new TdApi.ReplacePrimaryChatInviteLink(chatId), (newInviteLink, error) -> runOnUiThread(() -> + processInviteLink(newInviteLink, error) + )); } } @@ -131,35 +153,6 @@ public void unlock () { flags &= ~FLAG_GETTING_LINK; } - @Override - public void onResult (TdApi.Object object) { - switch (object.getConstructor()) { - case TdApi.ChatInviteLink.CONSTRUCTOR: { - final TdApi.ChatInviteLink newInviteLink = (TdApi.ChatInviteLink) object; - UI.post(() -> { - if (callback != null) { - callback.onInviteLinkChanged(newInviteLink); - } - if (!isDestroyed()) { - inviteLink = newInviteLink; - adapter.notifyItemChanged(0); - flags &= ~FLAG_GETTING_LINK; - } - }); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - UI.unlock(this); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.ReplacePrimaryChatInviteLink.class, TdApi.ChatInviteLink.class); - break; - } - } - } - @Override public void onClick (View v) { final int viewId = v.getId(); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/LanguageController.java b/app/src/main/java/org/thunderdog/challegram/ui/LanguageController.java index be8ce58c42..5e3d0b768d 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/LanguageController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/LanguageController.java @@ -416,8 +416,9 @@ public void onClick (View v) { String key = string.string.key; SpannableStringBuilder b = new SpannableStringBuilder(key); - - b.setSpan(new CustomTypefaceSpan(Fonts.getRobotoItalic(), ColorId.textNeutral).setEntityType(new TdApi.TextEntityTypeItalic()), 0, key.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + CustomTypefaceSpan italicSpan = new CustomTypefaceSpan(Fonts.getRobotoItalic(), ColorId.textNeutral); + italicSpan.setTextEntityType(new TdApi.TextEntityTypeItalic()); + b.setSpan(italicSpan, 0, key.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); showOptions(Spannable.Factory.getInstance().newSpannable(b), ids.get(), strings.get(), null, icons.get(), (itemView, id) -> { if (id == R.id.btn_string) { Intents.openLink(TD.getLanguageKeyLink(string.string.key)); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ListItem.java b/app/src/main/java/org/thunderdog/challegram/ui/ListItem.java index 821441a173..dabd1b1aa0 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/ListItem.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/ListItem.java @@ -23,12 +23,14 @@ import androidx.annotation.StringRes; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.util.DrawModifier; import me.vkryl.core.ArrayUtils; -import me.vkryl.core.StringUtils; import me.vkryl.core.BitwiseUtils; +import me.vkryl.core.StringUtils; public class ListItem { public static final int TYPE_CUSTOM = -1; @@ -170,6 +172,7 @@ public class ListItem { private final int checkId; private int flags; private long longId; + private String highlight; private @Nullable String[] sliderValues; private int sliderValue; @@ -177,7 +180,9 @@ public class ListItem { private @Nullable DrawModifier drawModifier; private String stringKey, stringValue; - private int textColorId, textPaddingLeft; + private @PorterDuffColorId int textColorId; + private TdlibAccentColor accentColor; + private int textPaddingLeft; private int intValue; private long longValue; @@ -205,6 +210,10 @@ public ListItem (int viewType, int id, int iconResource, int stringResource, boo this(viewType, id, iconResource, stringResource, null, id, isSelected); } + public ListItem (int viewType, int id, int iconResource, CharSequence string) { + this(viewType, id, iconResource, 0, string, id, false); + } + public ListItem (int viewType, int id, int iconResource, CharSequence string, boolean isSelected) { this(viewType, id, iconResource, 0, string, id, isSelected); } @@ -249,11 +258,20 @@ public int getTextColorId (@ColorId int defColorId) { return TGTheme.getColor(getTextColorId(defColorId)); }*/ - public ListItem setTextColorId (@ColorId int colorId) { + public ListItem setTextColorId (@PorterDuffColorId int colorId) { this.textColorId = colorId; return this; } + public TdlibAccentColor getAccentColor () { + return accentColor; + } + + public ListItem setAccentColor (TdlibAccentColor accentColor) { + this.accentColor = accentColor; + return this; + } + public ListItem setIntValue (int value) { this.intValue = value; return this; @@ -413,6 +431,11 @@ public ListItem setSelected (boolean isSelected) { return this; } + public ListItem setHighlightValue (String highlight) { + this.highlight = highlight; + return this; + } + public int decrementSelectionIndex () { if ((flags & FLAG_USE_SELECTION_INDEX) != 0) { intValue--; @@ -511,6 +534,10 @@ public int getStringResource () { return stringResource; } + public String getHighlightValue () { + return highlight; + } + private int[] stringResources; public boolean hasStringResources () { diff --git a/app/src/main/java/org/thunderdog/challegram/ui/MainController.java b/app/src/main/java/org/thunderdog/challegram/ui/MainController.java index b272998071..356e768d1f 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/MainController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/MainController.java @@ -20,6 +20,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.BitmapFactory; +import android.graphics.Color; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Build; @@ -35,8 +36,12 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.DrawableRes; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.Px; +import androidx.annotation.StringRes; import androidx.recyclerview.widget.RecyclerView; import org.drinkless.tdlib.TdApi; @@ -49,6 +54,7 @@ import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.emoji.Emoji; import org.thunderdog.challegram.filegen.PhotoGenerationInfo; import org.thunderdog.challegram.filegen.VideoGenerationInfo; import org.thunderdog.challegram.loader.ImageReader; @@ -66,13 +72,20 @@ import org.thunderdog.challegram.navigation.ViewPagerHeaderViewCompact; import org.thunderdog.challegram.navigation.ViewPagerTopView; import org.thunderdog.challegram.support.RippleSupport; +import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.sync.SyncAdapter; import org.thunderdog.challegram.telegram.ChatFilter; +import org.thunderdog.challegram.telegram.ChatFolderStyle; +import org.thunderdog.challegram.telegram.ChatFoldersListener; import org.thunderdog.challegram.telegram.CounterChangeListener; +import org.thunderdog.challegram.telegram.GlobalCountersListener; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibCounter; import org.thunderdog.challegram.telegram.TdlibManager; import org.thunderdog.challegram.telegram.TdlibOptionListener; +import org.thunderdog.challegram.telegram.TdlibSettingsManager; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PropertyId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Fonts; import org.thunderdog.challegram.tool.Screen; @@ -85,28 +98,47 @@ import org.thunderdog.challegram.unsorted.Test; import org.thunderdog.challegram.util.AppUpdater; import org.thunderdog.challegram.util.StringList; +import org.thunderdog.challegram.util.text.Counter; +import org.thunderdog.challegram.util.text.TextColorSet; import org.thunderdog.challegram.widget.BubbleLayout; import org.thunderdog.challegram.widget.NoScrollTextView; import org.thunderdog.challegram.widget.PopupLayout; +import org.thunderdog.challegram.widget.ShadowView; import org.thunderdog.challegram.widget.SnackBar; import org.thunderdog.challegram.widget.ViewPager; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.ViewUtils; +import me.vkryl.android.animator.BoolAnimator; import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.BitwiseUtils; +import me.vkryl.core.ColorUtils; +import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.collection.IntList; +import me.vkryl.core.collection.LongSparseIntArray; import me.vkryl.core.lambda.CancellableRunnable; import me.vkryl.td.ChatId; import me.vkryl.td.ChatPosition; +import me.vkryl.td.Td; import me.vkryl.td.TdConstants; -public class MainController extends ViewPagerController implements Menu, MoreDelegate, OverlayButtonWrap.Callback, TdlibOptionListener, AppUpdater.Listener, CounterChangeListener { +public class MainController extends ViewPagerController implements Menu, MoreDelegate, OverlayButtonWrap.Callback, TdlibOptionListener, AppUpdater.Listener, ChatFoldersListener, GlobalCountersListener, Settings.ChatFolderSettingsListener { + private static final long MAIN_PAGER_ITEM_ID = Long.MIN_VALUE; + private static final long ARCHIVE_PAGER_ITEM_ID = Long.MIN_VALUE + 1; + private static final long INVALID_PAGER_ITEM_ID = Long.MAX_VALUE; + public MainController (Context context, Tdlib tdlib) { super(context, tdlib); } @@ -117,17 +149,28 @@ public int getId () { } private FrameLayoutFix mainWrap; + private FrameLayoutFix pagerWrap; private OverlayButtonWrap composeWrap; + @Override + protected View onCreateView (Context context) { + initPagerSections(); + return super.onCreateView(context); + } + @Override protected void onCreateView (Context context, FrameLayoutFix contentView, ViewPager pager) { if (BuildConfig.DEBUG) { Test.execute(); } + pagerWrap = new FrameLayoutFix(context); + pagerWrap.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + pagerWrap.addView(pager); + mainWrap = new FrameLayoutFix(context); mainWrap.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - mainWrap.addView(pager); + mainWrap.addView(pagerWrap); generateChatSearchView(mainWrap); contentView.addView(mainWrap); @@ -179,41 +222,62 @@ protected void onCreateView (Context context, FrameLayoutFix contentView, ViewPa tdlib.wallpaper().ensureWallpaperAvailability(); tdlib.listeners().addOptionsListener(this); + tdlib.listeners().subscribeToChatFoldersUpdates(this); + tdlib.context().global().addCountersListener(this); + Settings.instance().addChatFolderSettingsListener(this); + if (Settings.instance().chatFoldersEnabled()) { + if (this.chatFolderInfos != tdlib.chatFolders()) { + updatePagerSections(true); + } + tdlib.settings().addChatListPositionListener(chatListPositionListener = new ChatListPositionListener()); + } prepareControllerForPosition(0, this::executeScheduledAnimation); if (headerCell != null) { headerCell.getTopView().setOnSlideOffListener(new ViewPagerTopView.OnSlideOffListener() { + + private long selectedPagerItemId = INVALID_PAGER_ITEM_ID; + @Override public boolean onSlideOffPrepare (View view, MotionEvent event, int index) { - return index == 0 && unlockTabs(); + return hasFolders() || getPagerItemId(index) == MAIN_PAGER_ITEM_ID; } @Override public void onSlideOffStart (View view, MotionEvent event, int index) { - setMenuVisible(view, true); + selectedPagerItemId = getPagerItemId(index); + setMenuVisible(view, selectedPagerItemId, true); + if (composeWrap != null && displayTabsAtBottom()) { + composeWrap.hide(); + } } @Override public void onSlideOffFinish (View view, MotionEvent event, int index, boolean apply) { + float offsetX = getOffsetX(view); + float offsetY = getOffsetX(view); if (apply && selectedSection != -1) { - requestChatsSection(selectedSection); - setSelectedSection(-1, event, view.getMeasuredHeight(),true); + requestChatsSection(selectedSection, selectedPagerItemId); + setSelectedSection(-1, event, offsetX, offsetY, true); } else { - setSelectedSection(-1, event, view.getMeasuredHeight(),false); + setSelectedSection(-1, event, offsetX, offsetY, false); } - setMenuVisible(view, false); + setMenuVisible(view, selectedPagerItemId, false); + selectedPagerItemId = INVALID_PAGER_ITEM_ID; } @Override public void onSlideOffMovement (View view, MotionEvent event, int index) { float x = event.getX(); float y = event.getY(); - x += (menu.getMeasuredWidth() - view.getMeasuredWidth()) / 2; - y -= view.getMeasuredHeight() + mainWrap.getTranslationY(); + float offsetX = getOffsetX(view); + float offsetY = getOffsetY(view); + x -= offsetX; + y -= offsetY + mainWrap.getTranslationY() + pagerWrap.getTranslationY(); int selectedSection = -1; if (x >= 0 && x < menu.getMeasuredWidth()) { - int firstItemEnd = menu.getTop() + menu.getChildAt(0).getBottom(); + int firstItemEnd = menu.getChildAt(0).getBottom(); if (y <= firstItemEnd) { selectedSection = 0; } else if (y < menu.getHeight()) { @@ -226,51 +290,58 @@ public void onSlideOffMovement (View view, MotionEvent event, int index) { } } } - setSelectedSection(selectedSection, event, view.getMeasuredHeight(), false); + setSelectedSection(selectedSection, event, offsetX, offsetY, false); + } + + private float getOffsetX (View view) { + return menu.getX() - getX(view, headerCell.getView()); + } + + private float getOffsetY (View view) { + FrameLayoutFix.LayoutParams lp = (FrameLayoutFix.LayoutParams) menu.getLayoutParams(); + int verticalGravity = Gravity.VERTICAL_GRAVITY_MASK & lp.gravity; + return verticalGravity == Gravity.BOTTOM ? -menu.getHeight() : view.getHeight(); } }); + if (Settings.instance().chatFoldersEnabled()) { + headerCell.getTopView().setCounterAlphaProvider(new ViewPagerTopView.CounterAlphaProvider() { + @Override + public float getTextAlpha (Counter counter, float alphaFactor) { + return MathUtils.fromTo(1f, .5f + .5f * alphaFactor, counter.getMuteFactor()); + } + }); + } } // tdlib.awaitConnection(this::unlockTabs); + checkTabs(); + checkMargins(); + context().appUpdater().addListener(this); if (context().appUpdater().state() == AppUpdater.State.READY_TO_INSTALL) { onAppUpdateAvailable(context().appUpdater().flowType() == AppUpdater.FlowType.TELEGRAM_CHANNEL, true); } } - private boolean unlockTabs () { - return true; - /*if (this.menuSection != 0) - return true; - if (tdlib.account().isDebug()) - return true; - if (needTabsForLevel(testerLevel)) - return true; - Calendar c = Dates.getNowCalendar(); - int month = c.get(Calendar.MONTH); - int day = c.get(Calendar.DAY_OF_MONTH); - if (month == Calendar.APRIL) { - if (day <= 14) - return true; - } - checkTesterLevel(); - return needTabsForLevel(testerLevel);*/ - } - - private int testerLevel = Tdlib.TESTER_LEVEL_NONE; - private long lastCheckTime; - - private boolean needTabsForLevel (int level) { - return level == Tdlib.TESTER_LEVEL_READER || level >= Tdlib.TESTER_LEVEL_ADMIN; + @Override + public CharSequence getName () { + return Lang.getString(R.string.Chats); } - private void checkTesterLevel () { - long now = SystemClock.uptimeMillis(); - if (lastCheckTime != 0 && now - lastCheckTime < TimeUnit.HOURS.toMillis(1)) - return; - lastCheckTime = now; - tdlib.getTesterLevel(level -> this.testerLevel = level); + @Override + public View getCustomHeaderCell () { + if (displayTabsAtBottom()) { + return null; + } + if (headerCell != null) { + View headerCellView = headerCell.getView(); + if (bottomBar != null && headerCellView.getParent() == bottomBar) { + return null; + } + return headerCellView; + } + return null; } // Menu @@ -278,29 +349,31 @@ private void checkTesterLevel () { private int selectedSection = -1; private long downTime; - private void setSelectedSection (int section, MotionEvent event, float yOffset, boolean apply) { + private void setSelectedSection (int section, MotionEvent event, float xOffset, float yOffset, boolean apply) { + float eventX = event.getX() - xOffset; + float eventY = event.getY() - yOffset; if (this.selectedSection != section) { if (this.selectedSection != -1) { View view = menu.getChildAt(this.selectedSection); - view.dispatchTouchEvent(MotionEvent.obtain(this.downTime, event.getEventTime(), apply ? MotionEvent.ACTION_UP : MotionEvent.ACTION_CANCEL, event.getX(), event.getY() - yOffset - view.getTop(), event.getMetaState())); + view.dispatchTouchEvent(MotionEvent.obtain(this.downTime, event.getEventTime(), apply ? MotionEvent.ACTION_UP : MotionEvent.ACTION_CANCEL, eventX, eventY - view.getTop(), event.getMetaState())); } this.selectedSection = section; if (section != -1) { View view = menu.getChildAt(this.selectedSection); - view.dispatchTouchEvent(MotionEvent.obtain(this.downTime = SystemClock.uptimeMillis(), event.getEventTime(), MotionEvent.ACTION_DOWN, event.getX(), event.getY() - yOffset - view.getTop(), event.getMetaState())); + view.dispatchTouchEvent(MotionEvent.obtain(this.downTime = SystemClock.uptimeMillis(), event.getEventTime(), MotionEvent.ACTION_DOWN, eventX, eventY - view.getTop(), event.getMetaState())); } } else if (section != -1) { View view = menu.getChildAt(this.selectedSection); - view.dispatchTouchEvent(MotionEvent.obtain(this.downTime, event.getEventTime(), MotionEvent.ACTION_MOVE, event.getX(), event.getY() - yOffset - view.getTop(), event.getMetaState())); + view.dispatchTouchEvent(MotionEvent.obtain(this.downTime, event.getEventTime(), MotionEvent.ACTION_MOVE, eventX, eventY - view.getTop(), event.getMetaState())); } } - private BubbleLayout menu; - private int menuSection; + private @Nullable BubbleLayout menu; private boolean menuNeedArchive; private View menuAnchor; private int pendingSection = -1; + private long pendingPagerItemId = INVALID_PAGER_ITEM_ID; private ChatsController pendingChatsController; private void cancelPendingSection () { @@ -309,104 +382,162 @@ private void cancelPendingSection () { pendingChatsController = null; } this.pendingSection = -1; + this.pendingPagerItemId = INVALID_PAGER_ITEM_ID; } - private void applySection (int section) { - if (this.pendingSection != section) - return; - this.pendingSection = -1; - ChatsController chatsController = this.pendingChatsController; - this.pendingChatsController = null; - - ((TextView) menu.getChildAt(FILTER_ARCHIVE)).setTextColor(menuNeedArchive ? Theme.getColor(ColorId.textNeutral) : Theme.textDecentColor()); + private int applySection (int section, long pagerItemId, ChatsController chatsController) { + if (pagerItemId == INVALID_PAGER_ITEM_ID || headerCell == null) + return NO_POSITION; - TextView textView = (TextView) menu.getChildAt(menuSection); - textView.setTextColor(Theme.textDecentColor()); - removeThemeListenerByTarget(textView); - addThemeTextDecentColorListener(textView); + setSelectedFilter(pagerItemId, section); - this.menuSection = section; + int pagerItemPosition = getPagerItemPosition(pagerItemId); + if (pagerItemPosition == NO_POSITION) + return NO_POSITION; - textView = (TextView) menu.getChildAt(menuSection); - textView.setTextColor(menuSection != FILTER_NONE || !menuNeedArchive ? Theme.getColor(ColorId.textNeutral) : Theme.textDecentColor()); - removeThemeListenerByTarget(textView); - addThemeTextColorListener(textView, ColorId.textNeutral); - - headerCell.getTopView().setItemAt(POSITION_CHATS, Lang.getString(getMenuSectionName()).toUpperCase()); + ViewPagerTopView.Item item; + if (hasFolders()) { + int chatFolderStyle = tdlib.settings().chatFolderStyle(); + if (pagerItemId == MAIN_PAGER_ITEM_ID) { + item = buildMainSectionItem(pagerItemPosition, chatFolderStyle); + } else if (pagerItemId == ARCHIVE_PAGER_ITEM_ID) { + item = buildArchiveSectionItem(pagerItemPosition, chatFolderStyle); + } else { + TdApi.ChatList chatList = pagerChatLists.get(pagerItemPosition); + if (TD.isChatListFolder(chatList)) { + TdApi.ChatListFolder chatListFolder = (TdApi.ChatListFolder) chatList; + TdApi.ChatFolderInfo chatFolderInfo = tdlib.chatFolderInfo(chatListFolder.chatFolderId); + if (chatFolderInfo != null) { + item = buildSectionItem(pagerItemId, pagerItemPosition, chatList, chatFolderInfo, chatFolderStyle); + } else { + chatsController.destroy(); + return NO_POSITION; + } + } else { + throw new UnsupportedOperationException("chatList = " + chatList); + } + } + } else { + int filter = getSelectedFilter(pagerItemId); + if (filter == FILTER_NONE && !menuNeedArchive) { + item = getDefaultMainItem(); + } else { + String sectionName = getMenuSectionName(pagerItemId, pagerItemPosition, /* hasFolders */ false, ChatFolderStyle.LABEL_ONLY).toUpperCase(); + item = new ViewPagerTopView.Item(sectionName); + } + getDefaultSectionItems().set(0, item); + } + headerCell.getTopView().setItemAt(pagerItemPosition, item); - replaceController(POSITION_CHATS, chatsController); - if (getCurrentPagerItemPosition() == POSITION_CHATS) { + replaceController(pagerItemId, chatsController); + if (getCurrentPagerItemPosition() == pagerItemPosition) { showComposeWrap(null); } + return pagerItemPosition; } private void setNeedArchive (boolean needArchive) { if (this.menuNeedArchive != needArchive) { this.menuNeedArchive = needArchive; - // ((TextView) menu.getChildAt(FILTER_ARCHIVE)).setCompoundDrawables(needArchive ? Drawables.get(R.drawable.baseline_check_24) : null, null, null, null); } } - private void requestChatsSection (int requestedSection) { - if ((this.menuSection == requestedSection && (requestedSection != FILTER_NONE || !menuNeedArchive)) || (menuNeedArchive && this.menuSection == FILTER_NONE && requestedSection == FILTER_ARCHIVE)) { + private void requestChatsSection (int requestedSection, long pagerItemId) { + if (pagerItemId == INVALID_PAGER_ITEM_ID) + return; + int menuSection = getSelectedFilter(pagerItemId); + if ((menuSection == requestedSection && (requestedSection != FILTER_NONE || (pagerItemId == MAIN_PAGER_ITEM_ID && !menuNeedArchive))) || + (pagerItemId == MAIN_PAGER_ITEM_ID && menuNeedArchive && menuSection == FILTER_NONE && requestedSection == FILTER_ARCHIVE)) { cancelPendingSection(); - setCurrentPagerPosition(POSITION_CHATS, true); + int pagerItemPosition = getPagerItemPosition(pagerItemId); + if (pagerItemPosition != NO_POSITION) { + setCurrentPagerPosition(pagerItemPosition, true); + } return; } - if (this.pendingSection == requestedSection) { + if (this.pendingSection == requestedSection && this.pendingPagerItemId == pagerItemId) { return; // Still waiting to switch, do nothing } cancelPendingSection(); + int position = getPagerItemPosition(pagerItemId); + if (position == NO_POSITION) + return; int section; if (requestedSection == FILTER_NONE) { section = FILTER_NONE; - setNeedArchive(false); + if (pagerItemId == MAIN_PAGER_ITEM_ID) { + setNeedArchive(false); + } } else if (requestedSection == FILTER_ARCHIVE) { - if (this.menuSection != FILTER_NONE) { - section = FILTER_NONE; - setNeedArchive(true); - } else { - setNeedArchive(!menuNeedArchive); - section = this.menuSection; + section = FILTER_NONE; + if (pagerItemId == MAIN_PAGER_ITEM_ID) { + setNeedArchive(menuSection != FILTER_NONE || !menuNeedArchive); } } else { section = requestedSection; } - - ChatsController c = newChatsController(section, menuNeedArchive); + TdApi.ChatList chatList; + if (pagerItemId == MAIN_PAGER_ITEM_ID) { + chatList = menuNeedArchive ? ChatPosition.CHAT_LIST_ARCHIVE : ChatPosition.CHAT_LIST_MAIN; + } else { + chatList = pagerChatLists.get(position); + } + ChatsController c = newChatsController(chatList, section); this.pendingChatsController = c; this.pendingSection = section; + this.pendingPagerItemId = pagerItemId; c.postOnAnimationExecute(() -> { - if (this.pendingSection == section && this.pendingChatsController == c) { - applySection(section); - setCurrentPagerPosition(POSITION_CHATS, true); + if (this.pendingSection == section && this.pendingChatsController == c && this.pendingPagerItemId == pagerItemId) { + this.pendingSection = -1; + this.pendingPagerItemId = INVALID_PAGER_ITEM_ID; + this.pendingChatsController = null; + int pagerItemPosition = applySection(section, pagerItemId, c); + if (pagerItemPosition != NO_POSITION) { + setCurrentPagerPosition(pagerItemPosition, true); + } } }); - modifyNewPagerItemController(c, POSITION_CHATS); + modifyNewPagerItemController(c); } private void layoutMenu (@NonNull View view) { - if (menu == null) + if (menu == null || headerCell == null) return; menuAnchor = view; - int left = view.getLeft(); + int menuWidth = menu.getMeasuredWidth(); + if (menuWidth == 0) + return; + float x = getX(view, headerCell.getView()); + boolean displayTabsAtBottom = displayTabsAtBottom(); + float translationX = x - menuWidth / 2 + view.getMeasuredWidth() / 2; + int dx = displayTabsAtBottom ? 0 : Screen.dp(14f); + menu.setTranslationX(MathUtils.clamp(translationX, -dx, Screen.currentWidth() - menuWidth + dx)); + if (displayTabsAtBottom) { + int cornerCenterX = menuWidth / 2 + Math.round(translationX - menu.getTranslationX()); + menu.setCornerCenterX(MathUtils.clamp(cornerCenterX, Screen.dp(18f), menuWidth - Screen.dp(18f))); + } + } + + private static float getX (View view, View parent) { + float x = view.getX(); View currentView = view; do { currentView = (View) currentView.getParent(); if (currentView == null) break; - left += currentView.getLeft(); - } while (currentView != headerCell); - menu.setTranslationX(Math.max(-Screen.dp(14f), left - menu.getMeasuredWidth() / 2 + view.getMeasuredWidth() / 2)); + x += currentView.getX(); + } while (currentView != parent); + return x; } - private void setMenuVisible (View anchorView, boolean visible) { + private void setMenuVisible (View anchorView, long pagerItemId, boolean visible) { if (!visible && menu == null) return; View parent = null; if (menu == null) { - menu = new BubbleLayout(context, this, true) { + boolean top = !displayTabsAtBottom(); + menu = new BubbleLayout(context, this, top) { @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); @@ -415,7 +546,9 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { } } }; - menu.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + int gravity = (top ? Gravity.TOP : Gravity.BOTTOM) | Gravity.LEFT; + int marginBottom = top ? 0 : getHeaderHeight(); + menu.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, gravity, 0, 0, 0, marginBottom)); int[] sections = new int[] { R.string.CategoryMain, R.string.CategoryArchive, @@ -438,11 +571,8 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { sectionView.setId(R.id.btn_send); sectionView.setLayoutParams(params); sectionView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15f); - int textColorId = menuSection == index ? ColorId.textNeutral : ColorId.textLight; - sectionView.setTextColor(Theme.getColor(textColorId)); sectionView.setGravity(Gravity.CENTER); sectionView.setPadding(Screen.dp(18f), Screen.dp(13f), Screen.dp(18f), Screen.dp(14f)); - addThemeTextColorListener(sectionView, textColorId); sectionView.setTypeface(Fonts.getRobotoMedium()); sectionView.setTag(index); sectionView.setOnClickListener(v -> { }); @@ -451,16 +581,48 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { menu.addView(sectionView); index++; } - mainWrap.addView(menu, 1); - parent = mainWrap; - tdlib.listeners().addTotalChatCounterListener(this); + pagerWrap.addView(menu, 1); + parent = pagerWrap; } if (visible) { - menu.getChildAt(FILTER_ARCHIVE).setVisibility(tdlib.hasArchivedChats() ? View.VISIBLE : View.GONE); + int selectedFilter = getSelectedFilter(pagerItemId); + + boolean needArchive = pagerItemId == MAIN_PAGER_ITEM_ID && (menuNeedArchive || (!tdlib.settings().isChatListEnabled(ChatPosition.CHAT_LIST_ARCHIVE) && tdlib.hasArchivedChats())); + menu.getChildAt(FILTER_ARCHIVE).setVisibility(needArchive ? View.VISIBLE : View.GONE); - boolean needUnread = tdlib.getMainCounter().chatCount > 0 || menuSection == FILTER_UNREAD; + TdApi.ChatList chatList; + if (pagerItemId == MAIN_PAGER_ITEM_ID) { + chatList = selectedFilter == FILTER_ARCHIVE || menuNeedArchive ? ChatPosition.CHAT_LIST_ARCHIVE : ChatPosition.CHAT_LIST_MAIN; + } else { + int pagerItemPosition = getPagerItemPosition(pagerItemId); + chatList = hasFolders() && pagerItemPosition < pagerChatLists.size() ? pagerChatLists.get(pagerItemPosition) : null;; + } + + boolean hasUnreadChats = chatList != null && tdlib.hasUnreadChats(chatList); + boolean needUnread = selectedFilter == FILTER_UNREAD || hasUnreadChats; menu.getChildAt(FILTER_UNREAD).setVisibility(needUnread ? View.VISIBLE : View.GONE); + + for (int index = 0; index < menu.getChildCount(); index++) { + TextView sectionView = (TextView) menu.getChildAt(index); + if (sectionView.getVisibility() != View.VISIBLE) + continue; + boolean isSelected; + if (index == FILTER_NONE && pagerItemId == MAIN_PAGER_ITEM_ID) { + isSelected = selectedFilter == FILTER_NONE && !menuNeedArchive; + } else if (index == FILTER_ARCHIVE && pagerItemId == MAIN_PAGER_ITEM_ID) { + isSelected = menuNeedArchive; + } else { + isSelected = selectedFilter == index; + } + int textColorId = isSelected ? ColorId.textNeutral : ColorId.textLight; + sectionView.setTextColor(Theme.getColor(textColorId)); + addThemeTextColorListener(sectionView, textColorId); + } layoutMenu(anchorView); + } else { + for (int index = 0; index < menu.getChildCount(); index++) { + removeThemeListenerByTarget(menu.getChildAt(index)); + } } menu.setBubbleVisible(visible, parent); } @@ -548,7 +710,7 @@ protected void onLeaveSelectMode () { @Override protected View getSearchAntagonistView () { - return getViewPager(); + return pagerWrap; } @Override @@ -563,29 +725,51 @@ protected int getChatSearchFlags () { @Override protected TdApi.ChatList getChatMessagesSearchChatList () { - if (menuNeedArchive) { + ChatsController c = findChatsControllerForSearchMessages(); + if (c != null && TD.isChatListArchive(c.chatList())) { return ChatPosition.CHAT_LIST_ARCHIVE; } return null; } @Nullable - public final ChatsController findChatsController () { - return (ChatsController) getCachedControllerForPosition(0); + public final ChatsController findMainChatsController () { + return (ChatsController) getCachedControllerForItemId(MAIN_PAGER_ITEM_ID); + } + + @Nullable + public final ChatsController getCurrentChatsController () { + ViewController current = getCurrentPagerItem(); + return current instanceof ChatsController ? (ChatsController) current : null; + } + + @Nullable + public final ChatsController findChatsControllerForSearchMessages () { + return hasFolders() ? getCurrentChatsController() : findMainChatsController(); } @Override protected boolean filterChatMessageSearchResult (TdApi.Chat chat) { - ChatsController c = findChatsController(); - ChatFilter filter = c != null ? c.getFilter() : null; - return filter != null && filter.canFilterMessages() ? filter.accept(chat) : super.filterChatMessageSearchResult(chat); + ChatsController c = findChatsControllerForSearchMessages(); + if (c != null) { + TdApi.ChatList chatList = c.chatList(); + if (TD.isChatListFolder(chatList) && Config.SEARCH_MESSAGES_ONLY_IN_SELECTED_FOLDER) { + if (ChatPosition.findPosition(chat, chatList) == null) + return false; + } + ChatFilter filter = c.getFilter(); + if (filter != null && filter.canFilterMessages()) { + return filter.accept(chat); + } + } + return super.filterChatMessageSearchResult(chat); } @Override protected int getChatMessagesSearchTitle () { - ChatsController c = findChatsController(); + ChatsController c = findChatsControllerForSearchMessages(); ChatFilter filter = c != null ? c.getFilter() : null; - boolean isArchive = c != null && c.chatList().getConstructor() == TdApi.ChatListArchive.CONSTRUCTOR; + boolean isArchive = c != null && TD.isChatListArchive(c.chatList()); if (filter != null && filter.canFilterMessages()) return filter.getMessagesStringRes(isArchive); if (isArchive) @@ -656,11 +840,23 @@ public boolean onOverlayButtonClick (int id, View view) { return true; } + @Override + public void onPageScrolled (int position, int actualPosition, float actualPositionOffset, int actualPositionOffsetPixels) { + super.onPageScrolled(position, actualPosition, actualPositionOffset, actualPositionOffsetPixels); + if (Config.CHAT_FOLDERS_HIDE_BOTTOM_BAR_ON_SCROLL && displayTabsAtBottom()) { + showBottomBar(); + } + } + @Override public void onPageSelected (int position, int actualPosition) { if (!inSearchMode()) { composeWrap.show(); } + if (hasFolders()) { + composeWrap.replaceMainButton(R.id.btn_float_compose, R.drawable.baseline_create_24); + return; + } switch (position) { case POSITION_CHATS: { composeWrap.replaceMainButton(R.id.btn_float_compose, R.drawable.baseline_create_24); @@ -680,31 +876,80 @@ public void onPageSelected (int position, int actualPosition) { @Override public void onPrepareToShow () { super.onPrepareToShow(); + if (needUpdatePagerSections) { + needUpdatePagerSections = false; + updatePagerSections(true); + if (headerCell != null) { + ViewUtils.runJustBeforeBeingDrawn(headerCell.getView(), () -> + headerCell.getTopView().updateAnchorPosition(false) + ); + } + } if (headerView != null) { headerView.updateLockButton(getMenuId()); } + checkTabs(); + checkMenu(); + checkMargins(); + } + + private void checkMargins () { + checkPagerMargins(); checkHeaderMargins(); + checkComposeWrapPaddings(); + } + + private void checkComposeWrapPaddings () { + if (composeWrap != null) { + int paddingBottom = displayTabsAtBottom() ? getHeaderHeight() : 0; + composeWrap.setPadding(composeWrap.getPaddingLeft(), composeWrap.getPaddingTop(), composeWrap.getPaddingRight(), paddingBottom); + composeWrap.setClipToPadding(paddingBottom == 0); + } + } + + private void checkPagerMargins () { + if (!Config.CHAT_FOLDERS_HIDE_BOTTOM_BAR_ON_SCROLL && displayTabsAtBottom()) { + Views.setBottomMargin(getViewPager(), getHeaderHeight()); + Views.setBottomMargin(pagerWrap, updateSnackBar != null ? Math.round(updateSnackBar.getHeight() * updateSnackBar.getVisibilityFactor()) : 0); // FIXME + } else { + Views.setBottomMargin(getViewPager(), 0); + Views.setBottomMargin(pagerWrap, 0); + } } private void checkHeaderMargins () { - if (headerCell != null) { - RecyclerView recyclerView = ((ViewPagerHeaderViewCompact) headerCell).getRecyclerView(); - FrameLayoutFix.LayoutParams params = (FrameLayoutFix.LayoutParams) recyclerView.getLayoutParams(); - int menuWidth = getMenuButtonsWidth(); - int marginLeft, marginRight; + if (headerCell == null) + return; + ViewPagerHeaderViewCompact headerCellView = ((ViewPagerHeaderViewCompact) headerCell.getView()); + RecyclerView recyclerView = headerCellView.getRecyclerView(); + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) recyclerView.getLayoutParams(); + if (params.rightMargin != 0 || params.leftMargin != 0) { + params.leftMargin = 0; + params.rightMargin = 0; + recyclerView.setLayoutParams(params); + } + int paddingLeft, paddingRight; + if (displayTabsAtBottom()) { + paddingLeft = paddingRight = 0; + recyclerView.setPadding(0, 0, 0, 0); + } else { + recyclerView.setClipToPadding(false); + recyclerView.setPadding(Screen.dp(12), 0, Screen.dp(12), 0); + int menuWidth = Screen.dp(44f); + if (Passcode.instance().isEnabled()) { + menuWidth += Screen.dp(48f); + } if (Lang.rtl()) { - marginLeft = menuWidth; - marginRight = Screen.dp(56f); + paddingLeft = menuWidth; + paddingRight = Screen.dp(44f); } else { - marginLeft = Screen.dp(56f); - marginRight = menuWidth; - } - if (params.rightMargin != marginRight || params.leftMargin != marginLeft) { - params.leftMargin = marginLeft; - params.rightMargin = marginRight; - recyclerView.setLayoutParams(params); + paddingLeft = Screen.dp(44f); + paddingRight = menuWidth; } } + if (headerCellView.getPaddingLeft() != paddingLeft || headerCellView.getPaddingRight() != paddingRight) { + headerCellView.setPadding(paddingLeft, headerCellView.getPaddingTop(), paddingRight, headerCellView.getPaddingBottom()); + } } private SnackBar updateSnackBar; @@ -715,11 +960,24 @@ private void onAppUpdateAvailable (boolean isApk, boolean immediate) { updateSnackBar.setCallback(new SnackBar.Callback() { @Override public void onSnackBarTransition (SnackBar v, float factor) { - composeWrap.setTranslationY(-v.getMeasuredHeight() * factor); + float offsetBySnackBar = v.getMeasuredHeight() * factor; + composeWrap.setTranslationY(-offsetBySnackBar); + if (Config.CHAT_FOLDERS_HIDE_BOTTOM_BAR_ON_SCROLL) { + bottomBarOffsetBySnackBar = -offsetBySnackBar; + invalidateBottomBarOffset(); + } else if (displayTabsAtBottom()) { + Views.setBottomMargin(pagerWrap, Math.round(offsetBySnackBar)); // FIXME + } } @Override public void onDestroySnackBar (SnackBar v) { + if (Config.CHAT_FOLDERS_HIDE_BOTTOM_BAR_ON_SCROLL) { + bottomBarOffsetBySnackBar = 0; + invalidateBottomBarOffset(); + } else { + Views.setBottomMargin(pagerWrap, 0); // FIXME + } if (updateSnackBar == v) { mainWrap.removeView(updateSnackBar); updateSnackBar.removeThemeListeners(MainController.this); @@ -749,7 +1007,22 @@ public void destroy () { super.destroy(); tdlib.listeners().removeOptionListener(this); context().appUpdater().removeListener(this); - tdlib.listeners().removeTotalChatCounterListener(this); + tdlib.context().global().removeCountersListener(this); + Settings.instance().removeChatFolderSettingsListener(this); + tdlib.listeners().unsubscribeFromChatFoldersUpdates(this); + if (chatListUnreadCountListener != null) { + tdlib.listeners().removeTotalChatCounterListener(chatListUnreadCountListener); + chatListUnreadCountListener = null; + } + if (defaultCounterListener != null) { + tdlib.listeners().removeTotalChatCounterListener(defaultCounterListener); + defaultCounterListener = null; + defaultMainItem = null; + } + if (chatListPositionListener != null) { + tdlib.settings().removeChatListPositionListener(chatListPositionListener); + chatListPositionListener = null; + } } private void showSuggestions () { @@ -857,17 +1130,6 @@ private boolean showEmojiUpdateSuggestion () { return true; } - @Override - public void onChatCounterChanged(@NonNull TdApi.ChatList chatList, boolean availabilityChanged, int totalCount, int unreadCount, int unreadUnmutedCount) { - if (chatList instanceof TdApi.ChatListArchive && availabilityChanged) { - tdlib.ui().post(() -> { - if (!isDestroyed() && menu != null) { - menu.getChildAt(FILTER_ARCHIVE).setVisibility(tdlib.hasArchivedChats() ? View.VISIBLE : View.GONE); - } - }); - } - } - @Override public void onFocus () { super.onFocus(); @@ -903,8 +1165,16 @@ public boolean canSlideBackFrom (NavigationController navigationController, floa return false; } if (headerCell != null) { - if (y < HeaderView.getSize(true) && y >= HeaderView.getTopOffset() && x < ((View) headerCell).getMeasuredWidth()) { - return !((ViewPagerHeaderViewCompact) headerCell).canScrollLeft(); + int top, bottom; + if (displayTabsAtBottom()) { + top = Views.getLocationInWindow(headerCell.getView())[1]; + bottom = top + headerCell.getView().getHeight(); + } else { + top = HeaderView.getTopOffset(); + bottom = HeaderView.getSize(true); + } + if (y < bottom && y >= top && x < headerCell.getView().getMeasuredWidth()) { + return !((ViewPagerHeaderViewCompact) headerCell.getView()).canScrollLeft(); } } return true; @@ -912,11 +1182,7 @@ public boolean canSlideBackFrom (NavigationController navigationController, floa @Override protected int getMenuButtonsWidth () { - int width = Screen.dp(56f); - if (Passcode.instance().isEnabled()) { - width += Screen.dp(28f); - } - return width; + return 0; // disable header margins } public boolean showComposeWrap (ViewController controller) { @@ -927,18 +1193,24 @@ public boolean showComposeWrap (ViewController controller) { return false; } - private boolean showHideCompose (int position, boolean show) { - if (getCurrentPagerItemPosition() == position) { + private boolean showHideCompose (ViewController controller, boolean show) { + if (getCurrentPagerItem() == controller) { if (show) { + if (Config.CHAT_FOLDERS_HIDE_BOTTOM_BAR_ON_SCROLL) { + showBottomBar(); + } return showComposeWrap(null); } else { - if (getCurrentPagerItemPosition() == POSITION_CHATS) { - ChatsController c = findChatsController(); + if (getCurrentPagerItemId() == MAIN_PAGER_ITEM_ID) { + ChatsController c = findMainChatsController(); if (c != null && c.isPullingArchive()) { return false; } } composeWrap.hide(); + if (Config.CHAT_FOLDERS_HIDE_BOTTOM_BAR_ON_SCROLL) { + hideBottomBar(); + } return true; } } @@ -951,6 +1223,10 @@ private boolean showHideCompose (int position, boolean show) { private static final int POSITION_CALLS = 1; private static final int POSITION_PEOPLE = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef({FILTER_NONE, FILTER_ARCHIVE, FILTER_PRIVATE, FILTER_GROUPS, FILTER_CHANNELS, FILTER_BOTS, FILTER_UNREAD}) + private @interface Filter { } + private static final int FILTER_NONE = 0; private static final int FILTER_ARCHIVE = 1; private static final int FILTER_PRIVATE = 2; @@ -959,42 +1235,122 @@ private boolean showHideCompose (int position, boolean show) { private static final int FILTER_BOTS = 5; private static final int FILTER_UNREAD = 6; + private @Filter int getSelectedFilter (long pagerItemId) { + return pagerChatListFilters.get(pagerItemId, FILTER_NONE); + } + + private void setSelectedFilter (long pagerItemId, @Filter int filter) { + pagerChatListFilters.put(pagerItemId, filter); + } + @Override protected int getPagerItemCount () { - return 2; + return hasFolders() ? pagerChatLists.size() : 2; + } + + protected long getPagerItemId (int position) { + if (hasFolders()) { + return getPagerItemId(pagerChatLists.get(position)); + } + if (position == POSITION_CHATS) { + return MAIN_PAGER_ITEM_ID; + } + return position; + } + + private long getPagerItemId (TdApi.ChatList chatList) { + switch (chatList.getConstructor()) { + case TdApi.ChatListMain.CONSTRUCTOR: + return MAIN_PAGER_ITEM_ID; + case TdApi.ChatListArchive.CONSTRUCTOR: + return ARCHIVE_PAGER_ITEM_ID; + case TdApi.ChatListFolder.CONSTRUCTOR: + return ((TdApi.ChatListFolder) chatList).chatFolderId; + default: + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean onPagerItemLongClick (int index) { + if (hasFolders()) { + TdApi.ChatList chatList = pagerChatLists.get(index); + String title; + if (TD.isChatListMain(chatList)) { + title = Lang.getString(R.string.CategoryMain); + } else if (TD.isChatListArchive(chatList)) { + title = Lang.getString(R.string.CategoryArchive); + } else if (TD.isChatListFolder(chatList)) { + int chatFolderId = ((TdApi.ChatListFolder) chatList).chatFolderId; + TdApi.ChatFolderInfo chatFolderInfo = tdlib.chatFolderInfo(chatFolderId); + title = chatFolderInfo != null ? chatFolderInfo.title : null; + } else { + title = null; + } + showChatListOptions(title, chatList); + return true; + } + return false; } - private ChatsController newChatsController (int section, boolean needArchive) { + private ChatsController newChatsController (TdApi.ChatList chatList, @Filter int filter) { ChatsController chats = new ChatsController(this.context, tdlib).setParent(this); - ChatFilter filter = null; - switch (section) { + ChatFilter chatFilter; + switch (filter) { + case FILTER_NONE: + chatFilter = null; + break; case FILTER_UNREAD: - filter = ChatFilter.unreadFilter(tdlib); + chatFilter = ChatFilter.unreadFilter(tdlib); break; case FILTER_PRIVATE: - filter = ChatFilter.privateFilter(tdlib); + chatFilter = ChatFilter.privateFilter(tdlib); break; case FILTER_GROUPS: - filter = ChatFilter.groupsFilter(tdlib); + chatFilter = ChatFilter.groupsFilter(tdlib); break; case FILTER_CHANNELS: - filter = ChatFilter.channelsFilter(tdlib); + chatFilter = ChatFilter.channelsFilter(tdlib); break; case FILTER_BOTS: - filter = ChatFilter.botsFilter(tdlib); + chatFilter = ChatFilter.botsFilter(tdlib); break; + case FILTER_ARCHIVE: + default: + throw new IllegalArgumentException(); } - if (filter != null) { - chats.setArguments(new ChatsController.Arguments(filter).setChatList(needArchive ? ChatPosition.CHAT_LIST_ARCHIVE : null).setIsBase(true)); - } else if (needArchive) { - chats.setArguments(new ChatsController.Arguments(ChatPosition.CHAT_LIST_ARCHIVE).setIsBase(true)); + if (chatFilter != null) { + chats.setArguments(new ChatsController.Arguments(chatFilter).setChatList(chatList).setIsBase(true)); + } else if (chatList != null) { + chats.setArguments(new ChatsController.Arguments(chatList).setIsBase(true)); } return chats; } - private int getMenuSectionName () { - if (menuNeedArchive) { - switch (menuSection) { + private String getMenuSectionName (long pagerItemId, int pagerItemPosition, boolean hasFolders, @ChatFolderStyle int chatFolderStyle) { + return Lang.getString(getMenuSectionNameRes(pagerItemId, pagerItemPosition, hasFolders, chatFolderStyle)); + } + + private int getMenuSectionNameRes (long pagerItemId, int pagerItemPosition, boolean hasFolders, @ChatFolderStyle int chatFolderStyle) { + int selectedFilter = getSelectedFilter(pagerItemId); + boolean isMain = pagerItemId == MAIN_PAGER_ITEM_ID; + boolean isArchive = pagerItemId == ARCHIVE_PAGER_ITEM_ID; + if (!isMain && !isArchive) { + throw new UnsupportedOperationException(); + } + //noinspection ConstantConditions + if (isArchive || (isMain && menuNeedArchive && (!hasFolders || pagerItemPosition > 0))) { + return getArchiveSectionName(selectedFilter, chatFolderStyle); + } + if (selectedFilter != FILTER_NONE) { + return getFilterName(selectedFilter); + } + return hasFolders ? R.string.CategoryMain : R.string.Chats; + } + + private int getArchiveSectionName (int selectedFilter, @ChatFolderStyle int chatFolderStyle) { + if (chatFolderStyle == ChatFolderStyle.LABEL_ONLY) { + switch (selectedFilter) { case FILTER_UNREAD: return R.string.CategoryArchiveUnread; case FILTER_PRIVATE: @@ -1005,26 +1361,50 @@ private int getMenuSectionName () { return R.string.CategoryArchiveChannels; case FILTER_BOTS: return R.string.CategoryArchiveBots; + case FILTER_NONE: + case FILTER_ARCHIVE: + return R.string.CategoryArchive; + default: + throw new UnsupportedOperationException("selectedFilter=" + selectedFilter); } - return R.string.CategoryArchive; - } else { - switch (menuSection) { - case FILTER_UNREAD: - return R.string.CategoryUnread; - case FILTER_PRIVATE: - return R.string.CategoryPrivate; - case FILTER_GROUPS: - return R.string.CategoryGroup; - case FILTER_CHANNELS: - return R.string.CategoryChannels; - case FILTER_BOTS: - return R.string.CategoryBots; + } + return selectedFilter == FILTER_NONE ? R.string.CategoryArchive : getFilterName(selectedFilter); + } + + private String getMenuSectionName (long pagerItemId, String folderName, int chatFolderStyle) { + int selectedFilter = getSelectedFilter(pagerItemId); + if (selectedFilter != FILTER_NONE) { + String filterName = Lang.getString(getFilterName(selectedFilter)); + if (chatFolderStyle == ChatFolderStyle.LABEL_ONLY) { + return Lang.getString(R.string.format_folderAndFilter, folderName, filterName); } - return R.string.Chats; + return filterName; } + return folderName; } - private void modifyNewPagerItemController (final ViewController c, final int position) { + private @StringRes int getFilterName (int filter) { + switch (filter) { + case FILTER_UNREAD: + return R.string.CategoryUnread; + case FILTER_PRIVATE: + return R.string.CategoryPrivate; + case FILTER_GROUPS: + return R.string.CategoryGroup; + case FILTER_CHANNELS: + return R.string.CategoryChannels; + case FILTER_BOTS: + return R.string.CategoryBots; + case FILTER_ARCHIVE: + return R.string.CategoryArchive; + case FILTER_NONE: + return 0; + default: + throw new UnsupportedOperationException("filter=" + filter); + } + } + + private void modifyNewPagerItemController (final ViewController c) { if (c instanceof RecyclerViewProvider) { c.getValue(); ((RecyclerViewProvider) c).provideRecyclerView().addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -1032,13 +1412,13 @@ private void modifyNewPagerItemController (final ViewController c, final int private float lastShowY; @Override - public void onScrolled (RecyclerView recyclerView, int dx, int dy) { + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { lastY += dy; if (dy < 0 && lastShowY - lastY >= Screen.getTouchSlop()) { - showHideCompose(position, true); + showHideCompose(c, true); lastShowY = lastY; } else if (lastY - lastShowY > Screen.getTouchSlopBig()) { - showHideCompose(position, false); + showHideCompose(c, false); lastShowY = lastY; } if (Math.abs(lastY - lastShowY) > Screen.getTouchSlopBig()) { @@ -1052,28 +1432,109 @@ public void onScrolled (RecyclerView recyclerView, int dx, int dy) { @Override protected ViewController onCreatePagerItemForPosition (Context context, final int position) { - ViewController c; - switch (position) { - case POSITION_CHATS: { - c = newChatsController(this.menuSection, this.menuNeedArchive); - break; - } - case POSITION_CALLS: - c = new CallListController(this.context, tdlib); - break; - case POSITION_PEOPLE: - c = new PeopleController(this.context, tdlib); - break; - default: - throw new IllegalArgumentException("position == " + position); + final ViewController c; + long pagerItemId = getPagerItemId(position); + if (pagerItemId == MAIN_PAGER_ITEM_ID) { + TdApi.ChatList chatList = this.menuNeedArchive ? ChatPosition.CHAT_LIST_ARCHIVE : ChatPosition.CHAT_LIST_MAIN; + c = newChatsController(chatList, getSelectedFilter(MAIN_PAGER_ITEM_ID)); + } else if (hasFolders()) { + TdApi.ChatList chatList = pagerChatLists.get(position); + c = newChatsController(chatList, getSelectedFilter(pagerItemId)); + } else if (position == POSITION_CALLS) { + c = new CallListController(this.context, tdlib); + } else if (position == POSITION_PEOPLE) { + c = new PeopleController(this.context, tdlib); + } else { + throw new IllegalArgumentException("position == " + position); } - modifyNewPagerItemController(c, position); + modifyNewPagerItemController(c); return c; } + private @Nullable TdApi.ChatFolderInfo[] chatFolderInfos; + private List pagerSections = Collections.emptyList(); + private List pagerChatLists = Collections.emptyList(); + private final LongSparseIntArray pagerChatListFilters = new LongSparseIntArray(); + + private boolean hasFolders () { + return Settings.instance().chatFoldersEnabled() && !pagerChatLists.isEmpty(); + } + @Override protected String[] getPagerSections () { - return new String[] {Lang.getString(getMenuSectionName()).toUpperCase(), Lang.getString(R.string.Calls).toUpperCase()/*, UI.getString(R.string.Contacts).toUpperCase()*/}; + if (hasFolders()) { + throw new UnsupportedOperationException(); + } + return new String[] { + getMenuSectionName(MAIN_PAGER_ITEM_ID, /* pagerItemPosition */ 0, /* hasFolders */ false, ChatFolderStyle.LABEL_ONLY).toUpperCase(), + Lang.getString(R.string.Calls).toUpperCase()/*, UI.getString(R.string.Contacts).toUpperCase()*/ + }; + } + + private List defaultSectionItems; + private ViewPagerTopView.Item defaultMainItem; + private CounterChangeListener defaultCounterListener; + + private ViewPagerTopView.Item getDefaultMainItem () { + if (defaultMainItem == null) { + String mainItem = getMenuSectionName(MAIN_PAGER_ITEM_ID, 0, false, ChatFolderStyle.LABEL_ONLY).toUpperCase(); + UnreadCounterColorSet unreadCounterColorSet = new UnreadCounterColorSet(); + Counter unreadCounter = new Counter.Builder() + .textSize(12f) + .backgroundPadding(4f) + .outlineAffectsBackgroundSize(false) + .colorSet(unreadCounterColorSet) + .callback(unreadCounterCallback(MAIN_PAGER_ITEM_ID)) + .build(); + unreadCounterColorSet.setCounter(unreadCounter); + updateCounter(ChatPosition.CHAT_LIST_MAIN, unreadCounter, tdlib.getCounter(ChatPosition.CHAT_LIST_MAIN), false); + defaultCounterListener = new CounterChangeListener() { + @Override + public void onChatCounterChanged (@NonNull TdApi.ChatList chatList, TdlibCounter counter, boolean availabilityChanged, int totalCount, int unreadCount, int unreadUnmutedCount) { + handleCounterChange(chatList, counter, false); + } + + @Override + public void onMessageCounterChanged (@NonNull TdApi.ChatList chatList, TdlibCounter counter, int unreadCount, int unreadUnmutedCount) { + handleCounterChange(chatList, counter, true); + } + + private void handleCounterChange (@NonNull TdApi.ChatList chatList, TdlibCounter counter, boolean areMessages) { + int badgeFlags = tdlib.settings().getChatFolderBadgeFlags(); + if (areMessages != BitwiseUtils.hasFlag(badgeFlags, Settings.BADGE_FLAG_MESSAGES)) { + return; + } + if (chatList.getConstructor() == TdApi.ChatListMain.CONSTRUCTOR) { + executeOnUiThreadOptional(() -> { + updateCounter(chatList, unreadCounter, counter, isFocused()); + }); + } else if (chatList.getConstructor() == TdApi.ChatListArchive.CONSTRUCTOR && BitwiseUtils.hasFlag(badgeFlags, Settings.BADGE_FLAG_ARCHIVED)) { + executeOnUiThreadOptional(() -> { + updateCounter(ChatPosition.CHAT_LIST_MAIN, unreadCounter, tdlib.getCounter(ChatPosition.CHAT_LIST_MAIN), isFocused()); + }); + } + } + }; + defaultMainItem = new ViewPagerTopView.Item(mainItem, unreadCounter); + tdlib.listeners().addTotalChatCounterListener(defaultCounterListener); + } + return defaultMainItem; + } + + private List getDefaultSectionItems () { + if (defaultSectionItems == null) { + String callsItem = Lang.getString(R.string.Calls).toUpperCase(); + defaultSectionItems = Arrays.asList( + getDefaultMainItem(), + new ViewPagerTopView.Item(callsItem) + ); + } + return defaultSectionItems; + } + + @Override + protected List getPagerSectionItems () { + return hasFolders() ? pagerSections : getDefaultSectionItems(); } // Menu @@ -1312,7 +1773,7 @@ private void shareIntentImplSingle (Tdlib tdlib, final Intent intent) throws Thr // If sendingText still unused, then add it to the beginning of send queue if (!StringUtils.isEmpty(sendingText)) { - out.addAll(0, TD.explodeText(new TdApi.InputMessageText(new TdApi.FormattedText(sendingText, null), false, false), tdlib.maxMessageTextLength())); + out.addAll(0, TD.explodeText(new TdApi.InputMessageText(new TdApi.FormattedText(sendingText, null), null, false), tdlib.maxMessageTextLength())); } } shareContents(tdlib, type, out, false); @@ -1350,7 +1811,7 @@ private void shareIntentImplMultiple (Tdlib tdlib, Intent intent) { } // If sendingText still unused, then add it to the beginning of send queue if (!StringUtils.isEmpty(sendingText)) { - out.addAll(0, TD.explodeText(new TdApi.InputMessageText(new TdApi.FormattedText(sendingText, null), false, false), tdlib.maxMessageTextLength())); + out.addAll(0, TD.explodeText(new TdApi.InputMessageText(new TdApi.FormattedText(sendingText, null), null, false), tdlib.maxMessageTextLength())); } shareContents(tdlib, type, out, true); @@ -1429,7 +1890,7 @@ private static boolean addShareUri (Tdlib tdlib, ArrayList 0) { + options.item(OptionItem.SEPARATOR); + } + options.item(new OptionItem(R.id.btn_chatFolders, Lang.getString(R.string.EditFolders), OPTION_COLOR_NORMAL, R.drawable.baseline_edit_folders_24)); + showOptions(options.build(), (v, id) -> { + if (id == R.id.btn_editFolder) { + tdlib.send(new TdApi.GetChatFolder(chatFolderId), (chatFolder, error) -> runOnUiThreadOptional(() -> { + if (error != null) { + UI.showError(error); + } else { + EditChatFolderController controller = new EditChatFolderController(context, tdlib); + controller.setArguments(new EditChatFolderController.Arguments(chatFolderId, chatFolder)); + navigateTo(controller); + } + })); + } else if (id == R.id.btn_hideFolder) { + tdlib.settings().setChatListEnabled(chatList, false); + if (headerCell != null && !StringUtils.isEmptyOrBlank(title)) { + context() + .tooltipManager() + .builder(headerCell.getTopView()) + .locate((targetView, outRect) -> outRect.left = outRect.right = Screen.dp(56f) - (int) targetView.getX()) + .controller(this) + .show(tdlib, Lang.getString(R.string.HideFolderInfo, title, Lang.getString(R.string.Settings), Lang.getString(R.string.ChatFolders))) + .hideDelayed(3500, TimeUnit.MILLISECONDS); + } + } else if (id == R.id.btn_folderIncludeChats) { + tdlib.send(new TdApi.GetChatFolder(chatFolderId), (chatFolder, error) -> runOnUiThreadOptional(() -> { + if (error != null) { + UI.showError(error); + } else { + SelectChatsController controller = new SelectChatsController(context, tdlib); + controller.setArguments(SelectChatsController.Arguments.includedChats(chatFolderId, chatFolder)); + navigateTo(controller); + } + })); + } else if (id == R.id.btn_changeFolderIcon) { + ChatFolderIconSelector.show(this, icon -> { + tdlib.send(new TdApi.GetChatFolder(chatFolderId), (chatFolder, getError) -> { + if (getError != null) { + UI.showError(getError); + } else { + if (!Td.equalsTo(chatFolder.icon, icon)) { + chatFolder.icon = icon; + tdlib.send(new TdApi.EditChatFolder(chatFolderId, chatFolder), (chatFolderInfo, editError) -> { + if (editError != null) { + UI.showError(editError); + } + }); + } + } + }); + }); + } else if (id == R.id.btn_removeFolder) { + showConfirm(Lang.getString(R.string.RemoveFolderConfirm), Lang.getString(R.string.Remove), R.drawable.baseline_delete_24, OPTION_COLOR_RED, () -> { + tdlib.send(new TdApi.DeleteChatFolder(chatFolderId, null), tdlib.typedOkHandler()); + }); + } else if (id == R.id.btn_chatFolders) { + navigateTo(new SettingsFoldersController(context, tdlib)); + } + return true; + }); + } + + private @Nullable ViewGroup bottomBar; + private @Px float bottomBarOffsetByPlayer; + private @Px float bottomBarOffsetByScroll; + private @Px float bottomBarOffsetBySnackBar; + private final BoolAnimator isBottomBarVisible = new BoolAnimator(0, (id, factor, fraction, callee) -> { + int headerHeight = getHeaderHeight(); + int shadowHeight = Screen.dp(2f); + bottomBarOffsetByScroll = (headerHeight + shadowHeight) * (1f - factor); + invalidateBottomBarOffset(); + }, AnimatorUtils.DECELERATE_INTERPOLATOR, 200l, true); + + private void showBottomBar () { + if (bottomBar != null) { + isBottomBarVisible.setValue(true, isFocused()); + } + } + + private void hideBottomBar () { + if (bottomBar != null) { + isBottomBarVisible.setValue(false, isFocused()); + } + } + + private void invalidateBottomBarOffset () { + if (bottomBar != null) { + bottomBar.setTranslationY(bottomBarOffsetByPlayer + bottomBarOffsetByScroll + bottomBarOffsetBySnackBar); + } + } + + @Override + protected boolean applyPlayerOffset (float factor, float top) { + boolean result = super.applyPlayerOffset(factor, top); + if (result) { + bottomBarOffsetByPlayer = factor < 1f ? -top : 0; + invalidateBottomBarOffset(); + } + return result; + } + + private void checkTabs () { + if (headerCell == null) + return; + ViewPagerHeaderViewCompact headerCellView = (ViewPagerHeaderViewCompact) headerCell.getView(); + boolean hasFolders = hasFolders(); + boolean displayTabsAtBottom = displayTabsAtBottom(); + headerCell.getTopView().setUseDarkBackground(displayTabsAtBottom); + headerCell.getTopView().setDrawSelectionAtTop(displayTabsAtBottom); + headerCell.getTopView().setSlideOffDirection(displayTabsAtBottom ? ViewPagerTopView.SLIDE_OFF_DIRECTION_TOP : ViewPagerTopView.SLIDE_OFF_DIRECTION_BOTTOM); + headerCell.getTopView().setItemPadding(Screen.dp(hasFolders ? ViewPagerTopView.COMPACT_ITEM_PADDING : ViewPagerTopView.DEFAULT_ITEM_PADDING)); + headerCell.getTopView().setItemSpacing(Screen.dp(hasFolders ? ViewPagerTopView.COMPACT_ITEM_SPACING : ViewPagerTopView.DEFAULT_ITEM_SPACING)); + headerCellView.setFadingEdgeLength(displayTabsAtBottom ? 0f : 16f); + + if (displayTabsAtBottom) { + if (bottomBar == null) { + ViewGroup parent = (ViewGroup) headerCellView.getParent(); + if (parent != null) { + if (headerView != null && isFocused() && !inTransformMode()) { + headerView.setTitle(this); + View titleView = headerView.findViewById(getId()); + if (titleView != null) { + titleView.setAlpha(1f); + titleView.setTranslationX(0); + titleView.setTranslationY(0); + titleView.setVisibility(View.VISIBLE); + } + } else { + parent.removeView(headerCellView); + } + } + int shadowHeight = Screen.dp(3f); + ShadowView shadowView = new ShadowView(context); + shadowView.setVerticalShadow(new int[]{0x00000000, 0x40000000}, null, shadowHeight); + shadowView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, shadowHeight)); + Views.setTopMargin(headerCellView, shadowHeight - Screen.dp(1f)); + + ViewSupport.setThemedBackground(headerCellView, ColorId.headerLightBackground, this); + headerCell.getTopView().setSelectionColorId(ColorId.headerLightText, .9f); + headerCell.getTopView().setTextFromToColorId(ColorId.headerLightText, ColorId.headerLightText, PropertyId.SUBTITLE_ALPHA); + + int headerHeight = getHeaderHeight(); + bottomBar = new FrameLayoutFix(context); + bottomBar.addView(shadowView); + bottomBar.addView(headerCellView); + pagerWrap.addView(bottomBar, FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, headerHeight + Views.getTopMargin(headerCellView), Gravity.BOTTOM)); + if (Config.CHAT_FOLDERS_HIDE_BOTTOM_BAR_ON_SCROLL) { + isBottomBarVisible.setValue(true, false); + invalidateBottomBarOffset(); + } + } + headerCellView.setAlpha(1f); + headerCellView.setTranslationX(0f); + headerCellView.setTranslationY(0f); + headerCellView.setVisibility(View.VISIBLE); + headerCellView.setScaleFactor(0f, 0f, 0f, false); + } else { + if (bottomBar != null) { + pagerWrap.removeView(bottomBar); + bottomBar.removeView(headerCellView); + bottomBar = null; + if (Config.CHAT_FOLDERS_HIDE_BOTTOM_BAR_ON_SCROLL) { + isBottomBarVisible.setValue(false, false); + } + headerCell.getTopView().setSelectionColorId(ColorId.headerTabActive); + headerCell.getTopView().setTextFromToColorId(ColorId.headerTabInactiveText, ColorId.headerTabActiveText); + ViewUtils.setBackground(headerCellView, null); + Views.setTopMargin(headerCellView, 0); + if (headerView != null && isFocused() && !inTransformMode()) { + headerView.setTitle(this); + } + } + } + } + + private void checkMenu () { + if (menu != null) { + FrameLayoutFix.LayoutParams layoutParams = (FrameLayoutFix.LayoutParams) menu.getLayoutParams(); + boolean displayTabsAtBottom = displayTabsAtBottom(); + int gravity = (displayTabsAtBottom ? Gravity.BOTTOM : Gravity.TOP) | Gravity.LEFT; + int bottomMargin = displayTabsAtBottom ? getHeaderHeight() : 0; + if (menu.setTop(!displayTabsAtBottom)) { + menu.setDefaultPadding(); + menu.setPadding(menu.getPaddingLeft(), menu.getPaddingTop() + Screen.dp(14f), menu.getPaddingRight(), menu.getPaddingBottom() + Screen.dp(13f)); + } + if (gravity != layoutParams.gravity || bottomMargin != layoutParams.bottomMargin) { + layoutParams.gravity = gravity; + layoutParams.bottomMargin = bottomMargin; + } + } + } + + private boolean displayTabsAtBottom () { + return hasFolders() && !tdlib.settings().displayFoldersAtTop(); + } + + private void initPagerSections () { + if (Settings.instance().chatFoldersEnabled()) { + updatePagerSections(true); + } + } + + private boolean needUpdatePagerSections; + + private void updatePagerSections (boolean force) { + if (force || isAttachedToNavigationController()) { + updatePagerSections(tdlib.chatFolders(), tdlib.mainChatListPosition(), tdlib.settings().archiveChatListPosition()); + } else { + needUpdatePagerSections = true; + } + } + + private void updatePagerSections (TdApi.ChatFolderInfo[] chatFolders, int mainChatListPosition, int archiveChatListPosition) { + boolean oldHasFolders = hasFolders(); + List chatLists; + List sections; + if (chatFolders.length > 0 || tdlib.settings().isChatListEnabled(ChatPosition.CHAT_LIST_ARCHIVE)) { + int chatListCount = chatFolders.length; + if (tdlib.settings().isChatListEnabled(ChatPosition.CHAT_LIST_MAIN)) { + mainChatListPosition = MathUtils.clamp(mainChatListPosition, 0, chatListCount); + chatListCount++; + } else { + mainChatListPosition = NO_POSITION; + } + if (tdlib.settings().isChatListEnabled(ChatPosition.CHAT_LIST_ARCHIVE)) { + archiveChatListPosition = MathUtils.clamp(archiveChatListPosition, 0, chatListCount); + chatListCount++; + if (mainChatListPosition >= archiveChatListPosition) { + mainChatListPosition++; + } + } else { + archiveChatListPosition = NO_POSITION; + } + @ChatFolderStyle int chatFolderStyle = tdlib.settings().chatFolderStyle(); + chatLists = new ArrayList<>(chatListCount); + sections = new ArrayList<>(chatListCount); + int chatFolderIndex = 0; + for (int position = 0; position < chatListCount; position++) { + TdApi.ChatList chatList; + ViewPagerTopView.Item sectionItem; + if (position == mainChatListPosition) { + chatList = ChatPosition.CHAT_LIST_MAIN; + sectionItem = buildMainSectionItem(position, chatFolderStyle); + } else if (position == archiveChatListPosition) { + chatList = ChatPosition.CHAT_LIST_ARCHIVE; + sectionItem = buildArchiveSectionItem(position, chatFolderStyle); + } else { + TdApi.ChatFolderInfo chatFolder = chatFolders[chatFolderIndex++]; + if (!tdlib.settings().isChatFolderEnabled(chatFolder.id)) + continue; + chatList = new TdApi.ChatListFolder(chatFolder.id); + sectionItem = buildSectionItem(getPagerItemId(chatList), position, chatList, chatFolder, chatFolderStyle); + } + chatLists.add(chatList); + sections.add(sectionItem); + } + } else { + sections = Collections.emptyList(); + chatLists = Collections.emptyList(); + } + if (chatLists.size() > 1 || (!chatLists.isEmpty() && !TD.isChatListMain(chatLists.get(0)))) { + this.pagerSections = sections; + this.pagerChatLists = chatLists; + this.chatFolderInfos = chatFolders; + if (chatListUnreadCountListener == null) { + chatListUnreadCountListener = new ChatListUnreadCountListener(); + tdlib.listeners().addTotalChatCounterListener(chatListUnreadCountListener); + } + } else { + this.pagerSections = Collections.emptyList(); + this.pagerChatLists = Collections.emptyList(); + this.chatFolderInfos = null; + if (chatListUnreadCountListener != null) { + tdlib.listeners().removeTotalChatCounterListener(chatListUnreadCountListener); + chatListUnreadCountListener = null; + } + } + notifyPagerItemPositionsChanged(); + ViewPager pager = getViewPager(); + if (pager != null) { + onPageSelected(getCurrentPagerItemPosition(), pager.getCurrentItem()); + } + if (oldHasFolders != hasFolders() && pagerWrap != null) { + checkTabs(); + checkMenu(); + checkMargins(); + } + } + + private ViewPagerTopView.Item buildMainSectionItem (int pagerItemPosition, @ChatFolderStyle int chatFolderStyle) { + CharSequence sectionName = getMenuSectionName(MAIN_PAGER_ITEM_ID, pagerItemPosition, /* hasFolders */ true, chatFolderStyle); + int iconResource = menuNeedArchive ? R.drawable.baseline_archive_24 : R.drawable.baseline_forum_24; + return buildSectionItem(MAIN_PAGER_ITEM_ID, pagerItemPosition, ChatPosition.CHAT_LIST_MAIN, sectionName, iconResource, chatFolderStyle); + } + + private ViewPagerTopView.Item buildArchiveSectionItem (int pagerItemPosition, @ChatFolderStyle int chatFolderStyle) { + CharSequence sectionName = getMenuSectionName(ARCHIVE_PAGER_ITEM_ID, pagerItemPosition, /* hasFolders */ true, chatFolderStyle); + int iconResource = R.drawable.baseline_archive_24; + return buildSectionItem(ARCHIVE_PAGER_ITEM_ID, pagerItemPosition, ChatPosition.CHAT_LIST_ARCHIVE, sectionName, iconResource, chatFolderStyle); + } + + private ViewPagerTopView.Item buildSectionItem (long pagerItemId, int pagerItemPosition, TdApi.ChatList chatList, TdApi.ChatFolderInfo chatFolderInfo, @ChatFolderStyle int chatFolderStyle) { + CharSequence sectionName = Emoji.instance().replaceEmoji(getMenuSectionName(pagerItemId, chatFolderInfo.title, chatFolderStyle)); + int iconResource = TD.findFolderIcon(chatFolderInfo.icon, R.drawable.baseline_folder_24); + return buildSectionItem(pagerItemId, pagerItemPosition, chatList, sectionName, iconResource, chatFolderStyle); + } + + private ViewPagerTopView.Item buildSectionItem (long pagerItemId, int pagerItemPosition, TdApi.ChatList chatList, CharSequence sectionName, @DrawableRes int iconResource, @ChatFolderStyle int chatFolderStyle) { + int selectedFilter = getSelectedFilter(pagerItemId); + Counter unreadCounter = buildUnreadCounter(pagerItemId, selectedFilter); + if (unreadCounter != null) { + TdlibCounter counter = tdlib.getCounter(chatList); + updateCounter(chatList, unreadCounter, counter, /* animated */ false); + } + ViewPagerTopView.Item item; + if (pagerItemId == MAIN_PAGER_ITEM_ID && pagerItemPosition == 0) { + if (selectedFilter != FILTER_NONE && selectedFilter != FILTER_ARCHIVE) { + item = new ViewPagerTopView.Item(sectionName, iconResource, unreadCounter); + } else { + item = new ViewPagerTopView.Item(iconResource, unreadCounter); + } + } else if (chatFolderStyle == ChatFolderStyle.LABEL_AND_ICON || + chatFolderStyle == ChatFolderStyle.ICON_ONLY && selectedFilter != FILTER_NONE) { + item = new ViewPagerTopView.Item(sectionName, iconResource, unreadCounter); + } else if (chatFolderStyle == ChatFolderStyle.ICON_ONLY) { + item = new ViewPagerTopView.Item(iconResource, unreadCounter); + } else { + item = new ViewPagerTopView.Item(sectionName, unreadCounter); + } + item.setMinWidth(Screen.dp(56f - ViewPagerTopView.COMPACT_ITEM_PADDING * 2)); + return item; + } + + private @Nullable Counter buildUnreadCounter (long pagerItemId, int selectedFilter) { + if (selectedFilter != FILTER_NONE || (pagerItemId == MAIN_PAGER_ITEM_ID && menuNeedArchive)) { + return null; + } + UnreadCounterColorSet unreadCounterColorSet = new UnreadCounterColorSet(); + Counter unreadCounter = new Counter.Builder() + .textSize(12f) + .backgroundPadding(4f) + .outlineAffectsBackgroundSize(false) + .colorSet(unreadCounterColorSet) + .callback(unreadCounterCallback(pagerItemId)) + .build(); + unreadCounterColorSet.setCounter(unreadCounter); + return unreadCounter; + } + + private Counter.Callback unreadCounterCallback (long pagerItemId) { + return (counter, sizeChanged) -> { + if (headerCell != null) { + int position = getPagerItemPosition(pagerItemId); + if (position != NO_POSITION) { + ViewPagerTopView topView = headerCell.getTopView(); + if (sizeChanged) { + topView.requestItemLayoutAt(position); + } + topView.invalidate(); + } + } + }; + } + + private void updateCounter (TdApi.ChatList chatList, Counter target, TdlibCounter counter, boolean animated) { + int mutedCount, unmutedCount; + int badgeFlags = tdlib.settings().getChatFolderBadgeFlags(); + boolean countMessages = BitwiseUtils.hasFlag(badgeFlags, Settings.BADGE_FLAG_MESSAGES); + if (countMessages) { + mutedCount = Math.max(0, counter.messageCount); + unmutedCount = Math.max(0, counter.messageUnmutedCount); + } else { + mutedCount = Math.max(0, counter.chatCount); + unmutedCount = Math.max(0, counter.chatUnmutedCount); + } + if (BitwiseUtils.hasFlag(badgeFlags, Settings.BADGE_FLAG_ARCHIVED) && chatList.getConstructor() == TdApi.ChatListMain.CONSTRUCTOR) { + TdlibCounter archiveCounter = tdlib.getCounter(ChatPosition.CHAT_LIST_ARCHIVE); + if (countMessages) { + mutedCount += Math.max(0, archiveCounter.messageCount); + unmutedCount += Math.max(0, archiveCounter.messageUnmutedCount); + } else { + mutedCount += Math.max(0, archiveCounter.chatCount); + unmutedCount += Math.max(0, archiveCounter.chatUnmutedCount); + } + } + if (BitwiseUtils.hasFlag(badgeFlags, Settings.BADGE_FLAG_MUTED)) { + boolean muted = mutedCount > 0 ? unmutedCount == 0 : target.isMuted(); + target.setCount(mutedCount, muted, animated); + } else { + target.setCount(unmutedCount, false, animated); + } + } + + private class UnreadCounterColorSet implements TextColorSet { + private @Nullable Counter counter; + + public void setCounter (@Nullable Counter counter) { + this.counter = counter; + } + + @Override + public int defaultTextColor () { + return counter != null ? ColorUtils.fromToArgb(foregroundColor(), backgroundColor(), counter.getMuteFactor()) : foregroundColor(); + } + + @Override + public int backgroundColor (boolean isPressed) { + return counter != null ? ColorUtils.alphaColor(1f - counter.getMuteFactor(), backgroundColor()) : backgroundColor(); + } + + @Override + public int outlineColor (boolean isPressed) { + return counter != null ? backgroundColor() : Color.TRANSPARENT; + } + + private int foregroundColor () { + return Theme.getColor(displayTabsAtBottom() ? ColorId.headerLightBackground : ColorId.headerBackground); + } + + private int backgroundColor() { + return Theme.getColor(displayTabsAtBottom() ? ColorId.headerLightText : ColorId.headerText); + } + }; + + private @Nullable ChatListPositionListener chatListPositionListener; + private @Nullable ChatListUnreadCountListener chatListUnreadCountListener; + + private class ChatListPositionListener implements TdlibSettingsManager.ChatListPositionListener { + @Override + public void onArchiveChatListPositionChanged (Tdlib tdlib, int archiveChatListPosition) { + if (tdlib.settings().isChatListEnabled(ChatPosition.CHAT_LIST_ARCHIVE)) { + updatePagerSections(false); + } + } + + @Override + public void onChatListStateChanged (Tdlib tdlib, TdApi.ChatList chatList, boolean isEnabled) { + updatePagerSections(false); + } + + @Override + public void onBadgeFlagsChanged (Tdlib tdlib, int newFlags) { + if (hasFolders()) { + updatePagerCounters(); + } + } + + @Override + public void onChatFolderStyleChanged (Tdlib tdlib, int newStyle) { + onFoldersAppearanceChanged(); + } + + @Override + public void onChatFolderOptionsChanged (Tdlib tdlib, int newOptions) { + onFoldersAppearanceChanged(); + } + } + + @Override + public void onBadgeSettingsChanged () { + if (hasFolders()) { + updatePagerCounters(); + } + } + + private void onFoldersAppearanceChanged () { + if (hasFolders()) { + updatePagerSections(false); + } + } + + @Override + public void onChatFolderOptionsChanged (int newOptions) { + onFoldersAppearanceChanged(); + } + + @Override + public void onChatFolderStyleChanged (int newStyle) { + onFoldersAppearanceChanged(); + } + + private class ChatListUnreadCountListener implements CounterChangeListener { + @Override + public void onChatCounterChanged (@NonNull TdApi.ChatList chatList, TdlibCounter counter, boolean availabilityChanged, int totalCount, int unreadCount, int unreadUnmutedCount) { + dispatchCounterChange(chatList, counter, false); + /*TODO update visibility? + if (chatList instanceof TdApi.ChatListArchive && availabilityChanged) { + runOnUiThreadOptional(() -> { + if (menu != null) { + menu.getChildAt(FILTER_ARCHIVE).setVisibility(tdlib.hasArchivedChats() ? View.VISIBLE : View.GONE); + } + }); + }*/ + } + + @Override + public void onMessageCounterChanged (@NonNull TdApi.ChatList chatList, TdlibCounter counter, int unreadCount, int unreadUnmutedCount) { + dispatchCounterChange(chatList, counter, true); + } + + private void dispatchCounterChange (TdApi.ChatList chatList, TdlibCounter counter, boolean areMessages) { + runOnUiThreadOptional(() -> { + int badgeFlags = tdlib.settings().getChatFolderBadgeFlags(); + if (areMessages != BitwiseUtils.hasFlag(badgeFlags, Settings.BADGE_FLAG_MESSAGES)) { + return; + } + dispatchCounter(chatList, counter); + if (chatList.getConstructor() == TdApi.ChatListArchive.CONSTRUCTOR && BitwiseUtils.hasFlag(badgeFlags, Settings.BADGE_FLAG_ARCHIVED)) { + dispatchCounter(ChatPosition.CHAT_LIST_MAIN, tdlib.getCounter(ChatPosition.CHAT_LIST_MAIN)); + } + }); + } + + private void dispatchCounter (TdApi.ChatList chatList, TdlibCounter counter) { + long itemId = getPagerItemId(chatList); + int positionToUpdate = getPagerItemPosition(itemId); + if (positionToUpdate != NO_POSITION) { + ViewPagerTopView.Item pagerSection = pagerSections.get(positionToUpdate); + if (pagerSection.counter != null) { + updateCounter(chatList, pagerSection.counter, counter, isFocused()); + } + } + } + } + + private void updatePagerCounters () { + for (int i = 0; i < pagerSections.size(); i++) { + TdApi.ChatList chatList = pagerChatLists.get(i); + ViewPagerTopView.Item section = pagerSections.get(i); + if (section.counter != null) { + TdlibCounter counter = tdlib.getCounter(chatList); + updateCounter(chatList, section.counter, counter, isFocused()); + } + } + } + + @Override + public void onChatFoldersChanged (TdApi.ChatFolderInfo[] chatFolders, int mainChatListPosition) { + if (Settings.instance().chatFoldersEnabled()) { + runOnUiThreadOptional(() -> + updatePagerSections(false) + ); + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/MapController.java b/app/src/main/java/org/thunderdog/challegram/ui/MapController.java index fa026e2892..134a44bc83 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/MapController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/MapController.java @@ -51,6 +51,7 @@ import org.thunderdog.challegram.telegram.LiveLocationManager; import org.thunderdog.challegram.telegram.MessageListener; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; import org.thunderdog.challegram.telegram.TdlibSender; import org.thunderdog.challegram.telegram.TdlibThread; import org.thunderdog.challegram.theme.ColorId; @@ -434,7 +435,8 @@ public void run () { subtitle = !StringUtils.isEmpty(args.address) ? args.address : Lang.beautifyCoordinates(args.latitude, args.longitude); } String title = args.locationOwnerChatId != 0 ? tdlib.chatTitle(args.locationOwnerChatId) : !StringUtils.isEmpty(args.title) ? args.title : Lang.getString(R.string.DroppedPin); - view.setLocation(title, subtitle, ColorId.file, null, false, 0, 0); + TdlibAccentColor accentColor = args.locationOwnerChatId != 0 ? tdlib.chatAccentColor(args.locationOwnerChatId) : tdlib.accentColor(TdlibAccentColor.InternalId.FILE_REGULAR); + view.setLocation(title, subtitle, accentColor, null, false, 0, 0); if (args.locationOwnerChatId != 0) { ImageFile avatarFile = tdlib.chatAvatar(args.locationOwnerChatId); if (avatarFile != null) { @@ -599,7 +601,7 @@ private void setLocationPlaceView (final MediaLocationPlaceView view, final TdAp TGMessageLocation.TimeResult result = buildLocationSubtitle(msg, isBase); TdApi.MessageLocation location = (TdApi.MessageLocation) msg.content; - view.setLocation(sender.getName(), result.text, sender.getAvatarColorId(), sender.getLetters(), expiresAt == 0 || SystemClock.uptimeMillis() >= expiresAt, location.livePeriod, expiresAt); + view.setLocation(sender.getName(), result.text, sender.getAccentColor(), sender.getLetters(), expiresAt == 0 || SystemClock.uptimeMillis() >= expiresAt, location.livePeriod, expiresAt); if (result.nextLiveLocationUpdateTime != -1) { view.scheduleSubtitleUpdater(new Runnable() { @Override @@ -785,7 +787,7 @@ public void onLocationResult (LocationHelper context, @NonNull String arg, @Null Args args = getArgumentsStrict(); inShareProgress = true; adapter.updateValuedSettingById(R.id.liveLocationSelf); - tdlib.sendMessage(args.chatId, args.messageThreadId, 0, Td.newSendOptions(tdlib.chatDefaultDisableNotifications(args.chatId)), new TdApi.InputMessageLocation(new TdApi.Location(myLocation.latitude, myLocation.longitude, myLocation.accuracy), arg1, myLocation.heading, 0)); + tdlib.sendMessage(args.chatId, args.messageThreadId, null, Td.newSendOptions(tdlib.chatDefaultDisableNotifications(args.chatId)), new TdApi.InputMessageLocation(new TdApi.Location(myLocation.latitude, myLocation.longitude, myLocation.accuracy), arg1, myLocation.heading, 0)); } }); break; @@ -1705,7 +1707,7 @@ public void onResult (TdApi.Object object) { if (messages.length > 0) { final ArrayList> list = new ArrayList<>(messages.length); for (TdApi.Message message : messages) { - if (message.content.getConstructor() != TdApi.MessageLocation.CONSTRUCTOR) { + if (!Td.isLocation(message.content)) { continue; } if (message.isOutgoing || tdlib.isSelfSender(message)) { @@ -1742,7 +1744,7 @@ private void addMessageIfNeeded (final TdApi.Message message) { if (isDestroyed()) { return; } - if (message.content.getConstructor() != TdApi.MessageLocation.CONSTRUCTOR) { + if (!Td.isLocation(message.content)) { return; } if (message.schedulingState != null || tdlib.isSelfSender(message)) { @@ -1787,13 +1789,13 @@ public void onMessageSendSucceeded (TdApi.Message message, long oldMessageId) { @Override public void onMessageContentChanged (long chatId, long messageId, TdApi.MessageContent newContent) { - if (newContent.getConstructor() == TdApi.MessageLocation.CONSTRUCTOR) { + if (Td.isLocation(newContent)) { updateMessageIfNeeded(chatId, messageId, (TdApi.MessageLocation) newContent); } } @Override - public void onMessageSendFailed (TdApi.Message message, long oldMessageId, int errorCode, String errorMessage) { } + public void onMessageSendFailed (TdApi.Message message, long oldMessageId, TdApi.Error error) { } @Override public int compare (LocationPoint o1, LocationPoint o2) { diff --git a/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsController.java b/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsController.java index 139943c9fc..ad66af7025 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsController.java @@ -3,6 +3,7 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.text.TextUtils; +import android.text.style.ClickableSpan; import android.util.TypedValue; import android.view.Gravity; import android.view.View; @@ -33,15 +34,19 @@ import org.thunderdog.challegram.util.text.TextEntity; import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.widget.CustomTextView; +import org.thunderdog.challegram.widget.EmojiPacksInfoView; import org.thunderdog.challegram.widget.EmojiTextView; import me.vkryl.core.StringUtils; public class MessageOptionsController extends BottomSheetViewController.BottomSheetBaseRecyclerViewController { private Options options; + private long[] emojiPackIds; + private long emojiPackFirstEmoji; private View.OnClickListener listener; private OptionsAdapter adapter; private ThemeListenerList themeProvider; + private Runnable hideWindowDelegate; public MessageOptionsController (Context context, Tdlib tdlib, ThemeListenerList themeProvider) { @@ -54,6 +59,9 @@ public void setArguments (MessageOptionsController.Args args) { super.setArguments(args); this.options = args.options; this.listener = args.listener; + this.emojiPackIds = args.emojiPackIds; + this.emojiPackFirstEmoji = args.emojiPackFirstEmoji; + this.hideWindowDelegate = args.hideWindowDelegate; } @Override @@ -73,7 +81,7 @@ protected int getRecyclerBackground () { @Override protected void onCreateView (Context context, CustomRecyclerView recyclerView) { - adapter = new OptionsAdapter(context, this, options, listener, themeProvider); + adapter = new OptionsAdapter(context, this, options, emojiPackFirstEmoji, emojiPackIds, listener, themeProvider); LinearLayoutManager manager = new LinearLayoutManager(context); addThemeInvalidateListener(recyclerView); recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER); @@ -89,10 +97,16 @@ public boolean needsTempUpdates () { public static class Args { public Options options; public View.OnClickListener listener; + public long[] emojiPackIds; + public long emojiPackFirstEmoji; + public Runnable hideWindowDelegate; - public Args (Options options, View.OnClickListener listener) { + public Args (Options options, View.OnClickListener listener, long emojiPackFirstEmoji, long[] emojiPackIds, Runnable hideWindowDelegate) { this.options = options; this.listener = listener; + this.emojiPackIds = emojiPackIds; + this.emojiPackFirstEmoji = emojiPackFirstEmoji; + this.hideWindowDelegate = hideWindowDelegate; } } @@ -102,7 +116,7 @@ public OptionHolder (@NonNull View itemView) { super(itemView); } - public static OptionHolder create (Context context, Tdlib tdlib, int viewType, View.OnClickListener onClickListener) { + public static OptionHolder create (Context context, ViewController parent, int viewType, View.OnClickListener onClickListener) { if (viewType == OptionsAdapter.TYPE_OPTION) { EmojiTextView text = new EmojiTextView(context); text.setScrollDisabled(true); @@ -118,8 +132,11 @@ public static OptionHolder create (Context context, Tdlib tdlib, int viewType, V Views.setClickable(text); RippleSupport.setTransparentSelector(text); return new OptionHolder(text); + } else if (viewType == OptionsAdapter.TYPE_EMOJI_PACK_INFO) { + EmojiPacksInfoView textView= new EmojiPacksInfoView(context, parent, parent.tdlib()); + return new OptionHolder(textView); } else { - CustomTextView textView = new CustomTextView(context, tdlib); + CustomTextView textView = new CustomTextView(context, parent.tdlib()); textView.setTextColorId(ColorId.textLight); textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); textView.setPadding(Screen.dp(16f), Screen.dp(14f), Screen.dp(16f), Screen.dp(6f)); @@ -133,26 +150,37 @@ private static class OptionsAdapter extends RecyclerView.Adapter { private final View.OnClickListener onClickListener; private final Options options; private final Tdlib tdlib; - private final ViewController parent; + private final MessageOptionsController parent; @Nullable private final ThemeListenerList themeProvider; + private final long[] emojiPackIds; + private final long emojiPackFirstEmoji; + + private final int textInfoPosition; + private final int emojiInfoPosition; public static final int TYPE_OPTION = 0; public static final int TYPE_INFO = 1; + public static final int TYPE_EMOJI_PACK_INFO = 2; - OptionsAdapter (Context context, ViewController parent, Options options, View.OnClickListener onClickListener, @Nullable ThemeListenerList themeProvider) { + OptionsAdapter (Context context, MessageOptionsController parent, Options options, long emojiPackFirstEmoji, long[] emojiPackIds, View.OnClickListener onClickListener, @Nullable ThemeListenerList themeProvider) { this.parent = parent; this.tdlib = parent.tdlib(); this.onClickListener = onClickListener; this.context = context; this.options = options; this.themeProvider = themeProvider; + this.emojiPackIds = emojiPackIds; + this.emojiPackFirstEmoji = emojiPackFirstEmoji; + + this.emojiInfoPosition = emojiPackIds.length > 0 ? 0 : -1; + this.textInfoPosition = StringUtils.isEmpty(options.info) ? -1 : (emojiInfoPosition + 1); } @NonNull @Override public OptionHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { - return OptionHolder.create(context, tdlib, viewType, onClickListener); + return OptionHolder.create(context, this.parent, viewType, onClickListener); } @Override @@ -163,6 +191,9 @@ public void onBindViewHolder (@NonNull OptionHolder holder, int position) { if (!StringUtils.isEmpty(options.info)) { position -= 1; } + if (emojiInfoPosition > -1) { + position -= 1; + } OptionItem item = options.items[position]; TextView textView = ((TextView) holder.itemView); @@ -199,26 +230,42 @@ public void onBindViewHolder (@NonNull OptionHolder holder, int position) { textView.setTextColorId(ColorId.textLight); textView.setText(str, parsed, false); } + + if (type == TYPE_EMOJI_PACK_INFO) { + EmojiPacksInfoView textView = ((EmojiPacksInfoView) holder.itemView); + textView.setId(R.id.btn_emojiPackInfoButton); + textView.setTextSize(15f); + textView.setTextColorId(ColorId.textLight); + textView.update(emojiPackFirstEmoji, emojiPackIds, new ClickableSpan() { + @Override + public void onClick (@NonNull View widget) { + parent.listener.onClick(textView); + } + }, false); + } } @Override public int getItemViewType (int position) { - if (position == 0 && !StringUtils.isEmpty(options.info)) { + if (position == textInfoPosition) { return TYPE_INFO; } + if (position == emojiInfoPosition) { + return TYPE_EMOJI_PACK_INFO; + } return TYPE_OPTION; } @Override public int getItemCount () { - return options.items.length + (StringUtils.isEmpty(options.info) ? 0 : 1); + return options.items.length + (textInfoPosition > -1 ? 1 : 0) + (emojiInfoPosition > -1 ? 1 : 0); } } @Override public int getItemsHeight (RecyclerView recyclerView) { - int totalHeight = options.items.length * Screen.dp(54); + int totalHeight = (options.items.length + 2) * Screen.dp(54); if (!StringUtils.isEmpty(options.info)) { View view = recyclerView.getLayoutManager().findViewByPosition(0); int hintHeight = @@ -235,6 +282,9 @@ public int getItemsHeight (RecyclerView recyclerView) { } } } + if (emojiPackIds.length > 0) { + totalHeight += Screen.dp(40); + } return totalHeight; } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsPagerController.java b/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsPagerController.java index 780519d3dd..0e7c2a2a06 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsPagerController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsPagerController.java @@ -16,17 +16,28 @@ import android.content.Context; import android.content.res.Resources; +import android.graphics.Canvas; import android.graphics.Point; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.Gravity; +import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import androidx.annotation.NonNull; import androidx.collection.SparseArrayCompat; import androidx.core.graphics.ColorUtils; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.emoji.MediaStickersAdapter; +import org.thunderdog.challegram.component.sticker.StickerSmallView; +import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.data.TGReaction; @@ -40,37 +51,41 @@ import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.ColorState; import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Keyboard; +import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.util.DrawableProvider; import org.thunderdog.challegram.util.OptionDelegate; import org.thunderdog.challegram.util.text.Counter; import org.thunderdog.challegram.util.text.TextColorSet; +import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.widget.CustomTextView; -import org.thunderdog.challegram.widget.ReactionsSelectorRecyclerView; +import org.thunderdog.challegram.widget.EmojiLayout; +import org.thunderdog.challegram.widget.PopupLayout; import org.thunderdog.challegram.widget.ViewPager; +import org.thunderdog.challegram.widget.decoration.ItemDecorationFirstViewTop; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutRecyclerController; import java.util.Arrays; +import java.util.Set; import java.util.concurrent.TimeUnit; +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.BoolAnimator; import me.vkryl.android.animator.FactorAnimator; import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; import me.vkryl.td.Td; public class MessageOptionsPagerController extends BottomSheetViewController implements - FactorAnimator.Target, View.OnClickListener, Menu, DrawableProvider, - Counter.Callback, ReactionsSelectorRecyclerView.ReactionSelectDelegate, TextColorSet { + FactorAnimator.Target, View.OnClickListener, Menu, DrawableProvider, PopupLayout.TouchDownInterceptor, + Counter.Callback, TextColorSet { - private final Options options; - private final TGMessage message; - - private final boolean needShowOptions; - private final boolean needShowViews; - private final boolean needShowReactions; - - private final TdApi.MessageReaction[] reactions; + private final State state; private final ViewPagerTopView.Item[] counters; - private int baseCountersWidth; private int startPage = 0; public MessageOptionsPagerController (Context context, Tdlib tdlib, Options options, TGMessage message, TdApi.ReactionType defaultReactionType, OptionDelegate optionDelegate) { @@ -78,46 +93,41 @@ public MessageOptionsPagerController (Context context, Tdlib tdlib, Options opti if (optionDelegate == null) throw new IllegalArgumentException(); setArguments(optionDelegate); - this.options = options; - this.message = message; - - final boolean needHideViews = !message.canGetViewers() || message.isUnread() || message.noUnread(); - this.reactions = message.getMessageReactions().getReactions(); - this.needShowOptions = options != null; - this.needShowViews = !needHideViews; - this.needShowReactions = reactions != null && message.canGetAddedReactions() && message.getMessageReactions().getTotalCount() > 0 && !tdlib.isUserChat(message.getChatId()); + + this.state = new State(message, options, this::onReactionClick); + this.state.headerAlwaysVisibleCountersWidth = 0; + this.counters = new ViewPagerTopView.Item[getPagerItemCount()]; - this.baseCountersWidth = 0; int i = 0; - if (needShowOptions) { + if (state.needShowMessageOptions) { OPTIONS_POSITION = i++; counters[OPTIONS_POSITION] = new ViewPagerTopView.Item(); } else { OPTIONS_POSITION = -1; } - if (needShowReactions) { + if (state.needShowMessageReactionSenders) { ALL_REACTED_POSITION = i++; counters[ALL_REACTED_POSITION] = new ViewPagerTopView.Item(new Counter.Builder() .noBackground().allBold(true).textSize(13f).colorSet(this).callback(this) .drawable(R.drawable.baseline_favorite_16, 16f, 6f, Gravity.LEFT) .build(), this, Screen.dp(16)); counters[ALL_REACTED_POSITION].counter.setCount(message.getMessageReactions().getTotalCount(), false); - baseCountersWidth += counters[ALL_REACTED_POSITION].calculateWidth(null); + state.headerAlwaysVisibleCountersWidth += counters[ALL_REACTED_POSITION].calculateWidth(null, Screen.dp(ViewPagerTopView.DEFAULT_ITEM_SPACING)); } else { ALL_REACTED_POSITION = -1; } - if (needShowViews) { + if (state.needShowMessageViews) { SEEN_POSITION = i++; counters[SEEN_POSITION] = new ViewPagerTopView.Item(new Counter.Builder() .noBackground().allBold(true).textSize(13f).colorSet(this).callback(this).visibleIfZero() .drawable(R.drawable.baseline_visibility_16, 16f, 6f, Gravity.LEFT) .build(), this, Screen.dp(16)); counters[SEEN_POSITION].counter.setCount(1, false); - int itemWidth = counters[SEEN_POSITION].calculateWidth(null); // - Screen.dp(16); - baseCountersWidth += itemWidth; + int itemWidth = counters[SEEN_POSITION].calculateWidth(null, Screen.dp(ViewPagerTopView.DEFAULT_ITEM_SPACING)); // - Screen.dp(16); + state.headerAlwaysVisibleCountersWidth += itemWidth; counters[SEEN_POSITION].setStaticWidth(itemWidth - Screen.dp(16)); counters[SEEN_POSITION].counter.setCount(Tdlib.CHAT_LOADING, false); getMessageOptions(); @@ -125,9 +135,9 @@ public MessageOptionsPagerController (Context context, Tdlib tdlib, Options opti SEEN_POSITION = -1; } - if (needShowReactions) { + if (state.needShowMessageReactionSenders) { REACTED_START_POSITION = i; - for (TdApi.MessageReaction reaction : reactions) { + for (TdApi.MessageReaction reaction : state.messageReactions) { TGReaction tgReaction = tdlib.getReaction(reaction.type); counters[i] = new ViewPagerTopView.Item(tgReaction, new Counter.Builder() .noBackground().allBold(true).textSize(13f).colorSet(this).callback(this) @@ -141,12 +151,25 @@ public MessageOptionsPagerController (Context context, Tdlib tdlib, Options opti } else { REACTED_START_POSITION = -1; } + + if (!state.needShowMessageOptions) { + state.headerAlwaysVisibleCountersWidth = 0; + } } private ViewPagerHeaderViewReactionsCompact headerCell; // Create view + private float headerViewOverTranslation; + + public void setHeaderViewOverTranslation (float headerViewOvertranslation) { + this.headerViewOverTranslation = headerViewOvertranslation; + if (headerView != null) { + headerView.setTranslationY(headerTranslationY); + } + } + @Override protected HeaderView onCreateHeaderView () { HeaderView headerView = new HeaderView(context) { @@ -163,18 +186,34 @@ protected void onLayout (boolean changed, int left, int top, int right, int bott startPage = 0; } } + + private float rTranslationY; + + @Override + public void setTranslationY (float translationY) { + super.setTranslationY(translationY + (rTranslationY = headerViewOverTranslation)); + } + + @Override + public float getTranslationY () { + return super.getTranslationY() - rTranslationY; + } }; headerView.initWithSingleController(this, false); headerView.getFilling().setShadowAlpha(0f); headerView.getBackButton().setIsReverse(true); - ViewSupport.setThemedBackground(headerView, ColorId.background, this); + if (state.needShowReactionsPopupPicker) { + headerView.setBackground(null); + } else { + ViewSupport.setThemedBackground(headerView, ColorId.background, this); + } return headerView; - }; + } @Override protected void onBeforeCreateView () { - headerCell = new ViewPagerHeaderViewReactionsCompact(context, tdlib, message, needShowOptions ? baseCountersWidth : 0, needShowOptions, needShowReactions, needShowViews) { + headerCell = new ViewPagerHeaderViewReactionsCompact(context, state) { @Override public void onThemeInvalidate (boolean isTempUpdate) { setHeaderBackgroundFactor(headerBackgroundFactor); @@ -182,17 +221,88 @@ public void onThemeInvalidate (boolean isTempUpdate) { super.onThemeInvalidate(isTempUpdate); } }; - headerCell.setReactionsSelectorDelegate(this); addThemeInvalidateListener(headerCell); } + @Override + protected View onCreateView (Context context) { + ViewGroup vg = (ViewGroup) super.onCreateView(context); + + if (state.needShowReactionsPopupPicker) { + reactionsPickerWrapper = new FrameLayoutFix(context) { + @Override + public boolean dispatchTouchEvent (MotionEvent ev) { + if (needIgnoreTouchEvent(ev)) { + return false; + } + return super.dispatchTouchEvent(ev); + } + + @Override + public boolean onInterceptTouchEvent (MotionEvent ev) { + if (needIgnoreTouchEvent(ev)) { + return false; + } + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent (MotionEvent ev) { + if (needIgnoreTouchEvent(ev)) { + return false; + } + return super.onTouchEvent(ev); + } + + private boolean needIgnoreTouchEvent (MotionEvent ev) { + return (ev.getAction() == MotionEvent.ACTION_DOWN && !between(ev.getY(), getPickerTop() + - HeaderView.getSize(true) * reactionsPickerController.getTopHeaderVisibility() * reactionsPickerVisibility.getFloatValue(), + getPickerBottom())); + } + + private boolean between (float y, float a, float b) { + return y > a && y < b; + } + + @Override + protected void dispatchDraw (Canvas canvas) { + float top = getPickerTop(); + float bottom = getPickerBottom(); + if (bottom <= top) return; + + canvas.save(); + canvas.clipRect(0, top, getMeasuredWidth(), bottom); + canvas.drawRect(0, top, getMeasuredWidth(), bottom, Paints.fillingPaint(Theme.backgroundColor())); + super.dispatchDraw(canvas); + canvas.restore(); + } + }; + vg.addView(reactionsPickerWrapper, 2, FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + reactionsPickerWrapper.addView(createReactionsPopupPicker()); + } + return vg; + } + + private void invalidatePickerWrapper () { + if (reactionsPickerWrapper != null) { + reactionsPickerWrapper.invalidate(); + setHeaderViewOverTranslation(getPickerTop() - headerTranslationY); + } + } + + @Override + protected void setHeaderPosition (float y) { + super.setHeaderPosition(y); + checkReactionPickerPosition(); + } + @Override protected void onCreateView (Context context, FrameLayoutFix contentView, ViewPager pager) { pager.setOffscreenPageLimit(1); prepareControllerForPosition(startPage, null); tdlib.ui().post(this::launchOpenAnimation); - headerCell.getTopView().setTextPadding(Screen.dp(0)); + headerCell.getTopView().setItemPadding(Screen.dp(0)); headerCell.getTopView().setItems(Arrays.asList(counters)); headerCell.getTopView().setOnItemClickListener(this); headerCell.getTopView().setSelectionColorId(ColorId.text); @@ -200,12 +310,16 @@ protected void onCreateView (Context context, FrameLayoutFix contentView, ViewPa headerCell.getBackButton().setColor(Theme.getColor(ColorId.headerLightIcon)); headerCell.getBackButton().setOnClickListener((v) -> { - if (needShowOptions) { + if (state.needShowMessageOptions) { headerCell.getTopView().getOnItemClickListener().onPagerItemClick(0); } else { hidePopupWindow(true); } }); + + if (headerCell.getMoreButton() != null) { + headerCell.getMoreButton().setOnClickListener(v -> showReactionPicker()); + } } @Override @@ -232,14 +346,14 @@ public void onPageScrolled (int position, float positionOffset, int positionOffs headerCell.onPageScrolled(position, positionOffset); } - if (position == 0 && positionOffset == 0f && needShowOptions) { + if (position == 0 && positionOffset == 0f && state.needShowMessageOptions) { ViewController controller = findCachedControllerByPosition(0); if (controller instanceof BottomSheetViewController.BottomSheetBaseControllerPage) { ((BottomSheetBaseControllerPage) controller).onScrollToBottomRequested(); } } - if (position == 0 && needShowOptions) { + if (position == 0 && state.needShowMessageOptions) { float targetPosition = getContentOffset() + HeaderView.getTopOffset(); float animPosition = targetPosition + (lastHeaderPosition - targetPosition) * positionOffset; setHeaderPosition(animPosition); @@ -248,6 +362,11 @@ public void onPageScrolled (int position, float positionOffset, int positionOffs } } + if (reactionsPickerRecyclerView != null) { + reactionsPickerRecyclerView.setTranslationX(-MathUtils.clamp(position + positionOffset) * reactionsPickerRecyclerView.getMeasuredWidth()); + invalidatePickerWrapper(); + } + super.onPageScrolled(position, positionOffset, positionOffsetPixels); } @@ -259,19 +378,20 @@ protected void setHeaderBackgroundFactor (float headerBackgroundFactor) { headerBackgroundFactor ); setLickViewColor(headerBackground); - if (headerView != null) { + if (headerView != null && !state.needShowReactionsPopupPicker) { headerView.setBackgroundColor(headerBackground); } if (headerCell != null) { headerCell.updatePaints(headerBackground); } + invalidatePickerWrapper(); } // private TdApi.MessageViewers messageViewers; private void getMessageOptions () { - tdlib.client().send(new TdApi.GetMessageViewers(message.getChatId(), message.getId()), (obj) -> { + tdlib.client().send(new TdApi.GetMessageViewers(state.message.getChatId(), state.message.getId()), (obj) -> { if (obj.getConstructor() != TdApi.MessageViewers.CONSTRUCTOR) return; runOnUiThreadOptional(() -> { messageViewers = (TdApi.MessageViewers) obj; @@ -287,32 +407,36 @@ private void getMessageOptions () { private CharSequence cachedHint; private int cachedHintHeight, cachedHintAvailWidth; + private int getOptionItemsHeight () { + int optionItemsHeight = state.options.items != null ? Screen.dp(54) * state.options.items.length : 0; + int hintHeight; + if (!StringUtils.isEmpty(state.options.info)) { + int availWidth = Screen.currentWidth() - Screen.dp(16f) * 2; // FIXME: rely on parent view width + if (cachedHint != null && cachedHintAvailWidth == availWidth && cachedHint.equals(state.options.info)) { + hintHeight = cachedHintHeight; + } else { + hintHeight = CustomTextView.measureHeight(this, state.options.info, 15f, availWidth); + cachedHint = state.options.info; + cachedHintAvailWidth = availWidth; + cachedHintHeight = hintHeight; + } + hintHeight += Screen.dp(14f) + Screen.dp(6f); + } else { + hintHeight = 0; + } + if (state.emojiPackIds.length > 0) { + hintHeight += Screen.dp(40); + } + return optionItemsHeight + hintHeight; + } + @Override protected int getContentOffset () { - if (needShowOptions) { - int optionItemsHeight = Screen.dp(54) * options.items.length; - int hintHeight; - if (!StringUtils.isEmpty(options.info)) { - int availWidth = Screen.currentWidth() - Screen.dp(16f) * 2; // FIXME: rely on parent view width - if (cachedHint != null && cachedHintAvailWidth == availWidth && cachedHint.equals(options.info)) { - hintHeight = cachedHintHeight; - } else { - hintHeight = CustomTextView.measureHeight(this, options.info, 15f, availWidth); - cachedHint = options.info; - cachedHintAvailWidth = availWidth; - cachedHintHeight = hintHeight; - } - hintHeight += Screen.dp(14f) + Screen.dp(6f); - } else { - hintHeight = 0; - } - return ( - getTargetHeight() - - (Screen.dp(54) + HeaderView.getTopOffset()) - - optionItemsHeight - - hintHeight - - Screen.dp(1) - ); + if (state.needShowMessageOptions) { + return (getTargetHeight() + - (Screen.dp(54) + HeaderView.getTopOffset()) + - getOptionItemsHeight() + - Screen.dp(1)); } else { return Screen.currentHeight() / 2; } @@ -337,7 +461,7 @@ protected ViewController onCreatePagerItemForPosition (Context context, int p }; MessageOptionsController c = new MessageOptionsController(context, this.tdlib, getThemeListeners()); - c.setArguments(new MessageOptionsController.Args(options, onClickListener)); + c.setArguments(new MessageOptionsController.Args(state.options, onClickListener, state.message.getFirstEmojiId(), state.emojiPackIds, () -> hidePopupWindow(true))); c.getValue(); setHeaderPosition(getContentOffset() + HeaderView.getTopOffset()); setDefaultListenersAndDecorators(c); @@ -345,23 +469,23 @@ protected ViewController onCreatePagerItemForPosition (Context context, int p } if (position == ALL_REACTED_POSITION) { - MessageOptionsReactedController c = new MessageOptionsReactedController(context, this.tdlib, getPopupLayout(), message, null); + MessageOptionsReactedController c = new MessageOptionsReactedController(context, this.tdlib, getPopupLayout(), state.message, null); c.getValue(); setDefaultListenersAndDecorators(c); return c; } if (position == SEEN_POSITION) { - MessageOptionsSeenController c = new MessageOptionsSeenController(context, this.tdlib, getPopupLayout(), message); + MessageOptionsSeenController c = new MessageOptionsSeenController(context, this.tdlib, getPopupLayout(), state.message); c.getValue(); setDefaultListenersAndDecorators(c); return c; } if (position >= REACTED_START_POSITION && REACTED_START_POSITION != -1) { - MessageOptionsReactedController c = new MessageOptionsReactedController(context, this.tdlib, getPopupLayout(), message, reactions[position - REACTED_START_POSITION].type); + MessageOptionsReactedController c = new MessageOptionsReactedController(context, this.tdlib, getPopupLayout(), state.message, state.messageReactions[position - REACTED_START_POSITION].type); c.getValue(); - if (isFirstCreation && !needShowOptions) { + if (isFirstCreation && !state.needShowMessageOptions) { setHeaderPosition(getContentOffset() + HeaderView.getTopOffset()); isFirstCreation = false; } @@ -374,7 +498,7 @@ protected ViewController onCreatePagerItemForPosition (Context context, int p @Override protected int getPagerItemCount () { - return (needShowOptions ? 1 : 0) + (needShowViews ? 1 : 0) + (needShowReactions ? reactions.length + 1 : 0); + return state.getPagesCount(); } @Override @@ -412,28 +536,37 @@ public void onClick (View v) { @Override public boolean onBackPressed (boolean fromTop) { + if (reactionsPickerVisibility != null && reactionsPickerVisibility.getValue()) { + if (!reactionsPickerController.onBackPressed(fromTop)) { + hideReactionPicker(); + } + return true; + } return false; } @Override - public void onThemeColorsChanged (boolean areTemp, ColorState state) { - super.onThemeColorsChanged(areTemp, state); - if (headerView != null) { - headerView.resetColors(this, null); + public void hideSoftwareKeyboard () { + if (reactionsPickerController != null) { + reactionsPickerController.hideSoftwareKeyboard(); + return; } + super.hideSoftwareKeyboard(); } @Override - public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { - switch (id) { - + public void destroy () { + super.destroy(); + if (reactionsPickerController != null) { + reactionsPickerController.destroy(); } } @Override - public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { - switch (id) { - + public void onThemeColorsChanged (boolean areTemp, ColorState state) { + super.onThemeColorsChanged(areTemp, state); + if (headerView != null) { + headerView.resetColors(this, null); } } @@ -466,58 +599,468 @@ public int defaultTextColor () { return Theme.getColor(ColorId.text); } - // Reactions selector delegate + private Client.ResultHandler handler (View v, Runnable onSuccess) { + return object -> { + switch (object.getConstructor()) { + case TdApi.Ok.CONSTRUCTOR: + tdlib.ui().post(onSuccess); + break; + case TdApi.Error.CONSTRUCTOR: + tdlib.ui().post(() -> onSendError(v, (TdApi.Error) object)); + break; + } + }; + } + + private void onSendError (View v, TdApi.Error error) { + context().tooltipManager().builder(v).show(tdlib, TD.toErrorString(error)).hideDelayed(3500, TimeUnit.MILLISECONDS); + state.message.cancelScheduledSetReactionAnimation(); + } @Override - public void onClick (View v, TGReaction reaction) { - int[] positionCords = new int[2]; - v.getLocationOnScreen(positionCords); + protected void setupPopupLayout (PopupLayout popupLayout) { + popupLayout.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + popupLayout.setBoundController(this); + popupLayout.setPopupHeightProvider(this); + popupLayout.init(true); + //popupLayout.setHideKeyboard(); + popupLayout.setNeedRootInsets(); + popupLayout.setTouchProvider(this); + popupLayout.setIgnoreHorizontal(); + popupLayout.setTouchDownInterceptor(this); + + // super.setupPopupLayout(popupLayout); + } + + private static final int KEYBOARD_HEIGHT = 2; + private final FactorAnimator keyboardHeight = new FactorAnimator(KEYBOARD_HEIGHT, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 220L, 0); - int startX = positionCords[0] + v.getMeasuredWidth() / 2; - int startY = positionCords[1] + v.getMeasuredHeight() / 2; + @Override + public boolean onKeyboardStateChanged (boolean visible) { + final boolean r = super.onKeyboardStateChanged(visible); + keyboardHeight.animateTo(getKeyboardState() ? Keyboard.getSize(Keyboard.getSize()) : 0); + if (reactionsPickerRecyclerView != null) { + reactionsPickerRecyclerView.invalidateItemDecorations(); + } + return r; + } - boolean hasReaction = message.getMessageReactions().hasReaction(reaction.type); - if (hasReaction || message.messagesController().callNonAnonymousProtection(message.getId() + reaction.getId(), tooltipManager().builder(v))) { - if (message.getMessageReactions().toggleReaction(reaction.type, false, true, handler(v, () -> { - }))) { - message.scheduleSetReactionAnimationFromBottomSheet(reaction, new Point(startX, startY)); + /* Reactions popup picker */ + + private static final int REACTIONS_PICKER_VISIBILITY_ANIMATOR_ID = 0; + + private BoolAnimator reactionsPickerVisibility; + + private FrameLayoutFix reactionsPickerWrapper; + private ReactionsPickerController reactionsPickerController; + private CustomRecyclerView reactionsPickerRecyclerView; + private PickerOpenerScrollListener reactionsPickerScrollListener; + private View reactionsPickerBottomHeaderView; + private boolean doNotUpdateScrollReactionPicker; + private ItemDecorationFirstViewTop reactionPickerTopDecoration; + + private CustomRecyclerView createReactionsPopupPicker () { + reactionsPickerVisibility = new BoolAnimator(REACTIONS_PICKER_VISIBILITY_ANIMATOR_ID, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 280L, false); + reactionsPickerController = new ReactionsPickerController(context, tdlib) { + @Override + protected void onBottomHeaderEnterSearchMode () { + reactionPickerTopDecoration.scheduleDisableDecorationOffset(); + } + + @Override + protected void onBottomHeaderLeaveSearchMode () { + reactionPickerTopDecoration.enableDecorationOffset(); + } + }; + reactionsPickerController.setArguments(state); + reactionsPickerController.getValue(); + + reactionsPickerRecyclerView = reactionsPickerController.getRecyclerView(); + reactionsPickerRecyclerView.setClipToPadding(false); + + reactionPickerTopDecoration = ItemDecorationFirstViewTop.attach(reactionsPickerRecyclerView, this::getReactionPickerOffsetTopReal); + reactionsPickerRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { + @Override + public void getItemOffsets (@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + final int position = parent.getChildAdapterPosition(view); + final int itemCount = parent.getAdapter().getItemCount(); + final boolean isUnknown = position == RecyclerView.NO_POSITION; + final int itemType = position < itemCount && !isUnknown ? parent.getAdapter().getItemViewType(position) : -1; + int leftRight = 0, bottom = 0; + + if (itemType == MediaStickersAdapter.StickerHolder.TYPE_STICKER || view instanceof StickerSmallView) { + leftRight = Screen.dp(-1); + } + + if (position == itemCount - 1) { + int keyboardHeight = getKeyboardState() ? Keyboard.getSize(Keyboard.getSize()) : 0; + bottom = Math.max(parent.getMeasuredHeight() - reactionsPickerController.measureItemsHeight(), keyboardHeight + Screen.dp(64)); + } + + outRect.set(leftRight, 0, leftRight, bottom); + } + }); + reactionsPickerRecyclerView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + reactionsPickerRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + invalidatePickerWrapper(); + checkReactionPickerHeaderTopVisibility(); } - hidePopupWindow(true); + + @Override + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + invalidatePickerWrapper(); + checkReactionPickerHeaderTopVisibility(); + } + } + }); + reactionsPickerRecyclerView.addOnScrollListener(reactionsPickerScrollListener = new PickerOpenerScrollListener()); + reactionsPickerRecyclerView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> invalidatePickerWrapper()); + + return reactionsPickerRecyclerView; + } + + private void showReactionPicker () { + fixView.setVisibility(View.GONE); + + if (reactionsPickerBottomHeaderView == null) { + reactionsPickerBottomHeaderView = reactionsPickerController.getBottomHeaderViewGroup(); + reactionsPickerBottomHeaderView.setAlpha(0f); + reactionsPickerBottomHeaderView.setVisibility(View.GONE); + reactionsPickerController.getTopHeaderView().getBackButton().setOnClickListener(v -> { + hideReactionPicker(); + }); + + if (state.needShowCustomEmojiInsidePicker) { + wrapView.addView(reactionsPickerBottomHeaderView); + } + wrapView.addView(reactionsPickerController.getTopHeaderViewGroup()); + reactionsPickerController.prepareToShow(); + } + + reactionsPickerVisibility.setValue(true, true); + + } + + private void hideReactionPicker () { + doNotUpdateScrollReactionPicker = true; + reactionsPickerRecyclerView.stopScroll(); + reactionsPickerScrollListener.reset(true); + reactionsPickerVisibility.setValue(false, true); + reactionsPickerController.closeBottomHeaderSearchMode(false); + reactionsPickerController.scrollToDefaultPosition(getReactionPickerOffsetTopReal()); + reactionPickerTopDecoration.enableDecorationOffset(); + contentView.setVisibility(View.VISIBLE); + if (headerView != null) { + headerView.setVisibility(View.VISIBLE); } } @Override - public void onLongClick (View v, TGReaction reaction) { - int[] positionCords = new int[2]; - v.getLocationOnScreen(positionCords); + protected void onCustomShowComplete () { + super.onCustomShowComplete(); + if (reactionsPickerRecyclerView != null) { + reactionsPickerRecyclerView.invalidateItemDecorations(); + reactionsPickerRecyclerView.scrollToPosition(0); + } + } + + private void checkReactionPickerPosition () { + if (reactionsPickerWrapper == null) { + return; + } - int startX = positionCords[0] + v.getMeasuredWidth() / 2; - int startY = positionCords[1] + v.getMeasuredHeight() / 2; + float y = MathUtils.fromTo(0, Math.min(getReactionPickerOffsetTopDefault() - getReactionPickerOffsetTopReal(), 0), reactionsPickerVisibility.getFloatValue()); + boolean isNotInScroll = reactionsPickerRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE; + if (isNotInScroll && !doNotUpdateScrollReactionPicker) { + ((LinearLayoutManager) reactionsPickerRecyclerView.getLayoutManager()).scrollToPositionWithOffset(0, (int) y); + } + + reactionsPickerRecyclerView.setTranslationY((headerTranslationY - Math.max(getHeaderHeight(), getContentOffset()) - HeaderView.getTopOffset()) + * (1f - reactionsPickerVisibility.getFloatValue())); + invalidatePickerWrapper(); + checkReactionPickerHeaderTopVisibility(); + } - if (message.messagesController().callNonAnonymousProtection(message.getId() + reaction.getId(), tooltipManager().builder(v))) { - if (message.getMessageReactions().toggleReaction(reaction.type, true, true, handler(v, () -> { - }))) { - message.scheduleSetReactionAnimationFullscreenFromBottomSheet(reaction, new Point(startX, startY)); + private void onReactionClick (View v, TGReaction reaction, boolean isLongClick) { + if (isLongClick) { + if (Config.DISABLE_ANONYMOUS_NON_OWNER_REACTIONS && tdlib.isAnonymousAdminNonCreator(state.message.getChatId())) { + return; + } + int[] positionCords = new int[2]; + v.getLocationOnScreen(positionCords); + + int startX = positionCords[0] + v.getMeasuredWidth() / 2; + int startY = positionCords[1] + v.getMeasuredHeight() / 2; + + if (!Config.PROTECT_ANONYMOUS_REACTIONS || state.message.messagesController().callNonAnonymousProtection(state.message.getId() + reaction.getId(), tooltipManager().builder(v))) { + if (state.message.getMessageReactions().toggleReaction(reaction.type, true, true, handler(v, () -> { + }))) { + state.message.scheduleSetReactionAnimationFullscreenFromBottomSheet(reaction, new Point(startX, startY)); + } + hidePopupWindow(true); + } + } else { + int[] positionCords = new int[2]; + v.getLocationOnScreen(positionCords); + + int startX = positionCords[0] + v.getMeasuredWidth() / 2; + int startY = positionCords[1] + v.getMeasuredHeight() / 2; + + boolean hasReaction = state.message.getMessageReactions().hasReaction(reaction.type); + if (Config.DISABLE_ANONYMOUS_NON_OWNER_REACTIONS && !hasReaction && tdlib.isAnonymousAdminNonCreator(state.message.getChatId())) { + tooltipManager().builder(v).show(tdlib, R.string.error_ANONYMOUS_REACTIONS_DISABLED).hideDelayed(); + return; + } + if (!Config.PROTECT_ANONYMOUS_REACTIONS || hasReaction || state.message.messagesController().callNonAnonymousProtection(state.message.getId() + reaction.getId(), tooltipManager().builder(v))) { + if (state.message.getMessageReactions().toggleReaction(reaction.type, false, true, handler(v, () -> { + }))) { + state.message.scheduleSetReactionAnimationFromBottomSheet(reaction, new Point(startX, startY)); + } + hidePopupWindow(true); } - hidePopupWindow(true); } } - private Client.ResultHandler handler (View v, Runnable onSuccess) { - return object -> { - switch (object.getConstructor()) { - case TdApi.Ok.CONSTRUCTOR: - tdlib.ui().post(onSuccess); - break; - case TdApi.Error.CONSTRUCTOR: - tdlib.ui().post(() -> onSendError(v, (TdApi.Error) object)); - break; + + /* * */ + + @Override + protected int getTopEdge () { + return Math.max(0, (int) (getPickerTop() - HeaderView.getTopOffset() - HeaderView.getSize(true) * (reactionsPickerController != null ? reactionsPickerController.getTopHeaderVisibility() * reactionsPickerVisibility.getFloatValue() : 0f))); + } + + @Override + public boolean shouldTouchOutside (float x, float y) { + return headerView != null && y < getPickerTop() - HeaderView.getSize(true); + } + + @Override + public boolean onBackgroundTouchDown (PopupLayout popupLayout, MotionEvent e) { + if (reactionsPickerVisibility != null && reactionsPickerVisibility.getValue()) { + hideReactionPicker(); + return true; + } + return false; + } + + /* * */ + + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + if (id == REACTIONS_PICKER_VISIBILITY_ANIMATOR_ID) { + float pickerOffset = Math.min(getOptionItemsHeight(), getTargetHeight()); + invalidatePickerWrapper(); + contentView.setTranslationY(pickerOffset * factor); + if (headerView != null) { + headerView.setAlpha(1f - factor); } - }; + float alpha = factor * (state.isPremium ? 1f: 0f); + reactionsPickerBottomHeaderView.setAlpha(alpha); + reactionsPickerBottomHeaderView.setVisibility(alpha > 0f ? View.VISIBLE: View.GONE); + checkReactionPickerPosition(); + } + + if (reactionsPickerBottomHeaderView != null) { + float pickerOffset = Math.min(getOptionItemsHeight(), getTargetHeight()); + reactionsPickerBottomHeaderView.setTranslationY(-pickerOffset * (1f - reactionsPickerVisibility.getFloatValue()) - keyboardHeight.getFactor()); + } } - private void onSendError (View v, TdApi.Error error) { - context().tooltipManager().builder(v).show(tdlib, TD.toErrorString(error)).hideDelayed(3500, TimeUnit.MILLISECONDS); - message.cancelScheduledSetReactionAnimation(); + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + if (id == REACTIONS_PICKER_VISIBILITY_ANIMATOR_ID) { + if (finalFactor == 1f) { + contentView.setVisibility(View.GONE); + if (headerView != null) { + headerView.setVisibility(View.GONE); + } + } + reactionsPickerScrollListener.reset(false); + doNotUpdateScrollReactionPicker = false; + checkReactionPickerHeaderTopVisibility(); + if (headerView != null) { + headerView.setTranslationY(headerTranslationY); + } + } + } + + + + /* Picker layout */ + + private float getPickerTop () { + if (reactionsPickerRecyclerView == null || reactionsPickerWrapper == null || reactionsPickerVisibility == null) { + return headerTranslationY; + } + + float top = Views.getRecyclerViewElementTop(reactionsPickerRecyclerView, 1) + reactionsPickerRecyclerView.getTranslationY() - getPickerTopPadding(); + return MathUtils.fromTo(Math.min(top, headerTranslationY), top, reactionsPickerVisibility.getFloatValue()); + } + + private float getPickerBottom () { + if (reactionsPickerRecyclerView == null || reactionsPickerWrapper == null || reactionsPickerVisibility == null) { + return headerTranslationY + getHeaderHeight(); + } + + return MathUtils.fromTo(headerTranslationY + getHeaderHeight(), reactionsPickerWrapper.getMeasuredHeight(), reactionsPickerVisibility.getFloatValue()); + } + + public static float getPickerTopPadding () { + return Screen.dp(4.5f); // ((getHeaderHeight() - Screen.dp(45)) / 2f); + } + + private int lastReactionPickerOffsetReal; + + private int getReactionPickerOffsetTopReal () { + int offset = Math.max(0, getContentOffset() - HeaderView.getSize(false) - EmojiLayout.getHeaderPadding() + ((getHeaderHeight() - reactionsPickerController.getItemHeight()) / 2)); + if (offset != lastReactionPickerOffsetReal) { + lastReactionPickerOffsetReal = offset; + if (reactionsPickerRecyclerView != null) { + if (!reactionsPickerRecyclerView.isComputingLayout()) { + reactionsPickerRecyclerView.invalidateItemDecorations(); + checkReactionPickerPosition(); + } else { + UI.post(() -> { + reactionsPickerRecyclerView.invalidateItemDecorations(); + checkReactionPickerPosition(); + }); + } + } + } + return offset; + } + + private static int getReactionPickerOffsetTopDefault () { + return (Screen.currentHeight() - Screen.dp(56) - HeaderView.getSize(true)) / 2; + } + + private void checkReactionPickerHeaderTopVisibility () { + if (reactionsPickerController != null) { + reactionsPickerController.setTopHeaderVisibility(reactionsPickerVisibility.getValue() && Views.getRecyclerViewElementTop(reactionsPickerRecyclerView, 1) <= HeaderView.getSize(true) + EmojiLayout.getHeaderPadding()); + } + } + + public static float getReactionsPickerRightHiddenWidth (State state) { + int buttonsWidth = state.getRightViewsWidth(); + + int emojiPickerWidthWithoutPadding = Screen.currentWidth() - Screen.dp(ReactionsPickerController.RECYCLER_VIEW_LEFT_RIGHT_PADDING * 2); + int emojiPickerSpanCount = EmojiLayoutRecyclerController.calculateSpanCount(emojiPickerWidthWithoutPadding, 9, Screen.dp(38)); + float emojiPickerItemSize = (float) emojiPickerWidthWithoutPadding / emojiPickerSpanCount; + return (float) Math.ceil(((float) buttonsWidth - Screen.dp(12)) / emojiPickerItemSize) + * emojiPickerItemSize + Screen.dp(ReactionsPickerController.RECYCLER_VIEW_LEFT_RIGHT_PADDING) + Screen.dp(1); + } + + private class PickerOpenerScrollListener extends RecyclerView.OnScrollListener { + private int totalDy; + private boolean ignore; + + @Override + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (ignore || reactionsPickerRecyclerView.isScrollDisabled() || reactionsPickerVisibility.getFloatValue() > 0f) { + return; + } + + totalDy += dy; + if (Math.abs(totalDy) > Screen.dp(30)) { + UI.post(() -> { + doNotUpdateScrollReactionPicker = true; + reset(true); + recyclerView.stopScroll(); + showReactionPicker(); + }); + } + } + + @Override + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { + if (newState != RecyclerView.SCROLL_STATE_IDLE || reactionsPickerVisibility.getFloatValue() > 0f) { + return; + } + + ignore = false; + if (totalDy != 0) { + reactionsPickerRecyclerView.smoothScrollBy(0, -totalDy); + reset(true); + } + } + + public void reset (boolean ignoreNextScrolls) { + ignore = ignoreNextScrolls; + totalDy = 0; + } + } + + /* */ + + public static class State { + public final Tdlib tdlib; + public final Options options; + public final TGMessage message; + public final TdApi.MessageReaction[] messageReactions; + public final TdApi.AvailableReaction[] availableReactions; + public final Set chosenReactions; + public final long[] emojiPackIds; + public final boolean isPremium; + + public final boolean needShowMessageOptions; + public final boolean needShowMessageViews; + public final boolean needShowMessageReactionSenders; + public final boolean needShowReactionsPopupPicker; + public final boolean needShowCustomEmojiInsidePicker; + public final boolean hasNonSelectedCustomReactions; + + public final OnReactionClickListener onReactionClickListener; + public final int headerButtonsVisibleWidth; + + public int headerAlwaysVisibleCountersWidth; + + public interface OnReactionClickListener { + void onReactionClick (View v, TGReaction reaction, boolean isLongClick); + } + + public State (TGMessage message, Options options, OnReactionClickListener onReactionClickListener) { + this.message = message; + this.options = options; + this.tdlib = message.tdlib(); + this.emojiPackIds = message.getUniqueEmojiPackIdList(); + this.onReactionClickListener = onReactionClickListener; + this.isPremium = message.tdlib().hasPremium(); + + this.messageReactions = message.getMessageReactions().getReactions(); + this.chosenReactions = message.getMessageReactions().getChosen(); + this.availableReactions = message.getMessageAvailableReactions(); + this.needShowMessageViews = !(!message.canGetViewers() || message.isUnread() || message.noUnread()); + this.needShowMessageOptions = options != null; + + this.needShowReactionsPopupPicker = needShowMessageOptions && message.needShowReactionPopupPicker(); + this.needShowMessageReactionSenders = messageReactions != null && message.canGetAddedReactions() && message.getMessageReactions().getTotalCount() > 0; + + this.headerButtonsVisibleWidth = needShowReactionsPopupPicker ? Screen.dp(56): 0; + this.needShowCustomEmojiInsidePicker = isPremium && message.isCustomEmojiReactionsAvailable(); + + boolean hasNonSelectedCustomReactions = false; + if (availableReactions != null) { + for (TdApi.AvailableReaction reaction : availableReactions) { + if (reaction.type.getConstructor() == TdApi.ReactionTypeCustomEmoji.CONSTRUCTOR && !chosenReactions.contains(TD.makeReactionKey(reaction.type))) { + hasNonSelectedCustomReactions = true; + break; + } + } + } + this.hasNonSelectedCustomReactions = hasNonSelectedCustomReactions; + } + + public int getPagesCount () { + return (needShowMessageOptions ? 1 : 0) + + (needShowMessageViews ? 1 : 0) + + (needShowMessageReactionSenders ? messageReactions.length + 1 : 0); + } + + public int getRightViewsWidth () { + return headerAlwaysVisibleCountersWidth + headerButtonsVisibleWidth; + } } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsReactedController.java b/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsReactedController.java index 52149f77a0..77db6b4431 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsReactedController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/MessageOptionsReactedController.java @@ -13,25 +13,27 @@ import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; -import org.thunderdog.challegram.data.TGReaction; import org.thunderdog.challegram.data.TGUser; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.util.ReactionModifier; +import org.thunderdog.challegram.util.StringList; import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.widget.ListInfoView; import org.thunderdog.challegram.widget.PopupLayout; import java.util.List; +import me.vkryl.td.Td; + public class MessageOptionsReactedController extends BottomSheetViewController.BottomSheetBaseRecyclerViewController implements View.OnClickListener { private SettingsAdapter adapter; - private PopupLayout popupLayout; - private TGMessage message; + private final PopupLayout popupLayout; + private final TGMessage message; @Nullable - private TdApi.ReactionType reactionType; + private final TdApi.ReactionType reactionType; private String offset = ""; private boolean canLoadMore = true; @@ -51,7 +53,6 @@ protected void onCreateView (Context context, CustomRecyclerView recyclerView) { adapter = new SettingsAdapter(this) { @Override protected void setUser (ListItem item, int position, UserView userView, boolean isUpdate) { - final TGReaction reactionObj = tdlib.getReaction(TD.toReactionType(item.getStringValue())); TdApi.MessageSender senderId = (TdApi.MessageSender) item.getData(); TGUser user; if (senderId.getConstructor() == TdApi.MessageSenderUser.CONSTRUCTOR) { @@ -61,8 +62,8 @@ protected void setUser (ListItem item, int position, UserView userView, boolean } user.setActionDateStatus(item.getIntValue(), R.string.reacted); userView.setUser(user); - if (item.getStringValue().length() > 0 && reactionObj != null && reactionType == null) { - userView.setDrawModifier(new ReactionModifier(userView.getComplexReceiver(), 8, reactionObj)); + if (item.getSliderValues() != null && item.getSliderValues().length > 0 && reactionType == null) { + userView.setDrawModifier(new ReactionModifier(tdlib, item.getSliderValues()).setMode(ReactionModifier.MODE_INLINE).setOffset(8).requestFiles(userView.getComplexReceiver())); } else { userView.setDrawModifier(null); } @@ -110,16 +111,34 @@ private void loadMore () { private void processNewAddedReactions (TdApi.AddedReactions addedReactions) { final TdApi.AddedReaction[] reactions = addedReactions.reactions; - List items = adapter.getItems(); + final List items = adapter.getItems(); + final int itemsCount = items.size(); + + StringList reactionKeys = new StringList(3); + TdApi.AddedReaction lastReaction = null; for (TdApi.AddedReaction reaction : reactions) { + if (lastReaction != null && !Td.equalsTo(reaction.senderId, lastReaction.senderId)) { + if (!items.isEmpty()) { + items.add(new ListItem(ListItem.TYPE_SEPARATOR)); + } + ListItem item = new ListItem(ListItem.TYPE_USER_SMALL, R.id.sender) + .setData(lastReaction.senderId) + .setIntValue(lastReaction.date) + .setSliderInfo(reactionKeys.get(), 0); + items.add(item); + reactionKeys.clear(); + } + lastReaction = reaction; + reactionKeys.append(TD.makeReactionKey(lastReaction.type)); + } + if (lastReaction != null) { if (!items.isEmpty()) { items.add(new ListItem(ListItem.TYPE_SEPARATOR)); } - ListItem item = new ListItem(ListItem.TYPE_USER_SMALL, R.id.sender) - .setData(reaction.senderId) - .setIntValue(reaction.date) - .setStringValue(TD.makeReactionKey(reaction.type)); + .setData(lastReaction.senderId) + .setIntValue(lastReaction.date) + .setSliderInfo(reactionKeys.get(), 0); items.add(item); } @@ -128,7 +147,7 @@ private void processNewAddedReactions (TdApi.AddedReactions addedReactions) { items.add(new ListItem(ListItem.TYPE_LIST_INFO_VIEW)); } - adapter.notifyAllStringsChanged(); + adapter.notifyItemRangeChanged(itemsCount, items.size() - itemsCount); } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/ui/MessageStatisticsController.java b/app/src/main/java/org/thunderdog/challegram/ui/MessageStatisticsController.java index d93608ce99..4d245c7487 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/MessageStatisticsController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/MessageStatisticsController.java @@ -31,7 +31,9 @@ import org.thunderdog.challegram.data.TGUser; import org.thunderdog.challegram.support.RippleSupport; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; import org.thunderdog.challegram.telegram.TdlibUi; +import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Strings; @@ -69,7 +71,7 @@ public MessageStatisticsController (Context context, Tdlib tdlib) { } private SettingsAdapter adapter; - private TdApi.FoundMessages publicShares; + private TdApi.PublicForwards publicForwards; @Override public CharSequence getName () { @@ -94,8 +96,11 @@ public void onClick (View v) { } } + private TdlibMessageViewer.Viewport messageViewport; + @Override protected void onCreateView (Context context, CustomRecyclerView recyclerView) { + messageViewport = tdlib.messageViewer().createViewport(new TdApi.MessageSourceSearch(), this); adapter = new SettingsAdapter(this) { @Override protected void setSeparatorOptions (ListItem item, int position, SeparatorView separatorView) { @@ -116,7 +121,7 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda itemId == R.id.btn_statsPublicShares) { view.setIgnoreEnabled(true); view.setEnabled(false); - view.setTextColorId(0); + view.setTextColorId(ColorId.NONE); if (item.getData() instanceof String) { view.setName(item.getData().toString()); } else { @@ -162,39 +167,44 @@ protected void setMessagePreview (ListItem item, int position, MessagePreviewVie if (message.interactionInfo.forwardCount > 0) { statString.append(", ").append(Lang.plural(R.string.StatsXShared, message.interactionInfo.forwardCount)); } - previewView.setMessage(message, null, statString.toString(), true); + previewView.setMessage(message, null, statString.toString(), MessagePreviewView.Options.IGNORE_ALBUM_REFRESHERS); } else { - previewView.setMessage(message, null, null, false); + previewView.setMessage(message, null, null, MessagePreviewView.Options.NONE); } RippleSupport.setSimpleWhiteBackground(previewView); previewView.setContentInset(Screen.dp(8)); } }; + tdlib.ui().attachViewportToRecyclerView(messageViewport, recyclerView); recyclerView.setAdapter(adapter); if (getArgumentsStrict().album != null) { setAlbum(getArgumentsStrict().album); } else { - tdlib.client().send(new TdApi.GetMessageStatistics(getArgumentsStrict().chatId, getArgumentsStrict().message.id, Theme.isDark()), result -> { - switch (result.getConstructor()) { - case TdApi.MessageStatistics.CONSTRUCTOR: - tdlib.client().send(new TdApi.GetMessagePublicForwards(getArgumentsStrict().chatId, getArgumentsStrict().message.id, "", 20), result2 -> { - if (result2.getConstructor() == TdApi.FoundMessages.CONSTRUCTOR) { - publicShares = (TdApi.FoundMessages) result2; - } - - runOnUiThreadOptional(() -> { - setStatistics((TdApi.MessageStatistics) result); - }); - }); - break; - case TdApi.Error.CONSTRUCTOR: - UI.showError(result); - break; + long chatId = getArgumentsStrict().chatId; + long messageId = getArgumentsStrict().message.id; + tdlib.send(new TdApi.GetMessageStatistics(chatId, messageId, Theme.isDark()), (messageStatistics, error) -> runOnUiThreadOptional(() -> { + if (error != null) { + UI.showError(error); + } else { + tdlib.send(new TdApi.GetMessagePublicForwards(chatId, messageId, null, 20), (foundMessages, error1) -> runOnUiThreadOptional(() -> { + if (foundMessages != null) { + publicForwards = foundMessages; + } + setStatistics(messageStatistics); + })); } - }); + })); + } + } + + @Override + public void destroy () { + super.destroy(); + if (messageViewport != null) { + messageViewport.performDestroy(); } } @@ -231,8 +241,8 @@ private void setStatistics (TdApi.MessageStatistics statistics) { this.statistics = statistics; int privateShareCount = getArgumentsStrict().message.interactionInfo.forwardCount; - if (publicShares != null) { - privateShareCount -= publicShares.totalCount; + if (publicForwards != null) { + privateShareCount -= publicForwards.totalCount; } TdApi.Message message = getArgumentsStrict().message; @@ -268,9 +278,23 @@ private void setStatistics (TdApi.MessageStatistics statistics) { } private void setPublicShares () { - if (publicShares == null || publicShares.messages.length == 0) return; + TdApi.Message[] publicMessages; + if (publicForwards != null) { + // TODO support stories + List messages = new ArrayList<>(); + for (TdApi.PublicForward publicForward : publicForwards.forwards) { + if (publicForward.getConstructor() == TdApi.PublicForwardMessage.CONSTRUCTOR) { + TdApi.PublicForwardMessage message = (TdApi.PublicForwardMessage) publicForward; + messages.add(message.message); + } + } + publicMessages = messages.toArray(new TdApi.Message[0]); + } else { + publicMessages = null; + } + if (publicMessages == null || publicMessages.length == 0) return; final int index = adapter.indexOfViewById(R.id.btn_statsPrivateShares) + 1; - adapter.getItems().add(index, new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_statsPublicShares, 0, R.string.StatsMessageSharesPublic, false).setData(publicShares.totalCount)); + adapter.getItems().add(index, new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_statsPublicShares, 0, R.string.StatsMessageSharesPublic, false).setData(publicForwards.totalCount)); adapter.getItems().add(index, new ListItem(ListItem.TYPE_SEPARATOR_FULL)); adapter.notifyItemRangeInserted(index, 2); @@ -278,10 +302,11 @@ private void setPublicShares () { adapter.getItems().add(new ListItem(ListItem.TYPE_CHART_HEADER_DETACHED).setData(new MiniChart(R.string.StatsMessageSharesPublic, null))); adapter.getItems().add(new ListItem(ListItem.TYPE_SHADOW_TOP)); - for (int i = 0; i < publicShares.messages.length; i++) { - adapter.getItems().add(new ListItem(ListItem.TYPE_USER, R.id.chat).setData(publicShares.messages[i])); - if (i != publicShares.messages.length - 1) + for (int i = 0; i < publicMessages.length; i++) { + if (i != 0) { adapter.getItems().add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + } + adapter.getItems().add(new ListItem(ListItem.TYPE_USER, R.id.chat).setData(publicMessages[i])); } adapter.getItems().add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java b/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java index 75f5a73cb6..56b3564d54 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java @@ -14,9 +14,6 @@ */ package org.thunderdog.challegram.ui; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.app.Activity; import android.app.AlertDialog; import android.app.DatePickerDialog; @@ -27,8 +24,6 @@ import android.content.res.Configuration; import android.graphics.BitmapFactory; import android.graphics.Canvas; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.location.Location; import android.location.LocationManager; @@ -41,6 +36,7 @@ import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.text.style.ClickableSpan; import android.util.SparseIntArray; import android.util.TypedValue; import android.view.Gravity; @@ -116,9 +112,10 @@ import org.thunderdog.challegram.component.chat.MessagesSearchManagerMiddleware; import org.thunderdog.challegram.component.chat.PinnedMessagesBar; import org.thunderdog.challegram.component.chat.RaiseHelper; -import org.thunderdog.challegram.component.chat.ReplyView; +import org.thunderdog.challegram.component.chat.ReplyBarView; import org.thunderdog.challegram.component.chat.SilentButton; import org.thunderdog.challegram.component.chat.StickerSuggestionAdapter; +import org.thunderdog.challegram.component.chat.TdlibSingleUnreadReactionsManager; import org.thunderdog.challegram.component.chat.TopBarView; import org.thunderdog.challegram.component.chat.VoiceInputView; import org.thunderdog.challegram.component.chat.VoiceVideoButtonView; @@ -133,9 +130,11 @@ import org.thunderdog.challegram.core.Background; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.core.Media; +import org.thunderdog.challegram.data.ContentPreview; +import org.thunderdog.challegram.data.InlineResult; import org.thunderdog.challegram.data.InlineResultButton; import org.thunderdog.challegram.data.InlineResultCommand; -import org.thunderdog.challegram.data.MessageListManager; +import org.thunderdog.challegram.data.InlineResultSticker; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGAudio; import org.thunderdog.challegram.data.TGBotStart; @@ -151,6 +150,8 @@ import org.thunderdog.challegram.filegen.PhotoGenerationInfo; import org.thunderdog.challegram.filegen.VideoGenerationInfo; import org.thunderdog.challegram.helper.BotHelper; +import org.thunderdog.challegram.helper.FoundUrls; +import org.thunderdog.challegram.helper.LinkPreview; import org.thunderdog.challegram.helper.LiveLocationHelper; import org.thunderdog.challegram.helper.Recorder; import org.thunderdog.challegram.loader.ImageFile; @@ -192,6 +193,7 @@ import org.thunderdog.challegram.telegram.EmojiMediaType; import org.thunderdog.challegram.telegram.GlobalAccountListener; import org.thunderdog.challegram.telegram.ListManager; +import org.thunderdog.challegram.telegram.MessageListManager; import org.thunderdog.challegram.telegram.MessageThreadListener; import org.thunderdog.challegram.telegram.NotificationSettingsListener; import org.thunderdog.challegram.telegram.RightId; @@ -225,6 +227,7 @@ import org.thunderdog.challegram.util.SenderPickerDelegate; import org.thunderdog.challegram.util.StringList; import org.thunderdog.challegram.util.Unlockable; +import org.thunderdog.challegram.util.text.TextColorSets; import org.thunderdog.challegram.v.HeaderEditText; import org.thunderdog.challegram.v.MessagesLayoutManager; import org.thunderdog.challegram.v.MessagesRecyclerView; @@ -234,6 +237,7 @@ import org.thunderdog.challegram.widget.CollapseListView; import org.thunderdog.challegram.widget.CustomTextView; import org.thunderdog.challegram.widget.EmojiLayout; +import org.thunderdog.challegram.widget.EmojiPacksInfoView; import org.thunderdog.challegram.widget.ForceTouchView; import org.thunderdog.challegram.widget.NoScrollTextView; import org.thunderdog.challegram.widget.PopupLayout; @@ -252,15 +256,17 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import me.vkryl.android.AnimatorUtils; import me.vkryl.android.animator.BoolAnimator; import me.vkryl.android.animator.FactorAnimator; -import me.vkryl.android.widget.AnimatedFrameLayout; +import me.vkryl.android.util.ClickHelper; import me.vkryl.android.widget.FrameLayoutFix; import me.vkryl.core.ArrayUtils; import me.vkryl.core.BitwiseUtils; @@ -271,7 +277,6 @@ import me.vkryl.core.collection.LongSet; import me.vkryl.core.lambda.CancellableRunnable; import me.vkryl.core.lambda.Future; -import me.vkryl.core.lambda.FutureLong; import me.vkryl.core.lambda.RunnableBool; import me.vkryl.core.lambda.RunnableData; import me.vkryl.td.ChatId; @@ -282,10 +287,10 @@ public class MessagesController extends ViewController implements Menu, Unlockable, View.OnClickListener, ActivityResultHandler, MoreDelegate, CommandKeyboardLayout.Callback, MediaCollectorDelegate, SelectDelegate, - ReplyView.Callback, RaiseHelper.Listener, VoiceInputView.Callback, + ReplyBarView.Callback, RaiseHelper.Listener, VoiceInputView.Callback, TGLegacyManager.EmojiLoadListener, ChatHeaderView.Callback, ChatListener, NotificationSettingsListener, EmojiLayout.Listener, - MessageThreadListener, + MessageThreadListener, TdlibSingleUnreadReactionsManager.UnreadSingleReactionListener, TdlibCache.SupergroupDataChangeListener, TdlibCache.BasicGroupDataChangeListener, TdlibCache.SecretChatDataChangeListener, TdlibCache.UserDataChangeListener, TdlibCache.UserStatusChangeListener, @@ -311,7 +316,6 @@ public void setDestroyInstance () { } private int flags; - private static final int FLAG_REPLY_ANIMATING = 0x01; private @Nullable TdApi.Chat chat; private @Nullable TdApi.ChatList openedFromChatList; @@ -327,6 +331,17 @@ public void setDestroyInstance () { private BotHelper botHelper; private @Nullable InputView inputView; + private final ClickHelper inputViewDisabledClickHelper = new ClickHelper(new ClickHelper.Delegate() { + @Override + public boolean needClickAt (View view, float x, float y) { + return !hasSendBasicMessagePermission(); + } + + @Override + public void onClickAt (View view, float x, float y) { + context().tooltipManager().builder(view).show(tdlib, R.string.MessageInputTextDisabledHint).hideDelayed(); + } + }); private SeparatorView bottomShadowView; private boolean enableOnResume; @@ -368,9 +383,9 @@ public List onCreateHapticMenu (View view) { if (!canSendWithoutMarkdown && tdlib.shouldSendAsDice(currentText) && !isEditingMessage()) { if (items == null) items = new ArrayList<>(); - if (TD.EMOJI_DART.textRepresentation.equals(currentText.text)) { + if (ContentPreview.EMOJI_DART.textRepresentation.equals(currentText.text)) { items.add(new HapticMenuHelper.MenuItem(R.id.btn_sendNoMarkdown, Lang.getString(R.string.SendDiceAsEmoji), R.drawable.baseline_gps_fixed_24)); - } else if (TD.EMOJI_DICE.textRepresentation.equals(currentText.text)) { + } else if (ContentPreview.EMOJI_DICE.textRepresentation.equals(currentText.text)) { items.add(new HapticMenuHelper.MenuItem(R.id.btn_sendNoMarkdown, Lang.getString(R.string.SendDiceAsEmoji), R.drawable.baseline_casino_24)); } else { items.add(new HapticMenuHelper.MenuItem(R.id.btn_sendNoMarkdown, Lang.getString(R.string.SendDiceAsEmoji), Drawables.emojiDrawable(currentText.text))); @@ -514,14 +529,11 @@ private void updateSearchSubtitle () { int totalCount = manager != null ? manager.getKnownTotalMessageCount() : -1; if (previewSearchFilter != null) { - switch (previewSearchFilter.getConstructor()) { - case TdApi.SearchMessagesFilterPinned.CONSTRUCTOR: { - if (totalCount > 0) { - headerCell.setForcedSubtitle(Lang.pluralBold(R.string.XPinnedMessages, totalCount)); - } else { - headerCell.setForcedSubtitle(Lang.getString(R.string.PinnedMessages)); - } - break; + if (Td.isPinnedFilter(previewSearchFilter)) { + if (totalCount > 0) { + headerCell.setForcedSubtitle(Lang.pluralBold(R.string.XPinnedMessages, totalCount)); + } else { + headerCell.setForcedSubtitle(Lang.getString(R.string.PinnedMessages)); } } return; @@ -698,6 +710,7 @@ protected void onLayout (boolean changed, int left, int top, int right, int bott @Override public boolean onTouchEvent (MotionEvent event) { boolean r = super.onTouchEvent(event); + inputViewDisabledClickHelper.onTouchEvent(this, event); if (textFormattingLayout != null) { textFormattingLayout.onInputViewTouchEvent(event); } @@ -725,15 +738,18 @@ public boolean onTouchEvent (MotionEvent event) { inputView.setSpanChangeListener(this::onInputSpansChanged); } - params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(48f)); - params.addRule(RelativeLayout.ALIGN_TOP, R.id.msg_bottom); + if (!inPreviewMode) { + params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(48f)); + params.addRule(RelativeLayout.ALIGN_TOP, R.id.msg_bottom); - replyView = new ReplyView(context(), tdlib); - ViewSupport.setThemedBackground(replyView, ColorId.filling, this); - replyView.setId(R.id.msg_bottomReply); - replyView.initWithCallback(this, this); - replyView.setOnClickListener(this); - replyView.setLayoutParams(params); + replyBarView = new ReplyBarView(context(), tdlib); + ViewSupport.setThemedBackground(replyBarView, ColorId.filling, this); + replyBarView.setId(R.id.msg_bottomReply); + replyBarView.setAnimationsDisabled(true); + replyBarView.initWithCallback(this, this); + replyBarView.setOnClickListener(this); + replyBarView.setLayoutParams(params); + } params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); params.addRule(RelativeLayout.ALIGN_PARENT_TOP); @@ -802,7 +818,7 @@ public View getValue () { toastAlertView.setPadding(Screen.dp(16f), Screen.dp(8f), Screen.dp(16f), Screen.dp(8f)); toastAlertView.setHeightChangeListener((v, newHeight) -> topBar.notifyItemHeightChanged(toastAlertItem)); - pinnedMessagesBar = new PinnedMessagesBar(context) { + pinnedMessagesBar = new PinnedMessagesBar(context, true) { @Override protected void onViewportChanged () { super.onViewportChanged(); @@ -813,7 +829,7 @@ protected void onViewportChanged () { pinnedMessagesBar.initialize(this); pinnedMessagesBar.setMessageListener(new PinnedMessagesBar.MessageListener() { @Override - public void onMessageClick (PinnedMessagesBar view, TdApi.Message message) { + public void onMessageClick (PinnedMessagesBar view, TdApi.Message message, TdApi.InputTextQuote quote) { highlightMessage(new MessageId(message.chatId, message.id)); } @@ -946,7 +962,7 @@ public void onCompletelyHidden () { fparams = FrameLayoutFix.newParams(Screen.dp(24f) * 2 + padding * 2, Screen.dp(24f) * 2 + padding * 2, Gravity.RIGHT | Gravity.BOTTOM); params.rightMargin = params.bottomMargin = Screen.dp(16f) - padding; - reactionsButton = new CircleButton(context()); + reactionsButton = new CircleButton(context(), tdlib); reactionsButton.setId(R.id.btn_reaction); reactionsButton.setOnClickListener(this); addThemeInvalidateListener(reactionsButton); @@ -1162,6 +1178,7 @@ public void onClick () { } sendButton = new SendButton(context, areScheduled ? R.drawable.baseline_schedule_24 : R.drawable.deproko_baseline_send_24); + sendButton.setIgnoreDrawMessageSender(); sendButton.setOnClickListener(this); addThemeInvalidateListener(sendButton); sendButton.setId(R.id.msg_send); @@ -1362,7 +1379,7 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { contentView.addView(wallpaperViewBlurPreview); } if (!inPreviewMode) { - contentView.addView(replyView); + contentView.addView(replyBarView); } contentView.addView(bottomWrap); contentView.addView(messagesView); @@ -1910,8 +1927,8 @@ public void onClick (View v) { } else if (viewId == R.id.btn_viewScheduled) { viewScheduledMessages(false); } else if (viewId == R.id.msg_bottomReply) { - if (replyMessage != null) { - highlightMessage(new MessageId(replyMessage.chatId, replyMessage.id)); + if (reply != null) { + highlightMessage(reply.toMessageId()); } } else if (viewId == R.id.btn_mute) { if (chat != null) { @@ -1934,14 +1951,14 @@ public void onClick (View v) { ((TdApi.BackgroundTypeWallpaper) newBackgroundType).isBlurred = backgroundParamsView.isBlurred(); } - tdlib().client().send(new TdApi.SetBackground( + tdlib().send(new TdApi.SetDefaultBackground( new TdApi.InputBackgroundRemote(getArgumentsStrict().wallpaperObject.id), newBackgroundType, Theme.isDark() - ), result -> { - if (result.getConstructor() == TdApi.Background.CONSTRUCTOR) { + ), (result, error) -> { + if (result != null) { runOnUiThread(() -> { - TGBackground bg = new TGBackground(tdlib(), (TdApi.Background) result); + TGBackground bg = new TGBackground(tdlib(), result); tdlib.wallpaper().addBackground(bg, Theme.isDark()); tdlib.settings().setWallpaper(bg, true, Theme.getWallpaperIdentifier()); navigateBack(); @@ -1968,7 +1985,9 @@ public void onClick (View v) { openMediaView(false, false); } else if (viewId == R.id.msg_send) { if (!leaveInlineMode()) { - if (isEditingMessage()) { + if (inputView != null && !isSelfChat() && !tdlib.hasPremium() && inputView.hasOnlyPremiumFeatures()) { + showBottomHint(Strings.buildMarkdown(this, Lang.getString(R.string.MessageContainsPremiumFeatures), null), false); + } else if (isEditingMessage()) { saveMessage(true); } else if (areScheduled) { tdlib.ui().showScheduleOptions(this, getChatId(), false, (modifiedSendOptions, disableMarkdown) -> send(modifiedSendOptions), null, null); @@ -2071,8 +2090,8 @@ public void onMoreItemPressed (int id) { manager.rebuildLayouts(); } else if (id == R.id.btn_botHelp || id == R.id.btn_botSettings) { if (chat != null) { - if (tdlib.chatBlocked(chat.id)) { - tdlib.blockSender(tdlib.sender(chat.id), false, tdlib.okHandler()); + if (tdlib.chatFullyBlocked(chat.id)) { + tdlib.unblockSender(tdlib.sender(chat.id), tdlib.okHandler()); } if (actionMode == ACTION_BOT_START) { hideActionButton(); @@ -2255,7 +2274,7 @@ public boolean saveInstanceState (Bundle outState, String keyPrefix) { if (args.messageThread != null) { args.messageThread.saveTo(outState, keyPrefix + "thread"); } - TD.saveFilter(outState, keyPrefix + "filter_", args.searchFilter); + Td.put(outState, keyPrefix + "filter_", args.searchFilter); if (args.constructor == 1 || args.constructor == 4) { outState.putInt(keyPrefix + "mode", args.highlightMode); if (args.highlightMessageId != null) { @@ -2268,7 +2287,7 @@ public boolean saveInstanceState (Bundle outState, String keyPrefix) { } if (args.constructor == 3 || args.constructor == 4) { outState.putString(keyPrefix + "query", args.searchQuery); - TD.saveSender(outState, keyPrefix + "sender_", args.searchSender); + Td.put(outState, keyPrefix + "sender_", args.searchSender); } outState.putBoolean(keyPrefix + "scheduled", args.areScheduled); return true; @@ -2287,7 +2306,7 @@ public boolean restoreInstanceState (Bundle in, String keyPrefix) { ThreadInfo messageThread = ThreadInfo.restoreFrom(tdlib, in, keyPrefix + "thread"); if (messageThread == ThreadInfo.INVALID) return false; - TdApi.SearchMessagesFilter filter = TD.restoreFilter(in, keyPrefix + "filter_"); + TdApi.SearchMessagesFilter filter = Td.restoreSearchMessagesFilter(in, keyPrefix + "filter_"); Arguments args = null; switch (constructor) { case 0: { @@ -2308,13 +2327,13 @@ public boolean restoreInstanceState (Bundle in, String keyPrefix) { } case 3: { String query = in.getString(keyPrefix + "query", null); - TdApi.MessageSender sender = TD.restoreSender(in, keyPrefix + "sender_"); + TdApi.MessageSender sender = Td.restoreMessageSender(in, keyPrefix + "sender_"); args = new Arguments(chatList, chat, query, sender, filter); break; } case 4: { String query = in.getString(keyPrefix + "query", null); - TdApi.MessageSender sender = TD.restoreSender(in, keyPrefix + "sender_"); + TdApi.MessageSender sender = Td.restoreMessageSender(in, keyPrefix + "sender_"); int highlightMode = in.getInt(keyPrefix + "mode", 0); long highlightMessageId = in.getLong(keyPrefix + "message_id", 0); long highlightMessageChatId = in.getLong(keyPrefix + "message_chat_id", 0); @@ -2580,16 +2599,18 @@ private void shareItem () { } public void shareItem (Object item) { - if (!hasWritePermission()) { // FIXME right - return; - } - if (item instanceof InlineResultButton) { + if (!hasSendBasicMessagePermission()) { + return; + } processSwitchPm((InlineResultButton) item); return; } if (item instanceof TGSwitchInline) { + if (!hasSendBasicMessagePermission()) { + return; + } if (inputView != null) { inputView.setInput(item.toString(), true, false); } @@ -2607,6 +2628,9 @@ public void shareItem (Object item) { } if (item instanceof TGRecord) { + if (!hasSendMessagePermission(RightId.SEND_VOICE_NOTES)) { + return; + } processRecord((TGRecord) item); return; } @@ -2615,7 +2639,7 @@ public void shareItem (Object item) { TGBotStart start = (TGBotStart) item; if (start.isGame()) { - tdlib.sendMessage(chat.id, getMessageThreadId(), 0, Td.newSendOptions(obtainSilentMode()), new TdApi.InputMessageGame(start.getUserId(), start.getArgument())); + tdlib.sendMessage(chat.id, getMessageThreadId(), null, Td.newSendOptions(obtainSilentMode()), new TdApi.InputMessageGame(start.getUserId(), start.getArgument())); } else if (start.useDeepLinking()) { if (ChatId.isUserChat(chat.id)) { showActionBotButton(start.getArgument()); @@ -2658,6 +2682,13 @@ private void updateView () { clearSelectedMessageIds(); } + if (sendButton != null) { + sendButton.getSlowModeCounterController(tdlib).setCurrentChat(getChatId()); + sendButton.getSlowModeCounterController(tdlib).setSlowModeCounterUpdateListener(this::onSlowModeCounterUpdate); + } + if (messageSenderButton != null) { + messageSenderButton.setInSlowMode(tdlib.inSlowMode(getChatId())); + } clearSwitchPmButton(); clearReply(); @@ -2733,9 +2764,10 @@ private void updateView () { if (tdlib.canSendBasicMessage(chat)) { TdApi.DraftMessage draftMessage = getDraftMessage(); - if (draftMessage != null && draftMessage.replyToMessageId != 0) { + if (draftMessage != null && draftMessage.replyTo != null && draftMessage.replyTo.getConstructor() == TdApi.InputMessageReplyToMessage.CONSTRUCTOR) { if (!ignoreDraftLoad) { - forceDraftReply(draftMessage.replyToMessageId); + TdApi.InputMessageReplyToMessage replyToMessage = (TdApi.InputMessageReplyToMessage) draftMessage.replyTo; + forceDraftReply(replyToMessage); } } updateSilentButton(tdlib.isChannel(chat.id)); @@ -2816,6 +2848,10 @@ public void processResult (TdApi.Object object) { } }); + if (emojiLayout != null) { + emojiLayout.setAllowPremiumFeatures(isSelfChat()); + } + updateCounters(false); checkRestriction(); checkLinkedChat(); @@ -2915,6 +2951,7 @@ protected void onFocusStateChanged () { private void checkInlineResults () { context().setInlineResultsHidden(this, !isFocused() || pagerScrollOffset >= 1f); + context().setEmojiSuggestionsVisible(isFocused() && !(pagerScrollOffset >= 1f) && canShowEmojiSuggestions); } public void checkRoundVideo () { @@ -2991,6 +3028,13 @@ public void updateInputHint () { } private void updateBottomBar (boolean isUpdate) { + setInputBlockFlag(FLAG_INPUT_TEXT_DISABLED, !tdlib.canSendBasicMessage(chat)); + if (sendButton != null) { + sendButton.getSlowModeCounterController(tdlib).updateSlowModeTimer(isUpdate); + } + if (messageSenderButton != null) { + messageSenderButton.setInSlowMode(tdlib.inSlowMode(getChatId())); + } if (isUpdate) { updateInputHint(); } @@ -3029,14 +3073,14 @@ private void updateBottomBar (boolean isUpdate) { boolean joinSupergroupToSendMessages = messageThread == null || supergroup != null && supergroup.joinToSendMessages; if (secretChat != null && !TD.isSecretChatReady(secretChat)) { showSecretChatAction(secretChat); - } else if (tdlib.chatBlocked(chat) && tdlib.isUserChat(chat)) { + } else if (tdlib.chatFullyBlocked(chat.id) && tdlib.isUserChat(chat)) { showActionUnblockButton(); } else if (tdlib.chatUserDeleted(chat) || (ChatId.isBasicGroup(chat.id) && (!tdlib.chatBasicGroupActive(chat.id) || TD.isNotInChat(status))) || (tdlib.isSupergroupChat(chat) && TD.isNotInChat(status) && joinSupergroupToSendMessages)) { if (tdlib.isSupergroupChat(chat) && status != null && TD.canReturnToChat(status)) { showActionJoinChatButton(); } else if (messageThread != null) { CharSequence restrictionStatus = tdlib.getBasicMessageRestrictionText(chat); - if (restrictionStatus != null) { + if (restrictionStatus != null && !hasSendSomeMediaPermission()) { showActionButton(restrictionStatus, ACTION_EMPTY, false); } else { hideActionButton(); @@ -3048,7 +3092,7 @@ private void updateBottomBar (boolean isUpdate) { showActionBotButton(); } else { CharSequence restrictionStatus = tdlib.getBasicMessageRestrictionText(chat); - if (restrictionStatus != null) { + if (restrictionStatus != null && !hasSendSomeMediaPermission()) { showActionButton(restrictionStatus, ACTION_EMPTY, false); } else { hideActionButton(); @@ -3080,13 +3124,32 @@ private void showBottomHint (CharSequence text, boolean isError) { .ignoreViewScale(true) .controller(this) .show(tdlib, text); + tooltipInfo.addOnCloseListener(this::onTooltipInfoClose); } else { tooltipInfo.reset(context().tooltipManager().newContent(tdlib, text, 0), isError ? R.drawable.baseline_warning_24 : 0); tooltipInfo.show(); } + isSlowModeRestrictionHintVisible = false; tooltipInfo.hideDelayed(false); } + private boolean isSlowModeRestrictionHintVisible; + + private void onTooltipInfoClose (long duration) { + isSlowModeRestrictionHintVisible = false; + } + + private void onSlowModeCounterUpdate (int duration) { + if (sendButton != null && tooltipInfo != null && tooltipInfo.isVisible() && isSlowModeRestrictionHintVisible) { + CharSequence restriction = tdlib().getSlowModeRestrictionText(getChatId(), null); + if (restriction != null) { + tooltipInfo.reset(context().tooltipManager().newContent(tdlib, restriction, 0), R.drawable.baseline_warning_24); + } else { + tooltipInfo.hideNow(); + } + } + } + @Override public void onPreferVideoModeChanged (boolean preferVideoMode) { if (!sendShown.getValue()) { @@ -3133,7 +3196,9 @@ public void setInputVisible (boolean visible, boolean notEmpty) { if (visible) { bottomWrap.setVisibility(View.VISIBLE); bottomShadowView.setVisibility(View.VISIBLE); - replyView.setVisibility(View.VISIBLE); + if (replyBarView != null) { + replyBarView.setVisibility(View.VISIBLE); + } emojiButton.setVisibility(View.VISIBLE); if (notEmpty) { attachButtons.setVisibility(View.INVISIBLE); @@ -3149,7 +3214,9 @@ public void setInputVisible (boolean visible, boolean notEmpty) { } else { hideActionButton(); bottomWrap.setVisibility(View.GONE); - replyView.setVisibility(View.GONE); + if (replyBarView != null) { + replyBarView.setVisibility(View.GONE); + } bottomShadowView.setVisibility(View.GONE); emojiButton.setVisibility(View.GONE); attachButtons.setVisibility(View.GONE); @@ -3170,8 +3237,8 @@ public void invalidateEmojiViews (boolean force) { emojiScheduled = true; return; } - if (replyView != null) { - replyView.invalidate(); + if (replyBarView != null) { + replyBarView.invalidate(); } if (messagesView != null) { LinearLayoutManager manager = (LinearLayoutManager) messagesView.getLayoutManager(); @@ -3249,18 +3316,9 @@ public boolean canSlideBackFrom (NavigationController navigationController, floa int baseY = Views.getLocationInWindow(navigationController.getValue())[1]; - if (areStickersVisible) { - int locationY = Views.getLocationInWindow(stickerSuggestionsWrap)[1]; - locationY -= baseY; - if (y >= locationY && y < locationY + stickerSuggestionsWrap.getMeasuredHeight()) { - int i = ((LinearLayoutManager) stickerSuggestionsView.getLayoutManager()).findFirstVisibleItemPosition(); - if (i == 0) { - View view = stickerSuggestionsView.getLayoutManager().findViewByPosition(0); - return view == null || view.getLeft() >= 0; - } - return false; - } - } + /*if (areInlineResultsVisible()) { + return false; + }*/ int bound = (getSlideBackBound()); @@ -3555,23 +3613,20 @@ public void onMenuItemPressed (int id, View view) { } if (selectedMessageIds != null) { int size = selectedMessageIds.size(); - TdApi.Message singleMessage = getSingleSelectedMessage(); - if (singleMessage != null) { - shareMessage(singleMessage); - } else if (size > 1) { + if (size > 0) { TdApi.Message[] messages = new TdApi.Message[size]; for (int i = 0; i < size; i++) { long messageId = selectedMessageIds.keyAt(i); TdApi.Message message = selectedMessageIds.valueAt(i).getMessage(messageId); messages[i] = message; } - shareMessages(chat.id, messages); + shareMessages(messages, true); } } } else if (id == R.id.menu_btn_reply) { TdApi.Message m = getSingleSelectedMessage(); if (m != null) { - showReply(m, true, true); + showReply(m, null, true, true); finishSelectMode(-1); if (inputView != null && inputView.isEmpty()) { Keyboard.show(inputView); @@ -3729,7 +3784,7 @@ public void onAccountSwitched (TdlibAccount newAccount, TdApi.User profile, int private static HashSet shownTutorials; private void showMessageMenuTutorial () { - if (sendShown.getValue() && !areScheduledOnly() && !isInputLess() && canWriteMessages() && hasWritePermission() && !isEditingMessage() && !isSecretChat() && isFocused() && !isVoicePreviewShowing() && !sendButton.inInlineMode()) { + if (sendShown.getValue() && !areScheduledOnly() && !isInputLess() && canWriteMessages() && hasSendBasicMessagePermission() && !isEditingMessage() && !isSecretChat() && isFocused() && !isVoicePreviewShowing() && !sendButton.inInlineMode()) { long tutorialFlag; if (isSelfChat()) { tutorialFlag = Settings.TUTORIAL_SET_REMINDER; @@ -3847,6 +3902,7 @@ public void onFocus () { openKeyboard = false; Keyboard.show(inputView); } + // showEmojiSuggestionsIfTemporarilyHidden(); // tdlib.context().changePreferredAccountId(tdlib.id(), TdlibManager.SWITCH_REASON_CHAT_FOCUS); } @@ -3863,7 +3919,7 @@ private void resetOnFocus () { @Override public final boolean shouldDisallowScreenshots () { - return chat != null && (isSecretChat() || chat.hasProtectedContent || manager.hasVisibleProtectedContent()); + return (chat != null && (isSecretChat() || chat.hasProtectedContent || manager.hasVisibleProtectedContent())) || super.shouldDisallowScreenshots(); } @Override @@ -3887,12 +3943,6 @@ public void onPrepareToShow () { } } - public void onChatOpenStateChanged (long chatId) { - if (!isDestroyed() && getChatId() == chatId && manager != null) { - manager.viewMessages(); - } - } - public boolean canSaveDraft () { return canWriteMessages() && getChatId() != 0 && !inPreviewMode() && !isInForceTouchMode(); } @@ -3903,14 +3953,14 @@ private void saveDraft () { // TODO save local draft } else if (inputView != null && inputView.textChangedSinceChatOpened() && isFocused()) { final TdApi.FormattedText outputText = inputView.getOutputText(false); - final long replyToMessageId = getCurrentReplyId(); + final @Nullable TdApi.InputMessageReplyToMessage replyTo = getCurrentReplyId(); final long date = tdlib.currentTime(TimeUnit.SECONDS); final TdApi.InputMessageText inputMessageText = new TdApi.InputMessageText( outputText, - getCurrentAllowLinkPreview(), + findTargetContext().linkPreviewOptions, false ); - final TdApi.DraftMessage draftMessage = new TdApi.DraftMessage(replyToMessageId, (int) date, inputMessageText); + final TdApi.DraftMessage draftMessage = new TdApi.DraftMessage(replyTo, (int) date, inputMessageText); final long outputChatId = messageThread != null ? messageThread.getChatId() : getChatId(); final long messageThreadId = messageThread != null ? messageThread.getMessageThreadId() : 0; if (messageThread != null) { @@ -3957,6 +4007,8 @@ public void onBlur () { if (translationPopup != null) { translationPopup.hidePopupWindow(true); } + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); + // hideEmojiSuggestionsTemporarily(); // closeEmojiKeyboard(); // Media.instance().stopVoice(); } @@ -4105,6 +4157,14 @@ public void destroy () { requestsView.performDestroy(); } + if (reactionsButton != null) { + reactionsButton.performDestroy(); + } + + if (sendButton != null) { + sendButton.destroySlowModeCounterController(); + } + // messagesView.clear(); closeVoicePreview(true); @@ -4125,10 +4185,14 @@ public void destroy () { liveLocation.destroy(); liveLocation = null; } + if (pinnedMessagesBar != null) + pinnedMessagesBar.completeDestroy(); + if (replyBarView != null) + replyBarView.completeDestroy(); if (topBar != null) topBar.performDestroy(); - if (replyView != null) - replyView.performDestroy(); + if (replyBarView != null) + replyBarView.performDestroy(); recordButton.performDestroy(); if (inputView != null) inputView.performDestroy(); @@ -4218,7 +4282,7 @@ public void showMore () { ids.append(R.id.btn_setPasscode); strings.append(R.string.PasscodeTitle); } - tdlib.ui().addDeleteChatOptions(getChatId(), ids, strings, true, false); + tdlib.ui().addDeleteChatOptions(getChatId(), ids, strings, !tdlib.isChannel(chat.id), false); if (!messagesHidden) { if (ChatId.isUserChat(chat.id)) { @@ -4255,7 +4319,7 @@ public void showMore () { ids.append(R.id.btn_sendScreenshotNotification); strings.append("Send screenshot notification"); } - if (!hasWritePermission()) { + if (!hasSendBasicMessagePermission()) { ids.append(R.id.btn_debugShowHideBottomBar); strings.append("Show/hide bottom bar"); } @@ -4311,12 +4375,13 @@ public void showMessageOptions (MessageContext messageContext, int[] ids, String b.append(from); b.append(": "); } - b.append(TD.buildShortPreview(tdlib, msg.getMessage(), true)); + ContentPreview contentPreview = ContentPreview.getChatListPreview(tdlib, msg.getChatId(), msg.getMessage(), true); + b.append(contentPreview.buildText(false)); break; } case TdApi.MessageDice.CONSTRUCTOR: { String emoji = ((TdApi.MessageDice) msg.getMessage().content).emoji; - b.append(Lang.getString(TD.EMOJI_DART.textRepresentation.equals(emoji) ? R.string.SendDartHint : TD.EMOJI_DICE.textRepresentation.equals(emoji) ? R.string.SendDiceHint : R.string.SendUnknownDiceHint, emoji)); + b.append(Lang.getString(ContentPreview.EMOJI_DART.textRepresentation.equals(emoji) ? R.string.SendDartHint : ContentPreview.EMOJI_DICE.textRepresentation.equals(emoji) ? R.string.SendDiceHint : R.string.SendUnknownDiceHint, emoji)); break; } } @@ -4352,6 +4417,15 @@ public void showMessageOptions (MessageContext messageContext, int[] ids, String b.append(Lang.getString(R.string.SendFailureInfo, Strings.join(", ", (Object[]) errors))); } } + if (msg.isSponsoredMessage()) { + String additionalInfo = msg.getSponsoredMessage().additionalInfo; + if (!StringUtils.isEmpty(additionalInfo)) { + if (b.length() > 0) { + b.append('\n'); + } + b.append(additionalInfo); + } + } if (!msg.canBeSaved()) { if (b.length() > 0) { // b.append("\n\n"); @@ -4382,6 +4456,7 @@ public void showMessageOptions (MessageContext messageContext, int[] ids, String } else { PopupLayout popupLayout = showOptions(StringUtils.isEmpty(text) ? null : text, ids, options, null, icons, messageHandler); patchReadReceiptsOptions(popupLayout, messageContext); + patchUsedEmojiPacks(popupLayout, messageContext); } }); }); @@ -4391,12 +4466,87 @@ public void showMessageAddedReactions (TGMessage message, TdApi.ReactionType rea showMessageOptions(null, message, reactionType, newMessageOptionDelegate(message, null, null)); } + private boolean isMessageOptionsVisible; + private void showMessageOptions (Options options, TGMessage message, @Nullable TdApi.ReactionType reactionType, OptionDelegate optionsDelegate) { - MessageOptionsPagerController r = new MessageOptionsPagerController(context, tdlib, options, message, reactionType, optionsDelegate); + if (isMessageOptionsVisible) { + return; + } + isMessageOptionsVisible = true; + + MessageOptionsPagerController r = new MessageOptionsPagerController(context, tdlib, options, message, reactionType, optionsDelegate) { + @Override + protected void onCustomShowComplete () { + super.onCustomShowComplete(); + optimizeEmojiLayoutForOptionsWindow(true); + } + }; r.show(); + r.setDismissListener(new PopupLayout.DismissListener() { + @Override + public void onPopupDismiss (PopupLayout popup) { + optimizeEmojiLayoutForOptionsWindow(false); + isMessageOptionsVisible = false; + } + + @Override + public void onPopupDismissPrepare (PopupLayout popup) { + onHideMessageOptions(); + } + }); + prepareToShowMessageOptions(); hideCursorsForInputView(); } + private boolean needShowKeyboardAfterHideMessageOptions; + private boolean needShowEmojiKeyboardAfterHideMessageOptions; + + private void prepareToShowMessageOptions () { + needShowKeyboardAfterHideMessageOptions = getKeyboardState(); + needShowEmojiKeyboardAfterHideMessageOptions = emojiShown; + if (needShowKeyboardAfterHideMessageOptions) { // показываем emoji-клавиатуру, чтобы скрыть системную + openEmojiKeyboard(); // делаем emojiLayout невидимым для оптимизации + emojiLayout.optimizeForDisplayMessageOptionsWindow(true); + } // todo: если меню сообщения ниже EmojiLayout, то не скрывать? + } + + private void optimizeEmojiLayoutForOptionsWindow (boolean needOptimize) { + if (needShowKeyboardAfterHideMessageOptions || needShowEmojiKeyboardAfterHideMessageOptions) { + emojiLayout.optimizeForDisplayMessageOptionsWindow(needOptimize); + } + } + + private void onHideMessageOptions () { + if (needShowEmojiKeyboardAfterHideMessageOptions) { + openEmojiKeyboard(); + emojiLayout.optimizeForDisplayMessageOptionsWindow(false); + } else if (needShowKeyboardAfterHideMessageOptions) { + showKeyboard(); + } + } + + + private void patchUsedEmojiPacks (PopupLayout layout, MessageContext messageContext) { + TGMessage message = messageContext.message; + long[] emojiPackIds = message.getUniqueEmojiPackIdList(); + if (emojiPackIds.length == 0) { + return; + } + + OptionsLayout optionsLayout = (OptionsLayout) layout.getChildAt(1); + EmojiPacksInfoView emojiPacksInfoView = new EmojiPacksInfoView(layout.getContext(), this, tdlib); + emojiPacksInfoView.update(message.getFirstEmojiId(), emojiPackIds, new ClickableSpan() { + @Override + public void onClick (@NonNull View widget) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); + tdlib.ui().showStickerSets(MessagesController.this, emojiPackIds, true, null); + layout.hideWindow(true); + } + }, false); + optionsLayout.addView(emojiPacksInfoView, 1); + + } + private void patchReadReceiptsOptions (PopupLayout layout, MessageContext messageContext) { TGMessage message = messageContext.message; if (!message.canGetViewers() || messageContext.disableMetadata || (message.isUnread() && !message.noUnread()) || !(layout.getChildAt(1) instanceof OptionsLayout)) { @@ -4695,7 +4845,7 @@ private void copySelectedMessages () { b.append("]"); } } - if (msg.replyToMessageId != 0) { + if (msg.replyTo != null) { String inReply = m.getInReplyTo(); if (!StringUtils.isEmpty(inReply)) { b.append("\n["); @@ -4711,7 +4861,8 @@ private void copySelectedMessages () { TdApi.FormattedText text = Td.textOrCaption(msg.content); if (msg.content.getConstructor() != TdApi.MessageText.CONSTRUCTOR && msg.content.getConstructor() != TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { b.append("\n["); - b.append(TD.buildShortPreview(tdlib, msg, false)); + ContentPreview preview = ContentPreview.getChatListPreview(tdlib, msg.chatId, msg, true); + b.append(preview.buildText(false)); b.append("]"); } //noinspection UnsafeOptInUsageError @@ -5083,7 +5234,7 @@ public boolean isSelfChat () { } @Deprecated - public boolean hasWritePermission () { + private boolean hasWritePermission () { // FIXME: this check is outdated and no longer correct return chat != null && tdlib.canSendBasicMessage(chat) && !isEventLog(); } @@ -5094,12 +5245,32 @@ public boolean canSendPhotosAndVideos () { // FIXME separate photos and videos tdlib.canSendMessage(chat, RightId.SEND_VIDEOS); } + public boolean hasSendMessagePermission (@RightId int rightId) { + return chat != null && tdlib.canSendMessage(chat, rightId) && !isEventLog(); + } + + public boolean hasSendBasicMessagePermission () { + return chat != null && tdlib.canSendBasicMessage(chat) && !isEventLog(); + } + + public boolean hasSendSomeMediaPermission () { + return chat != null && tdlib.canSendSendSomeMedia(chat) && !isEventLog(); + } + // test private OptionDelegate newMessageOptionDelegate (final MessageContext context) { return newMessageOptionDelegate(context.message, context.messageSender, context.tag); } + private void cancelSheduledKeyboardOpeningAndHideAllKeyboards () { + if (needShowKeyboardAfterHideMessageOptions || needShowEmojiKeyboardAfterHideMessageOptions) { + needShowEmojiKeyboardAfterHideMessageOptions = false; + needShowKeyboardAfterHideMessageOptions = false; + hideAllKeyboards(); + } + } + private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage, final TdApi.ChatMember selectedMessageSender, final Object selectedMessageTag) { return (itemView, id) -> { if (id == R.id.btn_cancel) { @@ -5108,7 +5279,11 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage if (selectedMessage == null) { return false; } - if (id == R.id.btn_messageApplyLocalization) { + if (id == R.id.btn_emojiPackInfoButton) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); + tdlib.ui().showStickerSets(MessagesController.this, ((EmojiPacksInfoView) itemView).getEmojiPacksIds(), true, null); + return true; + } else if (id == R.id.btn_messageApplyLocalization) { if (selectedMessage.getMessage().content.getConstructor() == TdApi.MessageDocument.CONSTRUCTOR) { TdApi.Document document = ((TdApi.MessageDocument) selectedMessage.getMessage().content).document; tdlib.ui().readCustomLanguage(this, document, langPack -> tdlib.ui().showLanguageInstallPrompt(this, langPack, selectedMessage.getMessage()), null); @@ -5121,6 +5296,7 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage } return true; } else if (id == R.id.btn_messageSponsorInfo) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); ModernActionedLayout mal = new ModernActionedLayout(this); mal.setController(new SponsoredMessagesInfoController(mal, R.string.SponsoredInfoMenu)); mal.initCustom(); @@ -5152,6 +5328,7 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage tdlib.ui().addContact(this, contact); return true; } else if (id == R.id.btn_messageCallContact) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); TdApi.Contact contact = ((TdApi.MessageContact) selectedMessage.getMessage().content).contact; showCallOptions(contact.phoneNumber, contact.userId); return true; @@ -5159,9 +5336,13 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage tdlib.resendMessages(selectedMessage.getChatId(), selectedMessage.getIds()); return true; } else if (id == R.id.btn_messageSendNow) { - tdlib.client().send(new TdApi.EditMessageSchedulingState(getChatId(), selectedMessage.getId(), null), tdlib.okHandler()); + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); + if (!showRestriction(null, tdlib.getSlowModeRestrictionText(getChatId()))) { + tdlib.client().send(new TdApi.EditMessageSchedulingState(getChatId(), selectedMessage.getId(), null), tdlib.okHandler()); + } return true; } else if (id == R.id.btn_messageReschedule) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); tdlib.ui().showScheduleOptions(this, getChatId(), false, (sendOptions, disableMarkdown) -> { if (sendOptions.schedulingState != null) { tdlib.client().send(new TdApi.EditMessageSchedulingState(getChatId(), selectedMessage.getId(), sendOptions.schedulingState), tdlib.okHandler()); @@ -5178,6 +5359,7 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage tdlib.ui().openMessage(this, chatId, new MessageId(chatId, messageId, otherMessageIds), selectedMessage.openParameters()); return true; } else if (id == R.id.btn_messageShowInChatSearch) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); if (inOnlyFoundMode()) { long chatId = selectedMessage.getChatId(); long messageId = selectedMessage.getSmallestId(); @@ -5187,6 +5369,7 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage } return true; } else if (id == R.id.btn_messageDirections) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); TdApi.MessageContent content = selectedMessage.getMessage().content; switch (content.getConstructor()) { case TdApi.MessageVenue.CONSTRUCTOR: { @@ -5202,28 +5385,34 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage } return true; } else if (id == R.id.btn_messageFoursquare) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); String venueId = ((TdApi.MessageVenue) selectedMessage.getMessage().content).venue.id; tdlib.ui().openUrl(this, "https://foursquare.com/v/" + venueId, selectedMessage.openParameters()); return true; } else if (id == R.id.btn_messageCall) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); tdlib.context().calls().makeCall(this, tdlib.calleeUserId(selectedMessage.getMessage()), null); return true; } else if (id == R.id.btn_messageDelete) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); tdlib.ui().showDeleteOptions(this, selectedMessage.getAllMessages(), null); return true; } else if (id == R.id.btn_messageReport) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); reportChat(selectedMessage.getAllMessages(), null); return true; } else if (id == R.id.btn_messageSelect) { selectAllMessages(selectedMessage, -1, -1); return true; } else if (id == R.id.btn_messageViewList) {//FIXME? + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); foundMessageId = new MessageId(selectedMessage.getMessage().chatId, selectedMessage.getMessage().id); searchFromUserMessageId = foundMessageId; manager.setHighlightMessageId(foundMessageId, MessagesManager.HIGHLIGHT_MODE_NORMAL); viewMessagesFromSender(selectedMessage.getMessage().senderId, true); return true; } else if (id == R.id.btn_messageRestrictMember) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); EditRightsController c = new EditRightsController(context, tdlib); c.setArguments(new EditRightsController.Args(selectedMessage.getChatId(), selectedMessage.getMessage().senderId, true, tdlib.chatStatus(selectedMessage.getChatId()), selectedMessageSender)); navigateTo(c); @@ -5238,6 +5427,7 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage } return true; } else if (id == R.id.btn_messageMore) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); IntList ids = new IntList(3); IntList icons = new IntList(3); StringList strings = new StringList(3); @@ -5263,6 +5453,7 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage } return true; } else if (id == R.id.btn_messageStickerSet) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); ((TGMessageSticker) selectedMessage).openStickerSet(); } else if (id == R.id.btn_messageFavoriteContent || id == R.id.btn_messageUnfavoriteContent) { boolean isFavorite = id == R.id.btn_messageFavoriteContent; @@ -5273,16 +5464,17 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage pinUnpinMessage(selectedMessage, id == R.id.btn_messagePin); return true; } else if (id == R.id.btn_messageReply) { - showReply(selectedMessage.getNewestMessage(), true, true); + showReply(selectedMessage.getNewestMessage(), null, true, true); if (inputView.isEmpty()) { Keyboard.show(inputView); } return true; } else if (id == R.id.btn_messageReplies) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); selectedMessage.openMessageThread(); return true; } else if (id == R.id.btn_messageReplyWithDice) { - sendDice(itemView, ((TdApi.MessageDice) selectedMessage.getMessage().content).emoji, 0); + sendDice(itemView, ((TdApi.MessageDice) selectedMessage.getMessage().content).emoji); return true; } else if (id == R.id.btn_copyTranslation || id == R.id.btn_messageCopy) { if (!selectedMessage.canBeSaved()) { @@ -5313,11 +5505,13 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage editMessage(message); return true; } else if (id == R.id.btn_messageShare) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); if (selectedMessage.canBeForwarded()) { - shareMessages(selectedMessage.getChatId(), selectedMessage.getAllMessages()); + shareMessages(selectedMessage.getAllMessages(), false); } return true; } else if (id == R.id.btn_chatTranslate) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); startTranslateMessages(selectedMessage); return true; } else if (id == R.id.btn_chatTranslateOff) { @@ -5369,6 +5563,7 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage } return true; } else if (id == R.id.btn_viewStatistics) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); TdApi.Message[] messages = selectedMessage.getAllMessages(); MessageStatisticsController statsController = new MessageStatisticsController(context, tdlib); if (messages.length == 1) { @@ -5398,6 +5593,7 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage } return true; } else if (id == R.id.btn_stickerSetInfo) { + cancelSheduledKeyboardOpeningAndHideAllKeyboards(); TdApi.MessageContent content = selectedMessage.getMessage().content; if (content.getConstructor() == TdApi.MessageSticker.CONSTRUCTOR) { TdApi.MessageSticker sticker = (TdApi.MessageSticker) content; @@ -5699,8 +5895,8 @@ private void processChatAction () { hideActionButton(); } }; - if (tdlib.chatBlocked(chat.id)) { - tdlib.blockSender(tdlib.sender(chat.id), false, result -> { + if (tdlib.chatFullyBlocked(chat.id)) { + tdlib.unblockSender(tdlib.sender(chat.id), result -> { if (TD.isOk(result)) { tdlib.ui().post(after); } else { @@ -5728,7 +5924,7 @@ private void processChatAction () { break; } case ACTION_UNBAN_USER: { - tdlib.blockSender(tdlib.sender(chat.id), false, tdlib.okHandler()); + tdlib.unblockSender(tdlib.sender(chat.id), tdlib.okHandler()); break; } case ACTION_EMPTY: { @@ -5862,6 +6058,9 @@ private void setReactionCountBadge (int reactionCount) { reactionsCountView.setCounter(reactionCount, true, animate && reactionButtonFactor > 0f); setReactionButtonVisible(visible, animate); } + if (reactionCount > 0) { + reactionsButton.setUnreadReaction(tdlib.getSingleUnreadReaction(getChatId())); + } } private void setReactionButtonFactor (float factor) { @@ -5927,8 +6126,34 @@ public void highlightMessage (MessageId messageId, long[] returnToMessageIds) { showMessagesListIfNeeded(); } - private TdApi.Message replyMessage; - private ReplyView replyView; + private static class ReplyInfo { + public final Tdlib tdlib; + public final TdApi.Message message; + public final @Nullable TdApi.InputTextQuote quote; + + public ReplyInfo (Tdlib tdlib, TdApi.Message message, @Nullable TdApi.InputTextQuote quote) { + this.tdlib = tdlib; + this.message = message; + this.quote = quote; + } + + public MessageId toMessageId () { + return new MessageId(message.chatId, message.id); + } + + public TdApi.InputMessageReplyToMessage toInputMessageReply (long inChatId, long inMessageThreadId) { + long chatId; + if (inChatId != message.chatId || (message.isTopicMessage && inMessageThreadId != 0 && message.messageThreadId != inMessageThreadId)) { + chatId = message.chatId; + } else { + chatId = 0; + } + return new TdApi.InputMessageReplyToMessage(chatId, message.id, quote); + } + } + + private ReplyInfo reply; + private @Nullable ReplyBarView replyBarView; private CollapseListView topBar; private TopBarView actionView; @@ -5943,59 +6168,41 @@ public void highlightMessage (MessageId messageId, long[] returnToMessageIds) { private JoinRequestsView requestsView; private CollapseListView.Item requestsItem; - @Override - public void onCloseReply (ReplyView view) { - if (showingLinkPreview()) { - closeLinkPreview(); - } else if (isEditingMessage()) { - closeEdit(); - } else { - closeReply(true); + public @Nullable TdApi.InputMessageReplyToMessage getCurrentReplyId () { + if (reply != null) { + return reply.toInputMessageReply(messageThread != null ? messageThread.getChatId() : getChatId(), getMessageThreadId()); } + return null; } - public long getCurrentReplyId () { - return replyMessage != null ? replyMessage.id : 0; - } - - public long obtainReplyId () { - if (replyMessage == null || replyMessage.id == 0 || (flags & FLAG_REPLY_ANIMATING) != 0) { - return 0; + public @Nullable TdApi.InputMessageReplyToMessage obtainReplyTo () { + if (reply != null) { + TdApi.InputMessageReplyToMessage replyTo = getCurrentReplyId(); + closeReply(true, false); + return replyTo; } - long messageId = replyMessage.id; - closeReply(true); - return messageId; - } - - private void showCurrentReply () { - replyView.setReplyTo(replyMessage, tdlib.isChannel(chat.id) ? chat.title : null); + return null; } - public void removeReply (long[] messageIds) { - if (replyMessage != null) { + public void removeReply (long chatId, long[] messageIds) { + if (reply != null) { for (long msgId : messageIds) { - if (msgId == replyMessage.id) { - if (showingLinkPreview() || isEditingMessage()) { - replyMessage = null; - } else { - closeReply(true); - } + if (reply.message.chatId == chatId && msgId == reply.message.id) { + reply = null; + updateReplyBarVisibility(true); break; } } } } - public void showReply (TdApi.Message msg, boolean byUser, boolean showKeyboard) { + public void showReply (TdApi.Message msg, @Nullable TdApi.InputTextQuote quote, boolean byUser, boolean showKeyboard) { if (inPreviewMode || isInForceTouchMode()) { return; } if (msg == null || msg.id == 0) { - if (showingLinkPreview()) { - showCurrentLinkPreview(); - } else { - closeReply(byUser); - } + this.reply = null; + updateReplyBarVisibility(true); return; } if (inSearchMode()) { @@ -6004,18 +6211,11 @@ public void showReply (TdApi.Message msg, boolean byUser, boolean showKeyboard) } else if (inSelectMode()) { finishSelectMode(-1); } + collapsePinnedMessagesBar(true); // TODO show keyboard properly - if ((flags & FLAG_REPLY_ANIMATING) == 0 && (replyMessage == null || replyMessage.id != msg.id)) { - if (!showingLinkPreview()) { - replyView.setReplyTo(msg, tdlib.isChannel(chat.id) ? chat.title : null); - } - - if (replyMessage != null || showingLinkPreview() || isEditingMessage()) { - replyMessage = msg; - } else { - replyMessage = msg; - openReplyView(); - } + if (reply == null || reply.message.id != msg.id || reply.message.chatId != msg.chatId || !Td.equalsTo(reply.quote, quote)) { + this.reply = new ReplyInfo(tdlib, msg, quote); + updateReplyBarVisibility(true); if (byUser) { inputView.setTextChangedSinceChatOpened(true); @@ -6027,80 +6227,103 @@ public void showReply (TdApi.Message msg, boolean byUser, boolean showKeyboard) } } - private void openReplyView () { - flags |= FLAG_REPLY_ANIMATING; + private void updateReplyBarVisibility (boolean animated) { + if (replyBarView == null) { + return; + } + boolean shouldBeVisible = true; + if (showingLinkPreview()) { + replyBarView.showWebPage(findTargetContext(), findTargetContext().findSelectedUrlIndex()); + } else if (isEditingMessage()) { + replyBarView.setEditingMessage(editContext.message); + } else if (reply != null) { + replyBarView.setReplyTo(reply.message, reply.quote); + } else { + shouldBeVisible = false; + } + final float toFactor = shouldBeVisible ? 1f : 0f; + if (animated && replyBarVisible.getFloatValue() != toFactor) { + setForceHw(true); // Resets back in onFactorChangeFinished + } + replyBarVisible.setValue(shouldBeVisible, animated); + } + + @Override + public void onDismissReplyBar (ReplyBarView view) { + if (showingLinkPreview()) { + closeLinkPreview(); + } else if (isEditingMessage()) { + closeEdit(false); + } else { + closeReply(true, true); + } + } - setForceHw(true); + private TooltipOverlayView.TooltipInfo anotherChatHint; - ValueAnimator obj; - final float startFactor = getReplyFactor(); - final float diffFactor = 1f - startFactor; - obj = AnimatorUtils.simpleValueAnimator(); - obj.addUpdateListener(animation -> setReplyFactor(startFactor + diffFactor * AnimatorUtils.getFraction(animation))); - obj.setInterpolator(AnimatorUtils.DECELERATE_INTERPOLATOR); - obj.setDuration(200l); - obj.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd (Animator animation) { - setForceHw(false); - flags &= ~FLAG_REPLY_ANIMATING; + @Override + public void onMessageHighlightRequested (ReplyBarView view, TdApi.Message message, @Nullable TdApi.InputTextQuote quote) { + if (message.chatId == getChatId()) { + highlightMessage(new MessageId(message.chatId, message.id)); + } else { + if (anotherChatHint != null && anotherChatHint.isVisible()) { + tdlib.ui().openMessage(this, message.chatId, new MessageId(message.chatId, message.id), new TdlibUi.UrlOpenParameters().controller(this)); + return; } - }); - obj.start(); + anotherChatHint = context() + .tooltipManager() + .builder(view) + .show(this, tdlib, R.drawable.baseline_info_24, Lang.getString(R.string.AnotherChatReplyHint)) + .hideDelayed(); + } } - private void forceDraftReply (final long messageId) { + private void forceDraftReply (final TdApi.InputMessageReplyToMessage replyTo) { final long currentChatId = chat.id; - TGMessage foundMessage = manager.getAdapter().findMessageById(messageId); - if (foundMessage != null) { - forceReply(foundMessage.getMessage()); - return; + final long replyToChatId = replyTo.chatId == 0 ? currentChatId : replyTo.chatId; + if (replyToChatId == currentChatId) { + TGMessage foundMessage = manager.getAdapter().findMessageById(replyTo.messageId); + if (foundMessage != null) { + forceReply(foundMessage.getMessage(), replyTo.quote); + return; + } } - tdlib.client().send(new TdApi.GetMessage(currentChatId, messageId), object -> tdlib.ui().post(() -> { - if (object.getConstructor() == TdApi.Message.CONSTRUCTOR && chat != null && chat.id == currentChatId) { - TdApi.DraftMessage draftMessage = getDraftMessage(); - if (draftMessage != null && draftMessage.replyToMessageId == messageId) { - forceReply((TdApi.Message) object); - } + tdlib.send(new TdApi.GetMessage(replyToChatId, replyTo.messageId), (foundReplyMessage, error) -> { + if (foundReplyMessage != null) { + runOnUiThreadOptional(() -> { + if (chat != null && chat.id == currentChatId) { + TdApi.DraftMessage draftMessage = getDraftMessage(); + TdApi.InputMessageReplyTo currentReplyTo = draftMessage != null ? draftMessage.replyTo : null; + if (Td.equalsTo(replyTo, currentReplyTo)) { + forceReply(foundReplyMessage, replyTo.quote); + } + } + }); } - })); + }); } - public void forceReply (TdApi.Message message) { + public void forceReply (TdApi.Message message, @Nullable TdApi.InputTextQuote quote) { if (message == null || chat == null || inPreviewMode || isInForceTouchMode()) { clearReply(); return; } - if (!showingLinkPreview()) { - replyView.setReplyTo(message, tdlib.isChannel(chat.id) ? chat.title : null); - } - - if (replyMessage != null || showingLinkPreview()) { - replyMessage = message; - } else { - replyMessage = message; - setReplyFactor(1f); - } + reply = new ReplyInfo(tdlib, message, quote); + updateReplyBarVisibility(false); } - public void clearReply () { - replyMessage = null; - dismissedLink = null; - attachedLink = null; - attachedPreview = null; - flags &= ~FLAG_REPLY_ANIMATING; - setReplyFactor(0f); - replyView.clear(); + private void clearReply () { + reply = null; + draftContext.reset(); + updateReplyBarVisibility(false); } - public void closeReply (final boolean byUser) { + public void closeReply (final boolean byUser, boolean animated) { tdlib.uiExecute(() -> { - if (replyMessage != null && (flags & FLAG_REPLY_ANIMATING) == 0) { - replyMessage = null; - if (editingMessage == null) { - closeReplyView(); - } + if (reply != null) { + reply = null; + updateReplyBarVisibility(animated); if (byUser) { inputView.setTextChangedSinceChatOpened(true); saveDraft(); @@ -6122,9 +6345,11 @@ private void setForceHw (boolean forceHw) { if (originalLayerType1 != View.LAYER_TYPE_HARDWARE) { Views.setLayerType(messagesView, View.LAYER_TYPE_HARDWARE); } - originalLayerType2 = replyView.getLayerType(); - if (originalLayerType2 != View.LAYER_TYPE_HARDWARE) { - Views.setLayerType(replyView, View.LAYER_TYPE_HARDWARE); + if (replyBarView != null) { + originalLayerType2 = replyBarView.getLayerType(); + if (originalLayerType2 != View.LAYER_TYPE_HARDWARE) { + Views.setLayerType(replyBarView, View.LAYER_TYPE_HARDWARE); + } } originalLayerType3 = bottomShadowView.getLayerType(); if (originalLayerType3 != View.LAYER_TYPE_HARDWARE) { @@ -6135,7 +6360,7 @@ private void setForceHw (boolean forceHw) { Views.setLayerType(messagesView, originalLayerType1); } if (originalLayerType2 != View.LAYER_TYPE_HARDWARE) { - Views.setLayerType(replyView, originalLayerType2); + Views.setLayerType(replyBarView, originalLayerType2); } if (originalLayerType3 != View.LAYER_TYPE_HARDWARE) { Views.setLayerType(bottomShadowView, originalLayerType3); @@ -6144,39 +6369,33 @@ private void setForceHw (boolean forceHw) { } } - private void closeReplyView () { - flags |= FLAG_REPLY_ANIMATING; + private static final boolean ANIMATE_REPLY_BAR = false; - if (isSendingText) { - setReplyFactor(0f); - replyView.clear(); - flags &= ~FLAG_REPLY_ANIMATING; - return; + private final BoolAnimator replyBarVisible = new BoolAnimator(0, new FactorAnimator.Target() { + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + if (ANIMATE_REPLY_BAR && replyBarView != null) { + replyBarView.setAnimationsDisabled(factor == 0f); + } + updateReplyView(); } - setForceHw(true); - - ValueAnimator obj; - final float startFactor = getReplyFactor(); - obj = AnimatorUtils.simpleValueAnimator(); - obj.addUpdateListener(animation -> setReplyFactor(startFactor - startFactor * AnimatorUtils.getFraction(animation))); - obj.setInterpolator(AnimatorUtils.DECELERATE_INTERPOLATOR); - obj.setDuration(200l); - obj.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd (Animator animation) { + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + if (finalFactor == 1f || finalFactor == 0f) { setForceHw(false); - replyView.clear(); - flags &= ~FLAG_REPLY_ANIMATING; } - }); - obj.start(); - } - - private float replyFactor; + if (finalFactor == 0f) { + replyBarView.reset(); + } + } + }, AnimatorUtils.DECELERATE_INTERPOLATOR, 200L); private float getReplyOffset () { - return replyFactor * (1f - getSearchTransformFactor()) * (float) (replyView.getLayoutParams().height); + if (replyBarView == null) { + return 0; + } + return replyBarVisible.getFloatValue() * (1f - getSearchTransformFactor()) * (float) (replyBarView.getLayoutParams().height); } private float getButtonsOffset () { @@ -6207,13 +6426,6 @@ private float getReactionButtonY () { return y; } - public void setReplyFactor (float factor) { - if (this.replyFactor != factor) { - this.replyFactor = factor; - updateReplyView(); - } - } - private void checkScrollButtonOffsets () { if (isInForceTouchMode()) { return; @@ -6242,31 +6454,39 @@ private void updateReplyView () { float y = -getReplyOffset(); messagesView.setTranslationY(y); bottomShadowView.setTranslationY(y); - replyView.setTranslationY(y); + if (replyBarView != null) { + replyBarView.setTranslationY(y); + } checkScrollButtonOffsets(); onMessagesFrameChanged(); } - public float getReplyFactor () { - return replyFactor; - } - // Edit utils public boolean isEditingMessage () { - return editingMessage != null; + return editContext != null; } public boolean hasEditedChanges () { if (isEditingMessage()) { TdApi.FormattedText newText = inputView != null ? inputView.getOutputText(true) : null; + TdApi.LinkPreviewOptions newOptions = inputView != null ? editContext.takeOutputLinkPreviewOptions(false) : null; - switch (editingMessage.content.getConstructor()) { + //noinspection SwitchIntDef + switch (editContext.message.content.getConstructor()) { + case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: { + TdApi.FormattedText oldText = Td.textOrCaption(editContext.message.content); + if (!Td.equalsTo(oldText, newText) || !Td.equalsTo(null, newOptions)) { + return true; + } + break; + } case TdApi.MessageText.CONSTRUCTOR: { - TdApi.MessageText oldMessageText = (TdApi.MessageText) editingMessage.content; - TdApi.InputMessageText newInputMessageText = new TdApi.InputMessageText(newText, getCurrentAllowLinkPreview(), false); - if (!Td.equalsTo(newInputMessageText.text, oldMessageText.text) || (newInputMessageText.disableWebPagePreview && oldMessageText.webPage != null) || (!newInputMessageText.disableWebPagePreview && oldMessageText.webPage == null && attachedPreview != null)) + TdApi.MessageText oldMessageText = (TdApi.MessageText) editContext.message.content; + TdApi.LinkPreviewOptions oldLinkPreviewOptions = oldMessageText.linkPreviewOptions; + if (!Td.equalsTo(oldMessageText.text, newText) || !Td.equalsTo(oldLinkPreviewOptions, newOptions)) { return true; + } break; } case TdApi.MessagePhoto.CONSTRUCTOR: @@ -6275,9 +6495,13 @@ public boolean hasEditedChanges () { case TdApi.MessageVoiceNote.CONSTRUCTOR: case TdApi.MessageDocument.CONSTRUCTOR: case TdApi.MessageAnimation.CONSTRUCTOR: { - TdApi.FormattedText oldText = Td.textOrCaption(editingMessage.content); + TdApi.FormattedText oldText = Td.textOrCaption(editContext.message.content); return !Td.equalsTo(oldText, newText); } + default: { + Td.assertMessageContent_d40af239(); + break; + } } } @@ -6285,70 +6509,235 @@ public boolean hasEditedChanges () { } public boolean isEditingCaption () { - return editingMessage != null && editingMessage.content.getConstructor() != TdApi.MessageText.CONSTRUCTOR; + return editContext != null && editContext.message.content.getConstructor() != TdApi.MessageText.CONSTRUCTOR; } - @Nullable - public TdApi.WebPage getEditingWebPage (TdApi.FormattedText currentText) { - if (editingMessage != null && editingMessage.content.getConstructor() == TdApi.MessageText.CONSTRUCTOR) { - TdApi.MessageText messageText = (TdApi.MessageText) editingMessage.content; - //noinspection UnsafeOptInUsageError - return Td.equalsTo(currentText, messageText.text, true) ? messageText.webPage : null; - } - return null; - } + public static class MessageInputContext { + private final MessagesController context; + private final Tdlib tdlib; + private final TdApi.Message message; + private @NonNull FoundUrls foundUrls; - private TdApi.Message editingMessage; + private FoundUrls dismissedFoundUrls; - private void showCurrentEdit () { - replyView.setReplyTo(editingMessage, Lang.getString(R.string.EditMessage)); - } + private final TdApi.LinkPreviewOptions linkPreviewOptions; - private void setInEditMode (boolean inEditMode, String futureText) { - sendButton.setInEditMode(inEditMode); - messageSenderButton.setAnimateVisible(!inEditMode); - if (inputView != null) { - inputView.setIsInEditMessageMode(inEditMode, futureText); + MessageInputContext (MessagesController context, Tdlib tdlib, @Nullable TdApi.Message existingMessage) { + this.context = context; + this.tdlib = tdlib; + this.foundUrls = FoundUrls.emptyResult(); + this.message = existingMessage; + if (existingMessage != null && Td.isText(existingMessage.content)) { + TdApi.MessageText messageText = (TdApi.MessageText) existingMessage.content; + linkPreviewOptions = Td.copyOf(messageText.linkPreviewOptions); + if (linkPreviewOptions.isDisabled) { + dismissedFoundUrls = foundUrls = new FoundUrls(messageText); + } + } else { + linkPreviewOptions = new TdApi.LinkPreviewOptions(); + } } - } - - public boolean inSimpleSendMode () { - return sendButton.inSimpleSendMode(); - } - - private static final int FLAG_INPUT_EDITING = 1; - private static final int FLAG_INPUT_OFFSCREEN = 1 << 1; - private static final int FLAG_INPUT_RECORDING = 1 << 2; - private int inputBlockFlags; + public @NonNull FoundUrls getFoundUrls () { + return foundUrls; + } - private boolean setInputBlockFlags (int flags) { - if (this.inputBlockFlags != flags) { - boolean prevIsBlocked = this.inputBlockFlags != 0; - this.inputBlockFlags = flags; - boolean nowIsBlocked = flags != 0; - if (prevIsBlocked != nowIsBlocked && inputView != null) { - inputView.setInputBlocked(nowIsBlocked); + public boolean setLinkPreviewUrl (@NonNull String url) { + if (url.equals(this.linkPreviewOptions.url)) { + return false; } + if (StringUtils.isEmpty(this.linkPreviewOptions.url) && url.equals(foundUrls.urls[0])) { + return false; + } + this.linkPreviewOptions.url = url; return true; } - return false; - } - private void setInputBlockFlag (int flag, boolean active) { - if (setInputBlockFlags(BitwiseUtils.setFlag(inputBlockFlags, flag, active))) { - if (flag == FLAG_INPUT_OFFSCREEN && inputView != null) { - inputView.setEnabled(!active); + private final Map linkPreviews = new HashMap<>(); + + public @Nullable LinkPreview getSelectedLinkPreview () { + if (linkPreviewOptions.isDisabled) { + return null; } + int index = findSelectedUrlIndex(); + if (index == -1) + return null; + String url = foundUrls.urls[index]; + return getLinkPreview(url); } - } - public boolean arePinnedMessages () { - return previewSearchFilter != null && previewSearchFilter.getConstructor() == TdApi.SearchMessagesFilterPinned.CONSTRUCTOR; - } + public @NonNull LinkPreview getLinkPreview (String url) { + LinkPreview linkPreview = linkPreviews.get(url); + if (linkPreview == null) { + linkPreview = new LinkPreview(tdlib, url, message); + linkPreview.addLoadCallback(loadedLinkPreview -> { + if (loadedLinkPreview.isNotFound()) { + context.updateReplyBarVisibility(true); + } + }); + linkPreviews.put(url, linkPreview); + } + return linkPreview; + } - public void openPreviewMessage (TGMessage msg) { - if (arePinnedMessages()) { + public int findSelectedUrlIndex () { + if (foundUrls.isEmpty()) { + return -1; + } else if (StringUtils.isEmpty(linkPreviewOptions.url)) { + return 0; + } else { + int index = foundUrls.indexOfUrl(linkPreviewOptions.url); + if (index != -1) { + return index; + } + return 0; + } + } + + public TdApi.LinkPreviewOptions takeOutputLinkPreviewOptions (boolean copy) { + LinkPreview linkPreview = getSelectedLinkPreview(); + if (linkPreview != null) { + boolean forceLargeMedia = linkPreview.forceLargeMedia(); + boolean forceSmallMedia = linkPreview.forceSmallMedia(); + if (linkPreviewOptions.forceLargeMedia != forceLargeMedia || linkPreviewOptions.forceSmallMedia != forceSmallMedia) { + linkPreviewOptions.forceLargeMedia = forceLargeMedia; + linkPreviewOptions.forceSmallMedia = forceSmallMedia; + } + if ((forceLargeMedia || forceSmallMedia) && StringUtils.isEmpty(linkPreviewOptions.url)) { + linkPreviewOptions.url = linkPreview.url; + } + } + return copy ? Td.copyOf(linkPreviewOptions) : linkPreviewOptions; + } + + public TdApi.WebPage takePreloadedOutputWebPage () { + LinkPreview linkPreview = getSelectedLinkPreview(); + TdApi.WebPage webPage = linkPreview != null ? linkPreview.webPage : null; + if (webPage != null && webPage.hasLargeMedia) { + boolean showLargeMedia = linkPreview.getOutputShowLargeMedia(); + if (webPage.showLargeMedia != showLargeMedia) { + webPage = Td.copyOf(webPage); + webPage.showLargeMedia = showLargeMedia; + } + } + return webPage; + } + + public boolean checkMessage (long chatId, long messageId) { + return message != null && message.chatId == chatId && message.id == messageId; + } + + boolean setFoundUrls (@Nullable FoundUrls foundUrls) { + if (foundUrls == null) { + foundUrls = FoundUrls.emptyResult(); + } + + FoundUrls previouslyFoundUrls = this.foundUrls; + this.foundUrls = foundUrls; + + boolean wasEmpty = previouslyFoundUrls.isEmpty(); + boolean nowEmpty = foundUrls.isEmpty(); + + boolean hasChanges = wasEmpty != nowEmpty || (!nowEmpty && !previouslyFoundUrls.equals(foundUrls)); + + if (dismissedFoundUrls != null && !foundUrls.equals(dismissedFoundUrls)) { + linkPreviewOptions.isDisabled = false; + dismissedFoundUrls = null; + hasChanges = true; + } + if (!StringUtils.isEmpty(linkPreviewOptions.url) && !foundUrls.hasUrl(linkPreviewOptions.url)) { + linkPreviewOptions.url = null; + hasChanges = true; + } + if (hasChanges && !foundUrls.isEmpty()) { + linkPreviewOptions.isDisabled = false; + } + if (hasChanges && foundUrls.size() > 1 && Settings.instance().needTutorial(Settings.TUTORIAL_MULTIPLE_LINK_PREVIEWS)) { + Settings.instance().markTutorialAsShown(Settings.TUTORIAL_MULTIPLE_LINK_PREVIEWS); + context.context() + .tooltipManager() + .builder(context.replyBarView) + .icon(R.drawable.baseline_info_24) + .show(tdlib, R.string.SwipeToSwapLinkPreview); + } + + return hasChanges; + } + + void dismiss () { + dismissedFoundUrls = foundUrls; + Td.reset(linkPreviewOptions); + linkPreviewOptions.isDisabled = true; + } + + void reset () { + dismissedFoundUrls = null; + foundUrls = FoundUrls.emptyResult(); + Td.reset(linkPreviewOptions); + } + + public boolean isVisible () { + if (!linkPreviewOptions.isDisabled && !foundUrls.isEmpty()) { + for (String url : foundUrls.urls) { + LinkPreview linkPreview = linkPreviews.get(url); + if (linkPreview == null || !linkPreview.isNotFound()) { + return true; + } + } + } + return false; + } + } + + private void setInEditMode (boolean inEditMode, String futureText) { + sendButton.setInEditMode(inEditMode); + messageSenderButton.setAnimateVisible(!inEditMode); + if (inputView != null) { + inputView.setIsInEditMessageMode(inEditMode, futureText); + } + } + + public boolean inSimpleSendMode () { + return sendButton.inSimpleSendMode(); + } + + private static final int FLAG_INPUT_EDITING = 1; + private static final int FLAG_INPUT_OFFSCREEN = 1 << 1; + private static final int FLAG_INPUT_RECORDING = 1 << 2; + private static final int FLAG_INPUT_TEXT_DISABLED = 1 << 3; + + private int inputBlockFlags; + + private boolean setInputBlockFlags (int flags) { + if (this.inputBlockFlags != flags) { + boolean prevIsBlocked = this.inputBlockFlags != 0; + this.inputBlockFlags = flags; + boolean nowIsBlocked = flags != 0; + if (prevIsBlocked != nowIsBlocked && inputView != null) { + inputView.setInputBlocked(nowIsBlocked); + } + return true; + } + return false; + } + + private void setInputBlockFlag (int flag, boolean active) { + if (setInputBlockFlags(BitwiseUtils.setFlag(inputBlockFlags, flag, active))) { + if ((flag == FLAG_INPUT_OFFSCREEN || flag == FLAG_INPUT_TEXT_DISABLED) && inputView != null) { + inputView.setEnabled( + !BitwiseUtils.hasFlag(inputBlockFlags, FLAG_INPUT_OFFSCREEN) && + !BitwiseUtils.hasFlag(inputBlockFlags, FLAG_INPUT_TEXT_DISABLED) + ); + } + } + } + + public boolean arePinnedMessages () { + return previewSearchFilter != null && Td.isPinnedFilter(previewSearchFilter); + } + + public void openPreviewMessage (TGMessage msg) { + if (arePinnedMessages()) { ViewController c = previousStackItem(); if (c instanceof MessagesController && c.getChatId() == getChatId()) { ((MessagesController) c).highlightMessage(msg.toMessageId()); @@ -6360,7 +6749,7 @@ public void openPreviewMessage (TGMessage msg) { } private void resetEditState () { - editingMessage = null; + editContext = null; if (inputView != null) { inputView.resetState(); setInputBlockFlag(FLAG_INPUT_EDITING, false); @@ -6369,10 +6758,11 @@ private void resetEditState () { } private void editMessage (TdApi.Message msg) { - if (editingMessage != null || (flags & FLAG_REPLY_ANIMATING) != 0) { + if (isEditingMessage()) { return; } + needShowEmojiKeyboardAfterHideMessageOptions = false; saveDraft(); if (inSelectMode()) { @@ -6381,14 +6771,11 @@ private void editMessage (TdApi.Message msg) { closeSearchMode(null); } - this.editingMessage = msg; + this.editContext = new MessageInputContext(this, tdlib, msg); TdApi.FormattedText text = Td.textOrCaption(msg.content); setInEditMode(true, text.text); sendButton.setIsActive(!StringUtils.isEmpty(text.text) || isEditingCaption()); - replyView.setReplyTo(msg, Lang.getString(R.string.EditMessage)); - if (!showingLinkPreview() && replyMessage == null) { - openReplyView(); - } + updateReplyBarVisibility(true); if (inputView != null) { TdApi.FormattedText pendingText = tdlib.getPendingFormattedText(msg.chatId, msg.id); if (pendingText != null) { @@ -6399,8 +6786,8 @@ private void editMessage (TdApi.Message msg) { Keyboard.show(inputView); } - private void closeEdit () { - if (editingMessage == null || (flags & FLAG_REPLY_ANIMATING) != 0) { + private void closeEdit (boolean isSaved) { + if (!isEditingMessage()) { return; } @@ -6409,50 +6796,49 @@ private void closeEdit () { setInputBlockFlag(FLAG_INPUT_EDITING, false); } setInEditMode(false, ""); - editingMessage = null; + editContext = null; if (inputView != null) { updateSendButton(inputView.getInput(), true); } - if (showingLinkPreview()) { - showCurrentLinkPreview(); - } else if (replyMessage != null) { - showCurrentReply(); - } else { - closeReplyView(); - } + // Intentionally doesn't match the send logic (where there's no animation after send). + // For the exact match, !isSaved could be passed here instead. + updateReplyBarVisibility(true); } private void saveMessage (boolean applyMarkdown) { - if (editingMessage == null || inputView == null) { + if (!isEditingMessage() || inputView == null) { return; } - //noinspection UnsafeOptInUsageError - TdApi.FormattedText newText = Td.trim(inputView.getOutputText(applyMarkdown)); - //noinspection UnsafeOptInUsageError - if (Td.isEmpty(newText) && !isEditingMessage()) - return; + TdApi.FormattedText newText = inputView.getOutputText(applyMarkdown); + TdApi.LinkPreviewOptions newOptions = editContext.takeOutputLinkPreviewOptions(true); - switch (editingMessage.content.getConstructor()) { + switch (editContext.message.content.getConstructor()) { case TdApi.MessageText.CONSTRUCTOR: case TdApi.MessageAnimatedEmoji.CONSTRUCTOR: { - TdApi.MessageText oldMessageText; + if (Td.isEmpty(newText)) { + return; + } - if (editingMessage.content.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { - oldMessageText = new TdApi.MessageText(Td.textOrCaption(editingMessage.content), null); + TdApi.MessageText oldMessageText; + if (editContext.message.content.getConstructor() == TdApi.MessageAnimatedEmoji.CONSTRUCTOR) { + oldMessageText = new TdApi.MessageText(Td.textOrCaption(editContext.message.content), null, null); } else { - oldMessageText = (TdApi.MessageText) editingMessage.content; + oldMessageText = (TdApi.MessageText) editContext.message.content; } - TdApi.InputMessageText newInputMessageText = new TdApi.InputMessageText(newText, getCurrentAllowLinkPreview(), false); - if (!Td.equalsTo(newInputMessageText.text, oldMessageText.text) || (newInputMessageText.disableWebPagePreview && oldMessageText.webPage != null) || (!newInputMessageText.disableWebPagePreview && oldMessageText.webPage == null && attachedPreview != null)) { + TdApi.LinkPreviewOptions oldLinkPreviewOptions = oldMessageText.linkPreviewOptions; + + TdApi.InputMessageText newInputMessageText = new TdApi.InputMessageText(newText, newOptions, false); + if (!Td.equalsTo(newInputMessageText.text, oldMessageText.text) || !Td.equalsTo(newInputMessageText.linkPreviewOptions, oldLinkPreviewOptions)) { final int maxLength = tdlib.maxMessageTextLength(); - final int newTextLength = newText.text.codePointCount(0, newText.text.length()); + final int newTextLength = newText != null ? newText.text.codePointCount(0, newText.text.length()) : 0; if (newTextLength > maxLength) { showBottomHint(Lang.pluralBold(R.string.EditMessageTextTooLong, newTextLength - maxLength), true); return; } - tdlib.editMessageText(editingMessage.chatId, editingMessage.id, newInputMessageText, attachedPreview); + TdApi.WebPage webPage = editContext.takePreloadedOutputWebPage(); + tdlib.editMessageText(editContext.message.chatId, editContext.message.id, newInputMessageText, webPage); } break; } @@ -6462,7 +6848,7 @@ private void saveMessage (boolean applyMarkdown) { case TdApi.MessageVoiceNote.CONSTRUCTOR: case TdApi.MessageDocument.CONSTRUCTOR: case TdApi.MessageAnimation.CONSTRUCTOR: { - TdApi.FormattedText oldText = Td.textOrCaption(editingMessage.content); + TdApi.FormattedText oldText = Td.textOrCaption(editContext.message.content); if (!Td.equalsTo(oldText, newText)) { String newString = newText.text.trim(); final int maxLength = tdlib.maxCaptionLength(); @@ -6471,31 +6857,28 @@ private void saveMessage (boolean applyMarkdown) { showBottomHint(Lang.pluralBold(R.string.EditMessageCaptionTooLong, newCaptionLength - maxLength), true); return; } - tdlib.editMessageCaption(editingMessage.chatId, editingMessage.id, newText); + tdlib.editMessageCaption(editContext.message.chatId, editContext.message.id, newText); } break; } default: { - throw new UnsupportedOperationException(Integer.toString(editingMessage.content.getConstructor())); + Td.assertMessageContent_d40af239(); + throw Td.unsupported(editContext.message.content); } } - closeEdit(); + closeEdit(true); } // Share utils - public void shareMessage (TdApi.Message msg) { - shareMessages(msg.chatId, new TdApi.Message[]{msg}); - } - - public void shareMessages (long chatId, TdApi.Message[] messages) { + public void shareMessages (TdApi.Message[] messages, boolean isExplicitSelection) { if (messages == null || messages.length == 0) { return; } hideAllKeyboards(); final ShareController c = new ShareController(context, tdlib); - c.setArguments(new ShareController.Args(messages).setAfter(() -> finishSelectMode(-1))); + c.setArguments(new ShareController.Args(messages).setDisallowReply(isExplicitSelection).setAfter(() -> finishSelectMode(-1))); c.show(); hideCursorsForInputView(); } @@ -6547,7 +6930,7 @@ public void showKeyboard () { public void hideAllKeyboards () { hideSoftwareKeyboard(); closeCommandsKeyboard(false); - closeEmojiKeyboard(); + closeEmojiKeyboard(true); } public void hideKeyboard (boolean personal) { @@ -7028,7 +7411,7 @@ public void onResizeCommandKeyboard (int size) { // Silent mode public int getHorizontalInputPadding () { - return attachButtons.getVisibleChildrenWidth() + (canSelectSender() ? Screen.dp(47): 0); + return attachButtons.getVisibleChildrenWidth() + (canSelectSender() ? Screen.dp(47) : 0); } private void updateSilentButton (boolean visible) { @@ -7104,7 +7487,7 @@ public void showHidePinnedMessage (boolean show, @Nullable TdApi.Message message if (show) { pinnedMessagesBar.setCollapseButtonVisible(false); pinnedMessagesBar.setContextChatId(getChatId() != getHeaderChatId() ? getHeaderChatId() : 0); - pinnedMessagesBar.setMessage(message); + pinnedMessagesBar.setMessage(tdlib, message); } topBar.setItemVisible(pinnedMessagesItem, show, isFocused()); } @@ -7160,14 +7543,15 @@ private void dismissPinnedMessage () { } public void onMessageChanged (long chatId, long messageId, TdApi.MessageContent content) { - if (editingMessage != null && editingMessage.chatId == chatId && editingMessage.id == messageId) { - if (!editingMessage.canBeEdited) { - closeEdit(); + if (isEditingMessage() && editContext.checkMessage(chatId, messageId)) { + if (!editContext.message.canBeEdited) { + closeEdit(false); } else { - TdApi.MessageContent oldContent = editingMessage.content; - editingMessage.content = content; - replyView.setReplyTo(editingMessage, Lang.getString(R.string.EditMessage)); - editingMessage.content = oldContent; + TdApi.MessageContent oldContent = editContext.message.content; + editContext.message.content = content; + updateReplyBarVisibility(true); + // This is required because we work with a reference from TGMessage, not a copy. + editContext.message.content = oldContent; } } } @@ -7177,8 +7561,8 @@ public void onInlineTranslationChanged (long chatId, long messageId, TdApi.Forma } public void onMessagesDeleted (long chatId, long[] messageIds) { - if (editingMessage != null && editingMessage.chatId == chatId && ArrayUtils.indexOf(messageIds, editingMessage.id) != -1) { - closeEdit(); + if (isEditingMessage() && editContext.message.chatId == chatId && ArrayUtils.indexOf(messageIds, editContext.message.id) != -1) { + closeEdit(false); } } @@ -7204,54 +7588,6 @@ public void onMessageThreadDeleted (long chatId, long messageThreadId) { } } - /*private void updatePinnedMessageView () { - if (topWrap != null) { - float factor = pinnedMessageFactor * (1f - getSearchTransformFactor()); - int height = Screen.dp(48f); - int translationY = -height + (int) ((float) height * factor) + getLiveLocationOffset(); - topWrap.setTranslationY(translationY); - topBasePinnedMessageShadow.setAlpha(Math.max(liveLocation != null ? liveLocation.getVisibilityFactor() : 0f, factor)); - if (needPinnedMessageOffset) { - float offsetFactor = factor - lastPinnedMessageOffsetFactor; - lastPinnedMessageOffsetFactor = factor; - messagesView.scrollBy(0, -(int) ((float) getPinnedMessageHeight() * offsetFactor)); - } - } - }*/ - - /*public int getPinnedMessageHeight () { - return Screen.dp(48f); - }*/ - - /*private boolean needPinnedMessageOffset; - private float lastPinnedMessageOffsetFactor; - - private void setPinnedMessageVisible (boolean isVisible, boolean animated) { - if (this.isPinnedMessageVisible != isVisible) { - this.isPinnedMessageVisible = isVisible; - final float toFactor = isVisible ? 1f : 0f; - - needPinnedMessageOffset = false; - if (isVisible && manager.canApplyRecyclerOffsets()) { - if (animated) { - needPinnedMessageOffset = true; - lastPinnedMessageOffsetFactor = this.pinnedMessageFactor; - } else { - try { - messagesView.scrollBy(0, -getPinnedMessageHeight()); - } catch (Throwable t) { - Log.e("messagesView.scrollBy failed", t); - } - } - } - if (animated) { - animatePinnedFactor(toFactor); - } else { - forcePinnedFactor(toFactor); - } - } - }*/ - // Live location private View liveLocationView; @@ -7298,6 +7634,9 @@ private TopBarView.Item newUnarchiveItem (long chatId) { true, 0, settings.useDefaultSound, settings.soundId, settings.useDefaultShowPreview, settings.showPreview, + settings.useDefaultMuteStories, settings.muteStories, + settings.useDefaultStorySound, settings.storySoundId, + settings.useDefaultShowStorySender, settings.showStorySender, settings.useDefaultDisablePinnedMessageNotifications, settings.disablePinnedMessageNotifications, settings.useDefaultDisableMentionNotifications, settings.disableMentionNotifications ); @@ -7332,11 +7671,11 @@ private TopBarView.Item newReportItem (long chatId, boolean isBlock) { } if (blockSender) { - tdlib.blockSender(tdlib.sender(chat.id), true, tdlib.okHandler()); + tdlib.blockSender(tdlib.sender(chat.id), new TdApi.BlockListMain(), tdlib.okHandler()); } if (reportSpam) { - tdlib.client().send(new TdApi.ReportChat(getChatId(), null, new TdApi.ChatReportReasonSpam(), null), tdlib.okHandler()); + tdlib.client().send(new TdApi.ReportChat(getChatId(), null, new TdApi.ReportReasonSpam(), null), tdlib.okHandler()); } if (deleteChat) { deleteAndLeave(); @@ -7387,7 +7726,7 @@ private void checkActionBar () { ); showOptions(title, new int[] {R.id.btn_reportChat, R.id.btn_cancel}, new String[] {Lang.getString(R.string.ReportLocationAction), Lang.getString(R.string.Cancel)}, new int[] {OPTION_COLOR_RED, OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_report_24, R.drawable.baseline_cancel_24}, (v, optionId) -> { if (optionId == R.id.btn_reportChat) { - tdlib.client().send(new TdApi.ReportChat(chatId, null, new TdApi.ChatReportReasonUnrelatedLocation(), null), tdlib.okHandler()); + tdlib.client().send(new TdApi.ReportChat(chatId, null, new TdApi.ReportReasonUnrelatedLocation(), null), tdlib.okHandler()); dismissActionBar(); tdlib.ui().exitToChatScreen(this, chatId); } @@ -7459,7 +7798,7 @@ public boolean onSenderPick (ContactsController context, View view, TdApi.Messag items.add(new TopBarView.Item(R.id.btn_shareMyContact, R.string.SharePhoneNumber, v -> { TdApi.User user = tdlib.myUser(); if (user != null) { - showOptions(TD.getUserName(user) + ", " + Strings.formatPhone(user.phoneNumber), new int[]{R.id.btn_shareMyContact, R.id.btn_cancel}, new String[]{Lang.getString(R.string.SharePhoneNumberAction), Lang.getString(R.string.Cancel)}, new int[]{OPTION_COLOR_BLUE, OPTION_COLOR_NORMAL}, new int[]{R.drawable.baseline_contact_phone_24, R.drawable.baseline_cancel_24}, (itemView, id1) -> { + showOptions(TD.getUserName(user) + ", " + Strings.formatPhone(user.phoneNumber), new int[] {R.id.btn_shareMyContact, R.id.btn_cancel}, new String[] {Lang.getString(R.string.SharePhoneNumberAction), Lang.getString(R.string.Cancel)}, new int[] {OPTION_COLOR_BLUE, OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_contact_phone_24, R.drawable.baseline_cancel_24}, (itemView, id1) -> { if (id1 == R.id.btn_shareMyContact) { tdlib.client().send(new TdApi.SharePhoneNumber(tdlib.chatUserId(chatId)), tdlib.okHandler()); } @@ -7469,6 +7808,15 @@ public boolean onSenderPick (ContactsController context, View view, TdApi.Messag })); break; } + case TdApi.ChatActionBarJoinRequest.CONSTRUCTOR: { + TdApi.ChatActionBarJoinRequest joinRequest = (TdApi.ChatActionBarJoinRequest) actionBar; + // TODO + break; + } + default: { + Td.assertChatActionBar_9b96400f(); + throw Td.unsupported(actionBar); + } } } if (ChatId.isSecret(chatId)) { @@ -7552,7 +7900,7 @@ public void switchInline (long viaBotUserId, final TdApi.InlineKeyboardButtonTyp final String username = Td.primaryUsername(user); - if (switchInline.targetChat.getConstructor() == TdApi.TargetChatCurrent.CONSTRUCTOR && canWriteMessages() && hasWritePermission()) { // FIXME rightId.SEND_OTHER_MESSAGES + if (switchInline.targetChat.getConstructor() == TdApi.TargetChatCurrent.CONSTRUCTOR && canWriteMessages() && hasSendMessagePermission(RightId.SEND_OTHER_MESSAGES)) { if (inputView != null) { inputView.setInput("@" + username + " " + switchInline.query, true, true); } @@ -7582,10 +7930,6 @@ public void act () { @Override public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { switch (id) { - case ANIMATOR_STICKERS: { - setStickersFactor(factor); - break; - } case ANIMATOR_SCROLL_TO_BOTTOM: { scrollToBottomButtonWrap.setAlpha(MathUtils.clamp(factor)); checkScrollButtonOffsets(); @@ -7653,12 +7997,6 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca } break; } - case ANIMATOR_STICKERS: { - if (finalFactor == 0f) { - onStickersDisappeared(); - } - break; - } } } @@ -7674,290 +8012,100 @@ public void removeView (EmojiToneHelper context, View displayedView) { contentView.removeView(displayedView); } - // Stickers suggestions + public void onUsernamePick (String username) { + inputView.setInput("@" + username + " ", true, true); + } - private AnimatedFrameLayout stickerSuggestionsWrap; - private RecyclerView stickerSuggestionsView; - private StickerSuggestionAdapter stickerSuggestionAdapter; - private boolean areStickersVisible; + // Link preview - @Override - public boolean onSendStickerSuggestion (View view, TGStickerObj sticker, TdApi.MessageSendOptions initialSendOptions) { - if (lastJunkTime == 0l || SystemClock.uptimeMillis() - lastJunkTime >= JUNK_MINIMUM_DELAY) { - if (showGifRestriction(view)) - return false; - pickDateOrProceed(initialSendOptions, (modifiedSendOptions, disableMarkdown) -> { - if (sendSticker(view, sticker.getSticker(), sticker.getFoundByEmoji(), true, Td.newSendOptions(modifiedSendOptions, false, Config.REORDER_INSTALLED_STICKER_SETS))) { - lastJunkTime = SystemClock.uptimeMillis(); - inputView.setInput("", false, true); - } - }); - return true; - } - return false; + private void closeLinkPreview () { + findTargetContext().dismiss(); + updateReplyBarVisibility(!isEditingMessage()); + inputView.setTextChangedSinceChatOpened(true); } @Override - public int getStickerSuggestionsTop () { - return Views.getLocationInWindow(stickerSuggestionsWrap)[1]; // stickerSuggestionsWrap.getTop() + Views.getParentsTop(stickerSuggestionsWrap, 2); + public void onSelectLinkPreviewUrl (ReplyBarView view, MessageInputContext messageContext, String url) { + if (messageContext.setLinkPreviewUrl(url)) { + Settings.instance().markTutorialAsComplete(Settings.TUTORIAL_MULTIPLE_LINK_PREVIEWS); + inputView.setTextChangedSinceChatOpened(true); + } } @Override - public int getStickerSuggestionPreviewViewportHeight () { - return HeaderView.getSize(true) + messagesView.getMeasuredHeight(); - } - - public void hideStickerSuggestions () { - setStickersVisible(false); - } - - private boolean choosingSuggestionSent; - - public void showStickerSuggestions (@Nullable ArrayList stickers, boolean isMore) { - if (stickers == null || stickers.isEmpty()) { - if (!isMore) { - setStickersVisible(false); - } - return; - } - - if (stickerSuggestionsWrap == null) { - int stickersListTopHeight = Screen.dp(72f) + Screen.dp(2.5f); - int stickersListTotalHeight = stickersListTopHeight + Screen.dp(6.5f); - int stickerArrowHeight = Screen.dp(12f); - - RelativeLayout.LayoutParams params; - - params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, stickersListTotalHeight + stickerArrowHeight); - params.addRule(RelativeLayout.ABOVE, R.id.msg_bottom); - params.bottomMargin = -(Screen.dp(8f) + stickerArrowHeight); - - stickerSuggestionsWrap = new AnimatedFrameLayout(context()); - stickerSuggestionsWrap.setLayoutParams(params); - - RecyclerView.LayoutManager manager = new LinearLayoutManager(context(), LinearLayoutManager.HORIZONTAL, false); - stickerSuggestionAdapter = new StickerSuggestionAdapter(this, this, manager, this) { - @Override - public void onStickerPreviewOpened (StickerSmallView view, TGStickerObj sticker) { - notifyChoosingEmoji(EmojiMediaType.STICKER, true); - if (areStickersVisible) { - choosingSuggestionSent = true; - } - } - - @Override - public void onStickerPreviewChanged (StickerSmallView view, TGStickerObj otherOrThisSticker) { - notifyChoosingEmoji(EmojiMediaType.STICKER, true); - if (areStickersVisible) { - choosingSuggestionSent = true; - } - } - - @Override - public void onStickerPreviewClosed (StickerSmallView view, TGStickerObj thisSticker) { - if (!choosingSuggestionSent) { - notifyChoosingEmoji(EmojiMediaType.STICKER, false); - } - } - }; - stickerSuggestionAdapter.setStickers(stickers); - stickerSuggestionsView = new RecyclerView(context()) { - @Override - public boolean onTouchEvent (MotionEvent e) { - return areStickersVisible && getAlpha() == 1f && super.onTouchEvent(e); - } - }; - stickerSuggestionsView.setItemAnimator(null); - stickerSuggestionsView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? View.OVER_SCROLL_IF_CONTENT_SCROLLS : View.OVER_SCROLL_NEVER); - stickerSuggestionsView.setAdapter(stickerSuggestionAdapter); - stickerSuggestionsView.addOnScrollListener(new RecyclerView.OnScrollListener() { - /*@Override - public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { - if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { - notifyChoosingEmoji(EmojiMediaType.STICKER, true); - choosingSuggestionSent = true; - } - }*/ - - @Override - public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { - if (dx != 0) { - notifyChoosingEmoji(EmojiMediaType.STICKER, true); - choosingSuggestionSent = true; - } - } - }); - stickerSuggestionsView.setLayoutManager(manager); - stickerSuggestionsView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, stickersListTotalHeight)); - stickerSuggestionsWrap.addView(stickerSuggestionsView); - - FrameLayoutFix.LayoutParams fparams; - fparams = FrameLayoutFix.newParams(Screen.dp(27f), stickerArrowHeight); - fparams.topMargin = stickersListTopHeight; - fparams.leftMargin = Screen.dp(55f) + Screen.dp(2.5f); - - stickerSuggestionsWrap.setPivotX(fparams.leftMargin + Screen.dp(27f) / 2); - stickerSuggestionsWrap.setPivotY(stickersListTopHeight + stickerArrowHeight); - - ImageView stickerSuggestionArrowView = new ImageView(context()); - stickerSuggestionArrowView.setScaleType(ImageView.ScaleType.CENTER); - stickerSuggestionArrowView.setImageResource(R.drawable.stickers_back_arrow); - stickerSuggestionArrowView.setColorFilter(new PorterDuffColorFilter(Theme.headerFloatBackgroundColor(), PorterDuff.Mode.MULTIPLY)); - addThemeSpecialFilterListener(stickerSuggestionArrowView, ColorId.overlayFilling); - stickerSuggestionArrowView.setLayoutParams(fparams); - stickerSuggestionsWrap.addView(stickerSuggestionArrowView); - } else if (isMore && stickerSuggestionAdapter.hasStickers() ) { - stickerSuggestionAdapter.addStickers(stickers); - } else { - stickerSuggestionAdapter.setStickers(stickers); + public boolean onRequestToggleLargeMedia (ReplyBarView view, View buttonView, MessageInputContext messageContext, LinkPreview linkPreview) { + TdApi.LinkPreviewOptions options = messageContext.takeOutputLinkPreviewOptions(false); + if (options.isDisabled) { + return false; } - - setStickersVisible(true); - } - - private void setStickersVisible (boolean areVisible) { - if (this.areStickersVisible != areVisible) { - this.areStickersVisible = areVisible; - if (choosingSuggestionSent) { - if (!areVisible) { - notifyChoosingEmoji(EmojiMediaType.STICKER, false); - } - choosingSuggestionSent = false; - } - boolean onLayout = stickerSuggestionsWrap.getParent() == null && areVisible; - if (onLayout) { - contentView.addView(stickerSuggestionsWrap); + if (linkPreview.toggleLargeMedia()) { + options.forceSmallMedia = linkPreview.forceSmallMedia(); + options.forceLargeMedia = linkPreview.forceLargeMedia(); + if (StringUtils.isEmpty(options.url)) { + options.url = linkPreview.url; } - animateStickersFactor(areVisible ? 1f : 0f, onLayout); - } - } - - private FactorAnimator stickersAnimator; - private static final int ANIMATOR_STICKERS = 1; - - private void animateStickersFactor (float toFactor, boolean onLayout) { - if (stickersAnimator == null) { - stickersAnimator = new FactorAnimator(ANIMATOR_STICKERS, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180l, stickersFactor); - } - if (toFactor == 1f && stickersFactor == 0f) { - stickersAnimator.setInterpolator(AnimatorUtils.OVERSHOOT_INTERPOLATOR); - stickersAnimator.setDuration(210l); - } else { - stickersAnimator.setInterpolator(AnimatorUtils.DECELERATE_INTERPOLATOR); - stickersAnimator.setDuration(100l); + inputView.setTextChangedSinceChatOpened(true); + showLinkPreviewHint(Lang.getString(linkPreview.getOutputShowLargeMedia() ? R.string.LinkPreviewEnlarged : R.string.LinkPreviewMinimized)); + return true; } - stickersAnimator.animateTo(toFactor, onLayout ? stickerSuggestionsWrap : null); - } - - private void onStickersDisappeared () { - stickerSuggestionAdapter.setStickers(null); - contentView.removeView(stickerSuggestionsWrap); + return false; } - private float stickersFactor; - - private void setStickersFactor (float factor) { - if (this.stickersFactor != factor) { - this.stickersFactor = factor; - - final float scale = .8f + .2f * factor; - stickerSuggestionsWrap.setScaleX(scale); - stickerSuggestionsWrap.setScaleY(scale); - stickerSuggestionsWrap.setAlpha(Math.min(1f, Math.max(0f, factor))); + @Override + public boolean onRequestToggleShowAbove (ReplyBarView view, View buttonView, MessageInputContext messageContext) { + TdApi.LinkPreviewOptions options = messageContext.takeOutputLinkPreviewOptions(false); + if (!options.isDisabled) { + options.showAboveText = !options.showAboveText; + showLinkPreviewHint(Lang.getString(options.showAboveText ? R.string.LinkPreviewShowAbove : R.string.LinkPreviewShowBelow)); + return true; } + return false; } - public void onUsernamePick (String username) { - inputView.setInput("@" + username + " ", true, true); - } - - // Link preview - - private String attachedLink; - private TdApi.WebPage attachedPreview; - private String dismissedLink; + private TooltipOverlayView.TooltipInfo linkPreviewHint; - private void closeLinkPreview () { - if ((flags & FLAG_REPLY_ANIMATING) == 0) { - dismissedLink = attachedLink; - if (editingMessage != null) { - showCurrentEdit(); - } else if (replyMessage != null) { - showCurrentReply(); - } else { - closeReplyView(); - } - inputView.setTextChangedSinceChatOpened(true); + private void showLinkPreviewHint (CharSequence text) { + if (linkPreviewHint == null) { + linkPreviewHint = context().tooltipManager().builder(replyBarView.getLinkPreviewToggleView()).locate((targetView, rect) -> replyBarView.getLinkPreviewToggleView().getTargetBounds(targetView, rect)) + .icon(R.drawable.baseline_info_24) + .ignoreViewScale(true) + .controller(this) + .show(tdlib, text); + } else { + linkPreviewHint.reset(context().tooltipManager().newContent(tdlib, text, 0), R.drawable.baseline_info_24); + linkPreviewHint.show(); } + linkPreviewHint.hideDelayed(false); } - private boolean allowSecretPreview () { - return !isSecretChat() || Settings.instance().needTutorial(Settings.TUTORIAL_SECRET_LINK_PREVIEWS) || Settings.instance().needSecretLinkPreviews(); - } - - private boolean getCurrentAllowLinkPreview () { - return allowSecretPreview() && dismissedLink != null && dismissedLink.equals(attachedLink); - } - - private boolean obtainAllowLinkPreview (boolean close) { - if (!allowSecretPreview()) { - return false; - } - if (dismissedLink != null) { - boolean equal = dismissedLink.equals(attachedLink); - dismissedLink = null; - return !equal; - } + private @NonNull TdApi.LinkPreviewOptions obtainLinkPreviewOptions (boolean close) { + TdApi.LinkPreviewOptions linkPreviewOptions = findTargetContext().takeOutputLinkPreviewOptions(true); if (close) { - attachedLink = null; - dismissedLink = null; - if (editingMessage == null && replyMessage == null) { - closeReplyView(); - } + findTargetContext().reset(); + updateReplyBarVisibility(false); } - return true; + return linkPreviewOptions; } - public void ignoreLinkPreview (final String link, final TdApi.WebPage page) { - attachedLink = dismissedLink = link; - attachedPreview = page; + private final MessageInputContext draftContext = new MessageInputContext(this, tdlib, null); + private MessageInputContext editContext; + + private MessageInputContext findTargetContext () { + return isEditingMessage() ? editContext : draftContext; } - public void showLinkPreview (final String link, final TdApi.WebPage page) { + public void showLinkPreview (@Nullable FoundUrls foundUrls) { if (inPreviewMode || isInForceTouchMode()) { return; } - - String previousLink = attachedLink; - - attachedLink = link; - attachedPreview = page; - dismissedLink = null; - - if (link == null) { - if (editingMessage != null) { - showCurrentEdit(); - } else if (replyMessage != null) { - showCurrentReply(); - } else { - closeReplyView(); - } - return; - } - - replyView.setWebPage(link, page); - - if (previousLink == null) { - openReplyView(); + MessageInputContext targetContext = findTargetContext(); + if (targetContext.setFoundUrls(foundUrls)) { + updateReplyBarVisibility(true); } } private boolean showingLinkPreview () { - return attachedLink != null && (dismissedLink == null || !dismissedLink.equals(attachedLink)); - } - - private void showCurrentLinkPreview () { - replyView.setWebPage(attachedLink, attachedPreview); + return findTargetContext().isVisible(); } // Guess about the future RecyclerView height @@ -8003,16 +8151,6 @@ protected int makeGuessAboutForcePreviewHeight () { return getForcePreviewHeight(/* hasHeader */ true, /* hasFooter */ true); } - /*public int getForceTouchModeOffset () { - int height = Screen.currentHeight() - HeaderView.getSize(true); - - if (tdlib.hasWritePermission(chat) || (tdlib.isChannel(chat.id) && !TD.isMember(tdlib.chatStatus(chat.id)))) { - height -= Screen.dp(49f); - } - - return (height - makeGuessAboutForcePreviewHeight()); - }*/ - // Commands public boolean getCommandsState () { @@ -8130,6 +8268,7 @@ private void openEmojiKeyboard () { if (emojiLayout == null) { emojiLayout = new EmojiLayout(context()); emojiLayout.initWithMediasEnabled(this, true, this, this, false); + emojiLayout.setAllowPremiumFeatures(isSelfChat()); bottomWrap.addView(emojiLayout); if (inputView != null) { textFormattingLayout = new TextFormattingLayout(context(), this, inputView); @@ -8139,7 +8278,7 @@ private void openEmojiKeyboard () { } contentView.getViewTreeObserver().addOnPreDrawListener(emojiLayout); } else { - emojiLayout.setVisibility(View.VISIBLE); + emojiLayout.setVisibility(View.VISIBLE); // todo: set invisible if message options is visible ? } // updateButtonsY(); @@ -8241,6 +8380,11 @@ public void onEnterEmoji (String emoji) { inputView.onEmojiSelected(emoji); } + @Override + public void onEnterCustomEmoji (TGStickerObj sticker) { + inputView.onCustomEmojiSelected(sticker); + } + @Override public long getStickerSuggestionsChatId () { return getChatId(); @@ -8315,6 +8459,19 @@ public final void onMessagesFrameChanged () { manager.onViewportMeasure(); } + private final int[] cursorCoordinates = new int[2], symbolUnderCursorPosition = new int[2]; + + public int[] getInputCursorOffset () { + if (inputView == null) { + cursorCoordinates[0] = cursorCoordinates[1] = 0; + return cursorCoordinates; + } + inputView.getSymbolUnderCursorPosition(symbolUnderCursorPosition); + cursorCoordinates[0] = symbolUnderCursorPosition[0] + inputView.getLeft() + inputView.getPaddingLeft(); + cursorCoordinates[1] = symbolUnderCursorPosition[1] - inputView.getLineHeight() + Screen.currentHeight() - getInputOffset(true) - Screen.dp(40); + return cursorCoordinates; + } + public int getInputOffset (boolean excludeTranslation) { if (bottomWrap.getVisibility() == View.GONE) { return 0; @@ -8340,11 +8497,11 @@ public LinearLayout getBottomWrap () { // Send sticker private boolean sendContent (View view, int rightId, int defaultRes, int specificRes, int specificUntilRes, boolean canReply, TdApi.MessageSendOptions sendOptions, Future content) { - return sendContent(view, rightId, defaultRes, specificRes, specificUntilRes, canReply ? this::obtainReplyId : null, sendOptions, content); + return sendContent(view, rightId, defaultRes, specificRes, specificUntilRes, canReply ? this::obtainReplyTo : null, sendOptions, content); } private boolean showGifRestriction (View view) { - return showRestriction(view, RightId.SEND_OTHER_MESSAGES, R.string.ChatDisabledStickers, R.string.ChatRestrictedStickers, R.string.ChatRestrictedStickersUntil); + return showSlowModeRestriction(view, null) || showRestriction(view, RightId.SEND_OTHER_MESSAGES, R.string.ChatDisabledStickers, R.string.ChatRestrictedStickers, R.string.ChatRestrictedStickersUntil); } public boolean showPhotoVideoRestriction (View view) { // TODO separate photos & videos @@ -8357,6 +8514,11 @@ public boolean showPhotoVideoRestriction (View view, boolean checkPhotos, boolea if (photosStatus == null && videosStatus == null) { return false; } + + if (showSlowModeRestriction(view, null)) { + return true; + } + if (videosStatus == null || (videosStatus.isGlobal() && photosStatus != null && !photosStatus.isGlobal())) { // photo return showRestriction(view, RightId.SEND_PHOTOS, R.string.ChatDisabledPhoto, R.string.ChatRestrictedPhoto, R.string.ChatRestrictedPhotoUntil); @@ -8373,6 +8535,21 @@ public boolean showRestriction (View view, @RightId int rightId) { return showRestriction(view, text); } + public boolean showSlowModeRestriction (View v, @Nullable TdApi.MessageSendOptions sendOptions) { + CharSequence restriction = tdlib().getSlowModeRestrictionText(getChatId(), sendOptions != null ? sendOptions.schedulingState : null); + if (restriction != null) { + if (v == sendButton || v == recordButton) { + showBottomHint(restriction, true); + isSlowModeRestrictionHintVisible = true; + return true; + } + showRestriction(v, restriction); + return true; + } + + return false; + } + public boolean showRestriction (View view, CharSequence restrictionText) { if (restrictionText != null) { if (view == sendButton || view == recordButton) { @@ -8392,11 +8569,11 @@ public boolean showRestriction (View view, @RightId int rightId, int defaultRes, return showRestriction(view, restrictionText); } - private boolean sendContent (View view, @RightId int rightId, int defaultRes, int specificRes, int specificUntilRes, FutureLong replyToMessageId, TdApi.MessageSendOptions initialSendOptions, Future content) { - if (showRestriction(view, rightId, defaultRes, specificRes, specificUntilRes)) + private boolean sendContent (View view, @RightId int rightId, int defaultRes, int specificRes, int specificUntilRes, Future replyTo, TdApi.MessageSendOptions initialSendOptions, Future content) { + if (showSlowModeRestriction(view, initialSendOptions) || showRestriction(view, rightId, defaultRes, specificRes, specificUntilRes)) return false; pickDateOrProceed(initialSendOptions, (modifiedSendOptions, disableMarkdown) -> { - tdlib.sendMessage(chat.id, getMessageThreadId(), replyToMessageId != null ? replyToMessageId.getLongValue() : 0, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), content.getValue(), null); + tdlib.sendMessage(chat.id, getMessageThreadId(), replyTo != null ? replyTo.getValue() : null, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), content.getValue(), null); }); return true; } @@ -8408,6 +8585,14 @@ private boolean sendSticker (View view, TdApi.Sticker sticker, String emoji, boo if (Td.isPremium(sticker) && tdlib.ui().showPremiumAlert(this, view, TdlibUi.PremiumFeature.STICKER)) { return false; } + if (Td.customEmojiId(sticker) != 0 && canWriteMessages() && inputView != null) { + inputView.onCustomEmojiSelected(sticker); + return false; + } + if (Td.customEmojiId(sticker) != 0 && canWriteMessages() && inputView != null) { + inputView.onCustomEmojiSelected(sticker); + return false; + } return sendContent(view, RightId.SEND_OTHER_MESSAGES, R.string.ChatDisabledStickers, R.string.ChatRestrictedStickers, R.string.ChatRestrictedStickersUntil, allowReply, initialSendOptions, () -> new TdApi.InputMessageSticker(new TdApi.InputFileId(sticker.sticker.id), null, 0, 0, emoji)); } @@ -8419,13 +8604,13 @@ private boolean sendAnimation (View view, TdApi.Animation animation, boolean all return sendContent(view, RightId.SEND_OTHER_MESSAGES, R.string.ChatDisabledGifs, R.string.ChatRestrictedGifs, R.string.ChatRestrictedGifsUntil, allowReply, Td.newSendOptions(), () -> TD.toInputMessageContent(animation)); } - private void sendDice (View view, String emoji, long messageId) { + private void sendDice (View view, String emoji) { int disabledRes, restrictedRes, restrictedUntilRes; - if (TD.EMOJI_DART.textRepresentation.equals(emoji)) { + if (ContentPreview.EMOJI_DART.textRepresentation.equals(emoji)) { disabledRes = R.string.ChatDisabledDart; restrictedRes = R.string.ChatRestrictedDart; restrictedUntilRes = R.string.ChatRestrictedDartUntil; - } else if (TD.EMOJI_DICE.textRepresentation.equals(emoji)) { + } else if (ContentPreview.EMOJI_DICE.textRepresentation.equals(emoji)) { disabledRes = R.string.ChatDisabledDice; restrictedRes = R.string.ChatRestrictedDice; restrictedUntilRes = R.string.ChatRestrictedDiceUntil; @@ -8434,7 +8619,7 @@ private void sendDice (View view, String emoji, long messageId) { restrictedRes = R.string.ChatRestrictedStickers; restrictedUntilRes = R.string.ChatRestrictedStickersUntil; } - sendContent(view, RightId.SEND_OTHER_MESSAGES, disabledRes, restrictedRes, restrictedUntilRes, () -> messageId, Td.newSendOptions(), () -> new TdApi.InputMessageDice(emoji, false)); + sendContent(view, RightId.SEND_OTHER_MESSAGES, disabledRes, restrictedRes, restrictedUntilRes, null, Td.newSendOptions(), () -> new TdApi.InputMessageDice(emoji, false)); } // Event log @@ -8890,7 +9075,7 @@ private void setSendVisible (boolean isVisible, boolean animated) { public void sendText (boolean applyMarkdown, TdApi.MessageSendOptions sendOptions) { if (inputView != null) { - sendText(inputView.getOutputText(applyMarkdown), true, applyMarkdown, true, obtainAllowLinkPreview(false), sendOptions); + sendText(inputView.getOutputText(applyMarkdown), true, applyMarkdown, true, true, sendOptions); } } @@ -8933,22 +9118,29 @@ private void setIsSendingText (boolean isSendingText) { } private void sendText (TdApi.FormattedText msg, boolean clearInput, boolean allowDice, boolean allowReply, boolean allowLinkPreview, TdApi.MessageSendOptions initialSendOptions) { - if ((Td.isEmpty(msg) && !(clearInput && inputView != null && inputView.getText().length() > 0)) || !hasWritePermission() || (isSendingText && clearInput)) { + if ((Td.isEmpty(msg) && !(clearInput && inputView != null && inputView.getText().length() > 0)) || (isSendingText && clearInput)) { + return; + } + if (!hasSendBasicMessagePermission()) { + context().tooltipManager().builder(sendButton != null ? sendButton : inputView).show(tdlib, R.string.MessageInputTextDisabledHint).hideDelayed(); return; } long chatId = getChatId(); long messageThreadId = getMessageThreadId(); - long replyToMessageId = allowReply ? (clearInput ? getCurrentReplyId() : obtainReplyId()) : 0; + final @Nullable TdApi.InputMessageReplyTo replyTo = allowReply ? (clearInput ? getCurrentReplyId() : obtainReplyTo()) : null; + final TdApi.LinkPreviewOptions linkPreviewOptions = allowLinkPreview ? obtainLinkPreviewOptions(false) : new TdApi.LinkPreviewOptions( + true, "", false, false, false + ); TdApi.InputMessageContent content; if (allowDice && tdlib.shouldSendAsDice(msg)) { int disabledRes, restrictedRes, restrictedUntilRes; - if (TD.EMOJI_DART.textRepresentation.equals(msg.text)) { + if (ContentPreview.EMOJI_DART.textRepresentation.equals(msg.text)) { disabledRes = R.string.ChatDisabledDart; restrictedRes = R.string.ChatRestrictedDart; restrictedUntilRes = R.string.ChatRestrictedDartUntil; - } else if (TD.EMOJI_DICE.textRepresentation.equals(msg.text)) { + } else if (ContentPreview.EMOJI_DICE.textRepresentation.equals(msg.text)) { disabledRes = R.string.ChatDisabledDice; restrictedRes = R.string.ChatRestrictedDice; restrictedUntilRes = R.string.ChatRestrictedDiceUntil; @@ -8962,13 +9154,17 @@ private void sendText (TdApi.FormattedText msg, boolean clearInput, boolean allo } content = new TdApi.InputMessageDice(msg.text.trim(), clearInput); } else { - content = new TdApi.InputMessageText(msg, !allowLinkPreview, clearInput); + content = new TdApi.InputMessageText(msg, linkPreviewOptions, clearInput); } final TdApi.MessageSendOptions finalSendOptions = Td.newSendOptions(initialSendOptions, obtainSilentMode()); - List functions = TD.sendMessageText(chatId, messageThreadId, replyToMessageId, finalSendOptions, content, tdlib.maxMessageTextLength()); + List functions = TD.sendMessageText(chatId, messageThreadId, replyTo, finalSendOptions, content, tdlib.maxMessageTextLength()); final boolean isSchedule = finalSendOptions.schedulingState != null; + if (showSlowModeRestriction(sendButton != null ? sendButton : inputView, finalSendOptions)) { + return; + } + if (clearInput) { final int expectedCount = functions.size(); final List sentMessages = new ArrayList<>(expectedCount); @@ -8980,11 +9176,11 @@ private void sendText (TdApi.FormattedText msg, boolean clearInput, boolean allo if (!isDestroyed()) { manager.setSentMessages(null); if (success) { - if (allowReply && replyToMessageId != 0 && getCurrentReplyId() == replyToMessageId) { - obtainReplyId(); + if (allowReply && replyTo != null && Td.equalsTo(getCurrentReplyId(), replyTo)) { + obtainReplyTo(); } if (allowLinkPreview) { - obtainAllowLinkPreview(true); + obtainLinkPreviewOptions(true); } inputView.setInput("", false, true); } @@ -9059,11 +9255,11 @@ public void onResult (TdApi.Object result) { } public void sendContact (TdApi.User user, boolean allowReply, TdApi.MessageSendOptions initialSendOptions) { - if (hasWritePermission()) { + if (hasSendMessagePermission(RightId.SEND_BASIC_MESSAGES)) { pickDateOrProceed(initialSendOptions, (modifiedSendOptions, disableMarkdown) -> { tdlib.sendMessage(chat.id, getMessageThreadId(), - allowReply ? obtainReplyId() : 0, + allowReply ? obtainReplyTo() : null, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), new TdApi.InputMessageContact(new TdApi.Contact(user.phoneNumber, user.firstName, user.lastName, null, user.id)), null @@ -9073,32 +9269,32 @@ public void sendContact (TdApi.User user, boolean allowReply, TdApi.MessageSendO } public void shareMyContact (boolean allowReply) { - shareMyContact(allowReply ? obtainReplyId() : 0); + shareMyContact(allowReply ? obtainReplyTo() : null); } - public void shareMyContact (long forceReplyMessageId) { - if (hasWritePermission()) { + public void shareMyContact (@Nullable TdApi.InputMessageReplyTo forceReplyTo) { + if (hasSendMessagePermission(RightId.SEND_BASIC_MESSAGES)) { TdApi.User user = tdlib.myUser(); if (user != null) { pickDateOrProceed(Td.newSendOptions(), (modifiedSendOptions, disableMarkdown) -> { - tdlib.sendMessage(chat.id, getMessageThreadId(), forceReplyMessageId, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), new TdApi.InputMessageContact(new TdApi.Contact(user.phoneNumber, user.firstName, user.lastName, null, user.id)), null); + tdlib.sendMessage(chat.id, getMessageThreadId(), forceReplyTo, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), new TdApi.InputMessageContact(new TdApi.Contact(user.phoneNumber, user.firstName, user.lastName, null, user.id)), null); }); } } } public void send (TdApi.InputMessageContent content, boolean allowReply, TdApi.MessageSendOptions initialSendOptions, RunnableData after) { - if (hasWritePermission()) { // FIXME RightId.SEND_POLLS + if (tdlib().getRestrictionText(chat, content) == null) { pickDateOrProceed(initialSendOptions, (modifiedSendOptions, disableMarkdown) -> { - tdlib.sendMessage(chat.id, getMessageThreadId(), allowReply ? obtainReplyId() : 0, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), content, after); + tdlib.sendMessage(chat.id, getMessageThreadId(), allowReply ? obtainReplyTo() : null, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), content, after); }); } } public void sendInlineQueryResult (long inlineQueryId, String id, boolean allowReply, boolean clearInput, TdApi.MessageSendOptions initialSendOptions) { - if (hasWritePermission()) { // FIXME RightId.SEND_OTHER + if (hasSendMessagePermission(RightId.SEND_OTHER_MESSAGES)) { pickDateOrProceed(initialSendOptions, (modifiedSendOptions, disableMarkdown) -> { - tdlib.sendInlineQueryResult(chat.id, getMessageThreadId(), allowReply ? obtainReplyId() : 0, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), inlineQueryId, id); + tdlib.sendInlineQueryResult(chat.id, getMessageThreadId(), allowReply ? obtainReplyTo() : null, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), inlineQueryId, id); if (clearInput) { inputView.setInput("", false, true); inputView.getInlineSearchContext().resetInlineBotsCache(); @@ -9108,22 +9304,22 @@ public void sendInlineQueryResult (long inlineQueryId, String id, boolean allowR } public void sendAudio (TdApi.Audio audio, boolean allowReply) { - if (hasWritePermission()) { + if (hasSendMessagePermission(RightId.SEND_AUDIO)) { pickDateOrProceed(Td.newSendOptions(), (modifiedSendOptions, disableMarkdown) -> { - tdlib.sendMessage(chat.id, getMessageThreadId(), allowReply ? obtainReplyId() : 0, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), TD.toInputMessageContent(audio), null); + tdlib.sendMessage(chat.id, getMessageThreadId(), allowReply ? obtainReplyTo() : null, Td.newSendOptions(modifiedSendOptions, obtainSilentMode()), TD.toInputMessageContent(audio), null); }); } } public void sendMusic (View view, List musicFiles, boolean needGroupMedia, boolean allowReply, TdApi.MessageSendOptions initialSendOptions) { - if (!showRestriction(view, RightId.SEND_AUDIO)) { + if (!showSlowModeRestriction(view, initialSendOptions) && !showRestriction(view, RightId.SEND_AUDIO)) { TdApi.InputMessageContent[] content = new TdApi.InputMessageContent[musicFiles.size()]; for (int i = 0; i < content.length; i++) { MediaBottomFilesController.MusicEntry musicFile = musicFiles.get(i); content[i] = tdlib.filegen().createThumbnail(new TdApi.InputMessageAudio(TD.createInputFile(musicFile.getPath(), musicFile.getMimeType()), null, (int) (musicFile.getDuration() / 1000l), musicFile.getTitle(), musicFile.getArtist(), null), isSecretChat()); } TdApi.MessageSendOptions finalSendOptions = Td.newSendOptions(initialSendOptions, obtainSilentMode()); - List> functions = TD.toFunctions(chat.id, getMessageThreadId(), allowReply ? obtainReplyId() : 0, finalSendOptions, content, needGroupMedia); + List> functions = TD.toFunctions(chat.id, getMessageThreadId(), allowReply ? obtainReplyTo() : null, finalSendOptions, content, needGroupMedia); for (TdApi.Function function : functions) { tdlib.client().send(function, tdlib.messageHandler()); } @@ -9131,25 +9327,25 @@ public void sendMusic (View view, List mu } public boolean sendRecord (View view, final TGRecord record, boolean allowReply, TdApi.MessageSendOptions initialSendOptions) { - if (showRestriction(view, RightId.SEND_VOICE_NOTES)) { + if (showSlowModeRestriction(view, initialSendOptions) || showRestriction(view, RightId.SEND_VOICE_NOTES)) { return false; } final long chatId = chat.id; - final long replyMessageId = allowReply ? obtainReplyId() : 0; + final TdApi.InputMessageReplyTo replyTo = allowReply ? obtainReplyTo() : null; TdApi.MessageSendOptions finalSendOptions = Td.newSendOptions(initialSendOptions, obtainSilentMode()); if (record.getWaveform() == null) { Background.instance().post(() -> { byte[] waveform = N.getWaveform(record.getPath()); - tdlib.sendMessage(chatId, getMessageThreadId(), replyMessageId, finalSendOptions, new TdApi.InputMessageVoiceNote(record.toInputFile(), record.getDuration(), waveform, null), null); + tdlib.sendMessage(chatId, getMessageThreadId(), replyTo, finalSendOptions, new TdApi.InputMessageVoiceNote(record.toInputFile(), record.getDuration(), waveform, null), null); }); } else { - tdlib.sendMessage(chatId, getMessageThreadId(), replyMessageId, finalSendOptions, new TdApi.InputMessageVoiceNote(record.toInputFile(), record.getDuration(), record.getWaveform(), null), null); + tdlib.sendMessage(chatId, getMessageThreadId(), replyTo, finalSendOptions, new TdApi.InputMessageVoiceNote(record.toInputFile(), record.getDuration(), record.getWaveform(), null), null); } return true; } public void forwardMessage (TdApi.Message message) { // TODO remove all related to Forward stuff to replace with ShareLayout - if (hasWritePermission()) { + if (tdlib.getRestrictionText(chat, message) == null) { tdlib.forwardMessage(chat.id, getMessageThreadId(), message.chatId, message.id, Td.newSendOptions(obtainSilentMode())); } } @@ -9239,7 +9435,7 @@ public void onActivityResult (int requestCode, int resultCode, Intent data) { case Intents.ACTIVITY_RESULT_VIDEO_CAPTURE: { File file = Intents.takeLastOutputMedia(); boolean isVideo = requestCode == Intents.ACTIVITY_RESULT_VIDEO_CAPTURE; - if (showRestriction(mediaButton, isVideo ? RightId.SEND_VIDEOS : RightId.SEND_PHOTOS)) { + if (showSlowModeRestriction(mediaButton, null) || showRestriction(mediaButton, isVideo ? RightId.SEND_VIDEOS : RightId.SEND_PHOTOS)) { return; } if (file != null) { @@ -9296,7 +9492,7 @@ stack, areScheduledOnly() } else if (requestCode == Intents.ACTIVITY_RESULT_GALLERY_FILE) { sendFiles(mediaButton, Collections.singletonList(imagePath), false, true, Td.newSendOptions()); } else { - sendPhotoCompressed(imagePath, 0, true); + sendPhotoCompressed(imagePath, null, true); } break; @@ -9304,21 +9500,21 @@ stack, areScheduledOnly() case Intents.ACTIVITY_RESULT_AUDIO: { final Uri path = data.getData(); if (path == null) break; - if (showRestriction(mediaButton, RightId.SEND_AUDIO)) { + if (showSlowModeRestriction(mediaButton, null) || showRestriction(mediaButton, RightId.SEND_AUDIO)) { return; } final String audioPath = U.tryResolveFilePath(path); if (audioPath != null) { final long chatId = chat.id; final boolean disableNotification = obtainSilentMode(); - final long replyToMessageId = obtainReplyId(); + final TdApi.InputMessageReplyTo replyTo = obtainReplyTo(); Background.instance().post(() -> { AudioFile file; file = new AudioFile(audioPath); file.loadId3Tags(); - tdlib.sendMessage(chatId, getMessageThreadId(), replyToMessageId, Td.newSendOptions(disableNotification), new TdApi.InputMessageAudio(TD.createInputFile(audioPath), null, file.getDuration(), file.getTitle(), file.getPerformer(), null)); + tdlib.sendMessage(chatId, getMessageThreadId(), replyTo, Td.newSendOptions(disableNotification), new TdApi.InputMessageAudio(TD.createInputFile(audioPath), null, file.getDuration(), file.getTitle(), file.getPerformer(), null)); }); } break; @@ -9327,10 +9523,14 @@ stack, areScheduledOnly() } public void sendFiles (View view, final List paths, boolean needGroupMedia, boolean allowReply, TdApi.MessageSendOptions initialSendOptions) { + if (showSlowModeRestriction(view, initialSendOptions)) { + return; + } + final long chatId = chat.id; final boolean isSecretChat = isSecretChat(); final TdApi.MessageSendOptions finalSendOptions = Td.newSendOptions(initialSendOptions, obtainSilentMode()); - final long replyMessageId = allowReply ? obtainReplyId() : 0; + final TdApi.InputMessageReplyTo replyTo = allowReply ? obtainReplyTo() : null; boolean allowAudio = tdlib.getRestrictionStatus(chat, RightId.SEND_AUDIO) == null; boolean allowDocs = tdlib.getRestrictionStatus(chat, RightId.SEND_DOCS) == null; boolean allowVideos = tdlib.getRestrictionStatus(chat, RightId.SEND_VIDEOS) == null; @@ -9359,15 +9559,15 @@ public void sendFiles (View view, final List paths, boolean needGroupMed content.set(i, tdlib.filegen().createThumbnail(inputMessageContent, isSecretChat)); } - List> functions = TD.toFunctions(chatId, getMessageThreadId(), replyMessageId, finalSendOptions, content.toArray(new TdApi.InputMessageContent[0]), needGroupMedia); + List> functions = TD.toFunctions(chatId, getMessageThreadId(), replyTo, finalSendOptions, content.toArray(new TdApi.InputMessageContent[0]), needGroupMedia); for (TdApi.Function function : functions) { tdlib.client().send(function, tdlib.messageHandler()); } }); } - public void sendPhotoCompressed (final String path, final int ttl, final boolean allowReply) { - if (showRestriction(mediaButton, RightId.SEND_PHOTOS)) { + public void sendPhotoCompressed (final String path, final @Nullable TdApi.MessageSelfDestructType selfDestructType, final boolean allowReply) { + if (showSlowModeRestriction(mediaButton, null) || showRestriction(mediaButton, RightId.SEND_PHOTOS)) { return; } if (StringUtils.isEmpty(path)) { @@ -9375,7 +9575,7 @@ public void sendPhotoCompressed (final String path, final int ttl, final boolean } if (path != null) { final long chatId = chat.id; - final long replyToMessageId = allowReply ? obtainReplyId() : 0; + final TdApi.InputMessageReplyTo replyTo = allowReply ? obtainReplyTo() : null; final boolean silent = obtainSilentMode(); final boolean isSecret = isSecretChat(); @@ -9394,8 +9594,8 @@ public void sendPhotoCompressed (final String path, final int ttl, final boolean height = sampledHeight; } TdApi.InputFileGenerated inputFile = PhotoGenerationInfo.newFile(path, U.getRotationForExifOrientation(orientation)); - TdApi.InputMessagePhoto photo = tdlib.filegen().createThumbnail(new TdApi.InputMessagePhoto(inputFile, null, null, width, height, null, ttl, false), isSecret); - tdlib.sendMessage(chatId, getMessageThreadId(), replyToMessageId, Td.newSendOptions(silent), photo); + TdApi.InputMessagePhoto photo = tdlib.filegen().createThumbnail(new TdApi.InputMessagePhoto(inputFile, null, null, width, height, null, selfDestructType, false), isSecret); + tdlib.sendMessage(chatId, getMessageThreadId(), replyTo, Td.newSendOptions(silent), photo); }); } } @@ -9408,14 +9608,14 @@ public boolean sendPhotosAndVideosCompressed (final ImageGalleryFile[] files, fi // TODO check RightId.SEND_PHOTOS / RightId.SEND_VIDEOS final long chatId = chat.id; - final long replyToMessageId = obtainReplyId(); + final TdApi.InputMessageReplyTo replyTo = obtainReplyTo(); final boolean isSecretChat = isSecretChat(); Media.instance().post(() -> { final TdApi.InputMessageContent[] inputContent = new TdApi.InputMessageContent[files.length]; int i = 0; for (ImageGalleryFile file : files) { - if (file.getTTL() > 0 && asFiles) + if (file.getSelfDestructType() != null && asFiles) throw new IllegalArgumentException(); TdApi.InputMessageContent content; if (file.isVideo()) { @@ -9452,10 +9652,10 @@ public boolean sendPhotosAndVideosCompressed (final ImageGalleryFile[] files, fi TdApi.FormattedText caption = file.getCaption(true, !disableMarkdown); if (asFiles && !forceVideo) { content = tdlib.filegen().createThumbnail(TD.toInputMessageContent(file.getFilePath(), inputVideo, fileInfo, caption, hasSpoiler), isSecretChat); - } else if (sendAsAnimation && file.getTTL() == 0 && (files.length == 1 || !needGroupMedia)) { + } else if (sendAsAnimation && file.getSelfDestructType() == null && (files.length == 1 || !needGroupMedia)) { content = tdlib.filegen().createThumbnail(new TdApi.InputMessageAnimation(inputVideo, null, null, file.getVideoDuration(true), width, height, caption, hasSpoiler), isSecretChat); } else { - content = tdlib.filegen().createThumbnail(new TdApi.InputMessageVideo(inputVideo, null, null, file.getVideoDuration(true), width, height, U.canStreamVideo(inputVideo), caption, file.getTTL(), hasSpoiler), isSecretChat); + content = tdlib.filegen().createThumbnail(new TdApi.InputMessageVideo(inputVideo, null, null, file.getVideoDuration(true), width, height, U.canStreamVideo(inputVideo), caption, file.getSelfDestructType(), hasSpoiler), isSecretChat); } } else { int[] size = new int[2]; @@ -9476,13 +9676,13 @@ public boolean sendPhotosAndVideosCompressed (final ImageGalleryFile[] files, fi if (asFiles) { content = tdlib.filegen().createThumbnail(new TdApi.InputMessageDocument(inputFile, null, false, caption), isSecretChat); } else { - content = tdlib.filegen().createThumbnail(new TdApi.InputMessagePhoto(inputFile, null, null, width, height, caption, file.getTTL(), hasSpoiler), isSecretChat); + content = tdlib.filegen().createThumbnail(new TdApi.InputMessagePhoto(inputFile, null, null, width, height, caption, file.getSelfDestructType(), hasSpoiler), isSecretChat); } } inputContent[i] = content; i++; } - List> functions = TD.toFunctions(chatId, getMessageThreadId(), replyToMessageId, options, inputContent, needGroupMedia); + List> functions = TD.toFunctions(chatId, getMessageThreadId(), replyTo, options, inputContent, needGroupMedia); for (TdApi.Function function : functions) { tdlib.client().send(function, tdlib.messageHandler()); } @@ -9526,7 +9726,7 @@ public boolean sendPhotosAndVideosCompressed (final ImageGalleryFile[] files, fi private CancellableRunnable broadcastActor; private void checkBroadcastingSomeAction () { - boolean needBroadcast = broadcastingAction != 0 && context.getActivityState() == UI.STATE_RESUMED && !isDestroyed(); + boolean needBroadcast = broadcastingAction != 0 && context.getActivityState() == UI.State.RESUMED && !isDestroyed(); if (this.broadcastingSomeAction != needBroadcast) { if (needBroadcast) { broadcastActor = new CancellableRunnable() { @@ -9591,9 +9791,14 @@ public void setChatAction (@TdApi.ChatAction.Constructors int action, boolean se } } }; - long messageThreadId = getMessageThreadId(); - if (messageThreadId == 0 && replyMessage != null) { - messageThreadId = replyMessage.messageThreadId != 0 ? replyMessage.messageThreadId : replyMessage.id; + long messageThreadId; + if (isEditingMessage()) { + messageThreadId = editContext.message.messageThreadId; + } else { + messageThreadId = getMessageThreadId(); + if (messageThreadId == 0 && reply != null) { + messageThreadId = reply.message.messageThreadId != 0 ? reply.message.messageThreadId : reply.message.id; + } } if (set) { int time = (int) (SystemClock.uptimeMillis() / 1000l); @@ -9628,8 +9833,19 @@ public void notifyChoosingEmoji (int emojiType, boolean isChoosingEmoji) { switch (emojiType) { case EmojiMediaType.STICKER: action = TdApi.ChatActionChoosingSticker.CONSTRUCTOR; + if (suggestionYDiff > Screen.dp(50) || stickerPreviewIsVisible) { + hideEmojiSuggestionsTemporarily(); + } else if (suggestionYDiff <= 0) { + showEmojiSuggestionsIfTemporarilyHidden(); + } break; case EmojiMediaType.EMOJI: + if (suggestionXDiff > Screen.dp(50)) { + hideStickersSuggestionsTemporarily(); + } else if (suggestionXDiff <= 0) { + showStickersSuggestionsIfTemporarilyHidden(); + } + return; case EmojiMediaType.GIF: default: return; @@ -9705,6 +9921,7 @@ public void setMediaItemVisible (int index, MediaItem item, boolean isVisible) { public void subscribeToUpdates (long chatId) { tdlib.listeners().subscribeToChatUpdates(chatId, this); + tdlib.singleUnreadReactionsManager().subscribeToUnreadSingleReactionUpdates(chatId, this); if (chatId != getHeaderChatId()) { tdlib.listeners().subscribeToChatUpdates(getHeaderChatId(), this); } @@ -9738,6 +9955,7 @@ public void subscribeToUpdates (long chatId) { public void unsubscribeFromUpdates (long chatId) { tdlib.listeners().unsubscribeFromChatUpdates(chatId, this); + tdlib.singleUnreadReactionsManager().unsubscribeFromUnreadSingleReactionUpdates(chatId, this); if (chatId != getHeaderChatId()) { tdlib.listeners().unsubscribeFromChatUpdates(getHeaderChatId(), this); } @@ -9848,6 +10066,15 @@ public void onChatHasScheduledMessagesChanged (long chatId, boolean hasScheduled }); } + @Override + public void onChatPermissionsChanged (long chatId, TdApi.ChatPermissions permissions) { + tdlib.ui().post(() -> { + if (getChatId() == chatId) { + updateBottomBar(true); + } + }); + } + @Override public void onChatReadInbox(final long chatId, final long lastReadInboxMessageId, final int unreadCount, boolean availabilityChanged) { tdlib.ui().post(() -> { @@ -9875,6 +10102,15 @@ public void onChatUnreadReactionCount (long chatId, int unreadReactionCount, boo }); } + @Override + public void onUnreadSingleReactionUpdate (long chatId, @Nullable TdApi.UnreadReaction unreadReaction) { + UI.execute(() -> { + if (getChatId() == chatId) { + updateCounters(true); + } + }); + } + @Override public void onChatReadOutbox (final long chatId, final long lastReadOutboxMessageId) { tdlib.ui().post(() -> { @@ -9917,21 +10153,26 @@ private void updateDraftMessage (long chatId, @Nullable TdApi.DraftMessage draft if (isEditingMessage()) { return; } - if (draftMessage == null || draftMessage.replyToMessageId == 0) { - closeReply(false); + TdApi.InputMessageReplyToMessage replyToMessage = + draftMessage != null && draftMessage.replyTo != null && draftMessage.replyTo.getConstructor() == TdApi.InputMessageReplyToMessage.CONSTRUCTOR ? + (TdApi.InputMessageReplyToMessage) draftMessage.replyTo : null; + if (replyToMessage == null) { + closeReply(false, true); } else { - TGMessage message = manager.getAdapter().findMessageById(draftMessage.replyToMessageId); + TGMessage message; + if (getChatId() == replyToMessage.chatId) { + message = manager.getAdapter().findMessageById(replyToMessage.messageId); + } else { + message = null; + } if (message != null) { - showReply(message.getMessage(), false, false); + showReply(message.getMessage(), replyToMessage.quote, false, false); } else { - tdlib.client().send(new TdApi.GetMessage(chatId, draftMessage.replyToMessageId), object -> tdlib.ui().post(() -> { - //noinspection UnsafeOptInUsageError + tdlib.send(new TdApi.GetMessage(replyToMessage.chatId, replyToMessage.messageId), (remoteMessage, error) -> runOnUiThreadOptional(() -> { if (getChatId() == chatId && Td.equalsTo(getDraftMessage(), draftMessage)) { - if (object.getConstructor() == TdApi.Message.CONSTRUCTOR) { - showReply((TdApi.Message) object, false, false); - } else { - closeReply(false); - } + showReply(remoteMessage, replyToMessage.quote, false, false); + } else { + closeReply(false, true); } })); } @@ -9995,6 +10236,9 @@ public void onSupergroupFullUpdated (final long supergroupId, final TdApi.Superg } if (ChatId.toSupergroupId(getChatId()) == supergroupId) { checkLinkedChat(); + if (messageSenderButton != null) { + messageSenderButton.setInSlowMode(tdlib.inSlowMode(getChatId())); + } } }); } @@ -10153,6 +10397,9 @@ private boolean sendShowingVoice (View view, TdApi.MessageSendOptions sendOption if (!isVoiceShowing) { return false; } + if (showSlowModeRestriction(view, sendOptions) || showRestriction(view, RightId.SEND_VOICE_NOTES)) { + return false; + } TGRecord record = voiceInputView.getRecord(); if (record != null) { voiceInputView.ignoreStop(); @@ -10246,7 +10493,7 @@ private void checkSearchFilteredModeButton (boolean animated) { private void setSearchByVisible (boolean isVisible) { searchByButton.setVisibility(isVisible ? View.VISIBLE : View.GONE); - searchSetTypeFilterButton.setTranslationX(isVisible ? 0: Screen.dp(-42.5f)); + searchSetTypeFilterButton.setTranslationX(isVisible ? 0 : Screen.dp(-42.5f)); } private TdApi.Function jumpToDateRequest; @@ -10657,7 +10904,7 @@ private void setSearchControlsFactor (float factor) { } private boolean needSearchControlsTranslate () { - return tdlib.isChannelChat(chat) && !hasWritePermission(); + return tdlib.isChannelChat(chat) && !canWriteMessages(); } private float getSearchControlsOffset () { @@ -10851,14 +11098,14 @@ public boolean isCameraButtonVisibleOnAttachPanel () { } public HapticMenuHelper.MenuItem createHapticSenderItem (int id, TdApi.MessageSender sender, boolean useUsername, boolean isLocked) { - String title = useUsername ? tdlib.senderName(sender): Lang.getString(R.string.SendAs); + String title = useUsername ? tdlib.senderName(sender) : Lang.getString(R.string.SendAs); if (tdlib.isSelfSender(sender)) { return new HapticMenuHelper.MenuItem(id, title, Lang.getString(R.string.YourAccount), R.drawable.dot_baseline_acc_personal_24, tdlib, sender, false); } else if (!tdlib.isChannel(sender)) { return new HapticMenuHelper.MenuItem(id, title, Lang.getString(R.string.AnonymousAdmin), R.drawable.dot_baseline_acc_anon_24, tdlib, sender, false); } else { String username = tdlib.chatUsername(Td.getSenderId(sender)); - String subtitle = useUsername && !StringUtils.isEmpty(username)? ("@" + username): tdlib.getMessageSenderTitle(sender); + String subtitle = useUsername && !StringUtils.isEmpty(username) ? ("@" + username) : tdlib.getMessageSenderTitle(sender); return new HapticMenuHelper.MenuItem(id, title, subtitle, 0, tdlib, sender, isLocked); } } @@ -10866,34 +11113,25 @@ public HapticMenuHelper.MenuItem createHapticSenderItem (int id, TdApi.MessageSe // Call methods public static void getChatAvailableMessagesSenders (Tdlib tdlib, long chatId, @NonNull RunnableData callback) { - tdlib.send(new TdApi.GetChatAvailableMessageSenders(chatId), result -> UI.post(() -> { - switch (result.getConstructor()) { - case TdApi.ChatMessageSenders.CONSTRUCTOR: { - callback.runWithData((TdApi.ChatMessageSenders) result); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(result); - break; - } + tdlib.send(new TdApi.GetChatAvailableMessageSenders(chatId), (result, error) -> UI.post(() -> { + if (error != null) { + UI.showError(error); + } else { + callback.runWithData(result); } })); } public static void setNewMessageSender (Tdlib tdlib, long chatId, TdApi.ChatMessageSender sender, @Nullable Runnable after) { - tdlib.send(new TdApi.SetChatMessageSender(chatId, sender.sender), o -> { - if (after != null) { - tdlib.ui().post(after); - } - }); + tdlib.send(new TdApi.SetChatMessageSender(chatId, sender.sender), tdlib.typedOkHandler(after)); } private void setNewMessageSender (TdApi.ChatMessageSender sender) { - tdlib.send(new TdApi.SetChatMessageSender(getChatId(), sender.sender), tdlib.okHandler()); + tdlib.send(new TdApi.SetChatMessageSender(getChatId(), sender.sender), tdlib.typedOkHandler()); } private void setNewMessageSender (TdApi.MessageSender sender) { - tdlib.send(new TdApi.SetChatMessageSender(getChatId(), sender), tdlib.okHandler()); + tdlib.send(new TdApi.SetChatMessageSender(getChatId(), sender), tdlib.typedOkHandler()); } private void getChatAvailableMessagesSenders (Runnable after) { @@ -10912,10 +11150,10 @@ private void updateSelectMessageSenderInterface (boolean animated) { boolean cameraVisible = isCameraButtonVisibleOnAttachPanel(); boolean canSetSender = canSelectSender(); if (cameraButton != null) { - cameraButton.setVisibility(cameraVisible ? View.VISIBLE: View.GONE); //.setVisible(cameraVisible); + cameraButton.setVisibility(cameraVisible ? View.VISIBLE : View.GONE); //.setVisible(cameraVisible); } - messageSenderButton.setVisibility((canSetSender && attachButtons.getVisibility() == View.VISIBLE) ? View.VISIBLE: View.GONE); + messageSenderButton.setVisibility((canSetSender && attachButtons.getVisibility() == View.VISIBLE) ? View.VISIBLE : View.GONE); if (canSetSender) { messageSenderButton.update(getChatMessageSender(), animated); } @@ -10935,7 +11173,7 @@ public boolean callNonAnonymousProtection (long hash, View view, @NonNull Toolti } public boolean callNonAnonymousProtection (long hash, @Nullable TooltipOverlayView.TooltipBuilder tooltipBuilder) { - if (chat == null || chat.messageSenderId == null || tdlib.isSelfSender(chat.messageSenderId)) { + if (chat == null || tdlib.isSelfSender(chat.messageSenderId) || (chat.messageSenderId == null && !tdlib.isAnonymousAdmin(chat.id))) { return true; } @@ -11013,7 +11251,7 @@ public List onCreateHapticMenu (View view) { final int maxCount = 5; final boolean needMoreButton = chatAvailableSenders.length > maxCount; - final int senderButtonsCount = needMoreButton ? maxCount - 1: chatAvailableSenders.length; + final int senderButtonsCount = needMoreButton ? maxCount - 1 : chatAvailableSenders.length; List items = new ArrayList<>(Math.min(chatAvailableSenders.length, maxCount)); for (int i = 0; i < senderButtonsCount; i++) { @@ -11214,7 +11452,7 @@ private void onSetSearchFilteredShowMode (boolean inSearchMode) { searchMessagesFilterMode = inSearchMode; if (searchShowOnlyFoundButton != null) { - searchShowOnlyFoundButton.setColorFilter(Theme.getColor(inSearchMode ? ColorId.iconActive: ColorId.icon)); + searchShowOnlyFoundButton.setColorFilter(Theme.getColor(inSearchMode ? ColorId.iconActive : ColorId.icon)); } } @@ -11388,7 +11626,7 @@ public void startTranslateMessages (TGMessage message, boolean forcePopup) { translationPopup = new TranslationControllerV2.Wrapper(context, tdlib, this); translationPopup.setArguments(new TranslationControllerV2.Args(message)); translationPopup.setClickCallback(message.clickCallback()); - translationPopup.setTextColorSet(message.getTextColorSet()); + translationPopup.setTextColorSet(TextColorSets.Regular.NORMAL); translationPopup.show(); translationPopup.setDismissListener(popup -> translationPopup = null); hideCursorsForInputView(); @@ -11428,8 +11666,8 @@ public void onInputSpansChanged (InputView view) { private void setTextFormattingLayoutVisible (boolean visible) { textFormattingVisible = visible; if (emojiLayout != null && textFormattingLayout != null) { - textFormattingLayout.setVisibility(visible ? View.VISIBLE: View.GONE); - emojiLayout.optimizeForDisplayTextFormattingLayout(!visible); + textFormattingLayout.setVisibility(visible ? View.VISIBLE : View.GONE); + emojiLayout.optimizeForDisplayTextFormattingLayout(visible); if (visible) { textFormattingLayout.checkButtonsActive(false); } @@ -11443,7 +11681,7 @@ private void closeTextFormattingKeyboard () { } public @DrawableRes int getTargetIcon (boolean isMessage) { - return (textInputHasSelection || (textFormattingVisible && emojiShown)) ? R.drawable.baseline_format_text_24: EmojiLayout.getTargetIcon(isMessage); + return (textInputHasSelection || (textFormattingVisible && emojiShown)) ? R.drawable.baseline_format_text_24 : EmojiLayout.getTargetIcon(isMessage); } @Override @@ -11456,4 +11694,233 @@ public void hideCursorsForInputView () { inputView.hideSelectionCursors(); } } + + /* * */ + + + private ArrayList> stickerSuggestionItems; + private boolean canShowEmojiSuggestions; + private boolean isStickerSuggestionsTemporarilyHidden; + + public void showStickerSuggestions (@Nullable ArrayList stickers, boolean isMore) { + ArrayList> items; + if (stickers != null) { + items = new ArrayList<>(stickers.size()); + for (TGStickerObj sticker : stickers) { + items.add(new InlineResultSticker(context, tdlib, "x", new TdApi.InlineQueryResultSticker("x", sticker.getSticker()))); + } + } else { + items = null; + } + + if (!isMore) { + stickerSuggestionItems = items; + context.showInlineResults(this, tdlib, items, true, null, getInlineResultsStickerScrollListener(), getInlineResultsStickerMovementsCallback()); + } else { + if (items != null && stickerSuggestionItems != null) { + stickerSuggestionItems.addAll(items); + } + context.addInlineResults(this, items, null, getInlineResultsStickerScrollListener(), getInlineResultsStickerMovementsCallback()); + } + } + + private String lastFoundByEmoji; + private boolean needSkipMoreEmojiSuggestions; + + public void showEmojiSuggestions (@Nullable ArrayList stickers, String foundByEmoji, boolean isMore) { + if (StringUtils.equalsOrBothEmpty(lastFoundByEmoji, foundByEmoji)) { + if (!isMore || needSkipMoreEmojiSuggestions) { + if (context.hasEmojiSuggestions()) { + if (isFocused() && pagerScrollOffset < 1f) { + context.setEmojiSuggestionsVisible(true); + } + canShowEmojiSuggestions = true; + } + return; + } + needSkipMoreEmojiSuggestions = true; + } else { + lastFoundByEmoji = foundByEmoji; + needSkipMoreEmojiSuggestions = false; + } + + if (stickers == null || stickers.isEmpty()) { + if (!isMore) { + context.setEmojiSuggestions(this, null, getInlineEmojiStickerScrollListener(), this::notifyChoosingEmoji); + context().setEmojiSuggestionsVisible(false); + canShowEmojiSuggestions = false; + } + return; + } + + if (isMore && context.hasEmojiSuggestions() && context.isEmojiSuggestionsVisible()) { + context.addEmojiSuggestions(this, stickers); + } else { + context.setEmojiSuggestions(this, stickers, getInlineEmojiStickerScrollListener(), this::notifyChoosingEmoji); + } + if (isFocused() && pagerScrollOffset < 1f) { + context.setEmojiSuggestionsVisible(true); + } + + canShowEmojiSuggestions = true; + } + + private void showStickersSuggestionsIfTemporarilyHidden () { + if (isStickerSuggestionsTemporarilyHidden && stickerSuggestionItems != null) { + isStickerSuggestionsTemporarilyHidden = false; + context.showInlineResults(this, tdlib, stickerSuggestionItems, true, null, getInlineResultsStickerScrollListener(), getInlineResultsStickerMovementsCallback()); + } + } + + private void hideStickersSuggestionsTemporarily () { + isStickerSuggestionsTemporarilyHidden = true; + context.showInlineResults(this, tdlib, null, true, null); + } + + private void showEmojiSuggestionsIfTemporarilyHidden () { + if (canShowEmojiSuggestions) { + context().setEmojiSuggestionsVisible(true); + } + } + + private void hideEmojiSuggestionsTemporarily () { + context().setEmojiSuggestionsVisible(false); + } + + private void hideEmojiAndStickerSuggestionsFinally () { + onHideEmojiAndStickerSuggestionsFinally(); + context.showInlineResults(this, tdlib, null, false, null); + } + + public void onHideEmojiAndStickerSuggestionsFinally () { + canShowEmojiSuggestions = false; + stickerSuggestionItems = null; + hideEmojiSuggestionsTemporarily(); + } + + private StickerSmallView.StickerMovementCallback getInlineResultsStickerMovementsCallback () { + return new StickerSmallView.StickerMovementCallback() { + @Override + public boolean onStickerClick (StickerSmallView view, View clickView, TGStickerObj sticker, boolean isMenuClick, TdApi.MessageSendOptions sendOptions) { + if (onSendStickerSuggestion(clickView, sticker, sendOptions)) { + hideEmojiAndStickerSuggestionsFinally(); + return true; + } + + return false; + } + + @Override + public long getStickerOutputChatId () { + return getOutputChatId(); + } + + @Override + public void setStickerPressed (StickerSmallView view, TGStickerObj sticker, boolean isPressed) { + InlineResultsWrap inlineResultsWrap = context.getInlineResultsView(); + if (inlineResultsWrap != null) { + inlineResultsWrap.setStickerPressed(view, sticker, isPressed); + } + } + + @Override + public boolean canFindChildViewUnder (StickerSmallView view, int recyclerX, int recyclerY) { + return true; + } + + @Override + public boolean needsLongDelay (StickerSmallView view) { + return true; + } + + @Override + public int getStickersListTop () { + return -getInputOffset(false); + } + + @Override + public int getViewportHeight () { + return getStickerSuggestionPreviewViewportHeight(); + } + + @Override + public void onStickerPreviewOpened (StickerSmallView view, TGStickerObj sticker) { + notifyChoosingEmoji(EmojiMediaType.STICKER, stickerPreviewIsVisible = true); + } + + @Override + public void onStickerPreviewChanged (StickerSmallView view, TGStickerObj otherOrThisSticker) { + notifyChoosingEmoji(EmojiMediaType.STICKER, stickerPreviewIsVisible = true); + } + + @Override + public void onStickerPreviewClosed (StickerSmallView view, TGStickerObj thisSticker) { + notifyChoosingEmoji(EmojiMediaType.STICKER, stickerPreviewIsVisible = false); + } + }; + } + + private int suggestionXDiff = 0; + private int suggestionYDiff = 0; + private boolean stickerPreviewIsVisible; + + private RecyclerView.OnScrollListener getInlineResultsStickerScrollListener () { + suggestionYDiff = 0; + return new RecyclerView.OnScrollListener() { + @Override + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy == 0) return; + suggestionYDiff = recyclerView.computeVerticalScrollOffset(); + notifyChoosingEmoji(EmojiMediaType.STICKER, true); + } + }; + } + + private RecyclerView.OnScrollListener getInlineEmojiStickerScrollListener () { + suggestionYDiff = 0; + return new RecyclerView.OnScrollListener() { + @Override + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dx == 0) return; + suggestionXDiff = recyclerView.computeHorizontalScrollOffset(); + notifyChoosingEmoji(EmojiMediaType.EMOJI, true); + } + }; + } + + @Override + public boolean onSendStickerSuggestion (View view, TGStickerObj sticker, TdApi.MessageSendOptions initialSendOptions) { + if (lastJunkTime == 0l || SystemClock.uptimeMillis() - lastJunkTime >= JUNK_MINIMUM_DELAY) { + if (sticker.isCustomEmoji()) { + inputView.onCustomEmojiSelected(sticker, true); + return true; + } + if (showGifRestriction(view)) + return false; + pickDateOrProceed(initialSendOptions, (modifiedSendOptions, disableMarkdown) -> { + if (sendSticker(view, sticker.getSticker(), sticker.getFoundByEmoji(), true, Td.newSendOptions(modifiedSendOptions, false, Config.REORDER_INSTALLED_STICKER_SETS))) { + lastJunkTime = SystemClock.uptimeMillis(); + inputView.setInput("", false, true); + } + }); + return true; + } + return false; + } + + @Override + public int getStickerSuggestionsTop (boolean isEmoji) { + View v = context().getEmojiSuggestionsView(); + return v != null ? Views.getLocationInWindow(v)[1] : 0; + } + + @Override + public int getStickerSuggestionPreviewViewportHeight () { + return HeaderView.getSize(true) + messagesView.getMeasuredHeight(); + } + + @Override + public long getCurrentChatId () { + return getChatId(); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/PasscodeController.java b/app/src/main/java/org/thunderdog/challegram/ui/PasscodeController.java index ee75e9771a..cfdf0fca56 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/PasscodeController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/PasscodeController.java @@ -1026,7 +1026,7 @@ private void setNeedFinger (boolean need) { private boolean fingerUsed; private void checkFingerprintNeeded () { - boolean need = this.needFinger && context.getActivityState() == UI.STATE_RESUMED; + boolean need = this.needFinger && context.getActivityState() == UI.State.RESUMED; if (fingerUsed != need) { if (need) { FingerprintPassword.authenticate(this); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/PasswordController.java b/app/src/main/java/org/thunderdog/challegram/ui/PasswordController.java index f42ed0d584..8593cd4a9d 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/PasswordController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/PasswordController.java @@ -74,6 +74,7 @@ import me.vkryl.android.widget.FrameLayoutFix; import me.vkryl.core.StringUtils; import me.vkryl.core.lambda.RunnableData; +import me.vkryl.td.Td; public class PasswordController extends ViewController implements View.OnClickListener, FactorAnimator.Target, MaterialEditTextGroup.EmptyListener, MaterialEditTextGroup.DoneListener, MaterialEditTextGroup.TextChangeListener, AuthorizationListener, Handler.Callback { public static final int MODE_EDIT = 0; @@ -90,6 +91,7 @@ public class PasswordController extends ViewController public static final int MODE_CONFIRM = 11; public static final int MODE_CODE_EMAIL = 12; public static final int MODE_EMAIL_LOGIN = 13; + public static final int MODE_CUSTOM_CONFIRM = 14; private final Handler handler = new Handler(this); @@ -98,6 +100,7 @@ public static class Args { public final TdApi.PasswordState state; public final TdApi.AuthorizationState authState; public @Nullable String phoneNumber; + public @Nullable CustomConfirmDelegate confirmDelegate; public Args (int mode, TdApi.PasswordState state) { this.mode = mode; @@ -173,6 +176,11 @@ public Args setSuccessListener (@Nullable RunnableData onSuccessListener this.onSuccessListener = onSuccessListener; return this; } + + public Args setConfirmDelegate (@Nullable CustomConfirmDelegate confirmDelegate) { + this.confirmDelegate = confirmDelegate; + return this; + } } private int mode; @@ -205,8 +213,19 @@ public void setArguments (Args args) { this.state = args.state; this.authState = args.authState; this.formattedPhone = args.phoneNumber; + this.confirmDelegate = args.confirmDelegate; + } + + public interface CustomConfirmDelegate { + default CharSequence getName () { + return Lang.getString(R.string.EnterPassword); + } + boolean needNext (); + void onPasswordConfirmed (ViewController c, String password); } + private CustomConfirmDelegate confirmDelegate; + @Override public CharSequence getName () { switch (mode) { @@ -223,6 +242,9 @@ public CharSequence getName () { case MODE_UNLOCK_EDIT: { return Lang.getString(R.string.EnterPassword); } + case MODE_CUSTOM_CONFIRM: { + return confirmDelegate.getName(); + } case MODE_TRANSFER_OWNERSHIP_CONFIRM: { return Lang.getString(R.string.TransferOwnershipPasswordAlert); } @@ -289,6 +311,8 @@ private int getDoneIcon () { case MODE_CONFIRM: case MODE_TRANSFER_OWNERSHIP_CONFIRM: return R.drawable.baseline_check_24; + case MODE_CUSTOM_CONFIRM: + return confirmDelegate.needNext() ? R.drawable.baseline_arrow_forward_24 : R.drawable.baseline_check_24; } return R.drawable.baseline_arrow_forward_24; } @@ -367,6 +391,7 @@ protected View onCreateView (Context context) { break; } case MODE_TRANSFER_OWNERSHIP_CONFIRM: + case MODE_CUSTOM_CONFIRM: case MODE_CONFIRM: case MODE_UNLOCK_EDIT: { if (state != null && state.passwordHint != null && !state.passwordHint.isEmpty()) { @@ -437,11 +462,24 @@ protected View onCreateView (Context context) { switch (mode) { case MODE_TRANSFER_OWNERSHIP_CONFIRM: + case MODE_CUSTOM_CONFIRM: case MODE_CONFIRM: case MODE_UNLOCK_EDIT: case MODE_LOGIN: { updatePasswordResetTextViews(); - hint = Lang.getString(mode == MODE_TRANSFER_OWNERSHIP_CONFIRM ? R.string.TransferOwnershipPasswordAlertHint : R.string.LoginPasswordText); + @StringRes int res; + switch (mode) { + case MODE_TRANSFER_OWNERSHIP_CONFIRM: + res = R.string.TransferOwnershipPasswordAlertHint; + break; + case MODE_CUSTOM_CONFIRM: + res = R.string.ConfirmPasswordAlertHint; + break; + default: + res = R.string.LoginPasswordText; + break; + } + hint = Lang.getString(res); break; } case MODE_EMAIL_RECOVERY: @@ -479,7 +517,7 @@ protected View onCreateView (Context context) { } } - if (mode == MODE_TRANSFER_OWNERSHIP_CONFIRM || mode == MODE_UNLOCK_EDIT || mode == MODE_CONFIRM || mode == MODE_LOGIN || mode == MODE_CODE || mode == MODE_CODE_CHANGE || mode == MODE_CODE_PHONE_CONFIRM || mode == MODE_CODE_EMAIL) { + if (mode == MODE_TRANSFER_OWNERSHIP_CONFIRM || mode == MODE_UNLOCK_EDIT || mode == MODE_CUSTOM_CONFIRM || mode == MODE_CONFIRM || mode == MODE_LOGIN || mode == MODE_CODE || mode == MODE_CODE_CHANGE || mode == MODE_CODE_PHONE_CONFIRM || mode == MODE_CODE_EMAIL) { RelativeLayout forgotWrap = new RelativeLayout(context); forgotWrap.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.LEFT | Gravity.BOTTOM)); @@ -917,10 +955,10 @@ private void onDeadEndReached () { if (message instanceof Spannable) { CustomTypefaceSpan[] spans = ((Spannable) message).getSpans(0, message.length(), CustomTypefaceSpan.class); for (CustomTypefaceSpan span : spans) { - if (span.getEntityType() != null && span.getEntityType().getConstructor() == TdApi.TextEntityTypeItalic.CONSTRUCTOR) { + if (span.getTextEntityType() != null && Td.isItalic(span.getTextEntityType())) { span.setTypeface(null); span.setColorId(ColorId.textLink); - span.setEntityType(new TdApi.TextEntityTypeEmailAddress()); + span.setTextEntityType(new TdApi.TextEntityTypeEmailAddress()); int start = ((Spannable) message).getSpanStart(span); int end = ((Spannable) message).getSpanEnd(span); ((Spannable) message).setSpan(new NoUnderlineClickableSpan() { @@ -1036,7 +1074,7 @@ private void updatePasswordResetTextViews () { handler.sendMessageDelayed(Message.obtain(handler, UPDATE_TEXT_VIEWS_TIMER), 1000); } else { - forgotView.setText(Lang.getString(R.string.ForgotPassword)); + forgotView.setText(Lang.getString(canResetPassword() ? R.string.ResetPassword : R.string.ForgotPassword)); resetWaitView.setVisibility(View.GONE); } } @@ -1277,39 +1315,30 @@ private void unlockEdit (final String password) { } setInProgress(true); - tdlib.client().send(new TdApi.GetRecoveryEmailAddress(password), object -> tdlib.ui().post(() -> { - if (!isDestroyed()) { - setInProgress(false); + tdlib.send(new TdApi.GetRecoveryEmailAddress(password), (recoveryEmailAddress, error) -> runOnUiThreadOptional(() -> { + setInProgress(false); - boolean success = false; - String recoveryEmail = null; + boolean success = false; + String recoveryEmail = null; - switch (object.getConstructor()) { - case TdApi.RecoveryEmailAddress.CONSTRUCTOR: { - success = true; - recoveryEmail = ((TdApi.RecoveryEmailAddress) object).recoveryEmailAddress; - break; - } - case TdApi.Error.CONSTRUCTOR: { - setHintText(R.string.InvalidPasswordTryAgain, true); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.GetRecoveryEmailAddress.class, TdApi.RecoveryEmailAddress.class); - break; - } - } + if (error != null) { + setHintText(R.string.InvalidPasswordTryAgain, true); + } else { + success = true; + recoveryEmail = recoveryEmailAddress.recoveryEmailAddress; + } - if (success) { - ViewController prev = navigationController != null ? navigationController.getPreviousStackItem() : null; - if (prev instanceof SettingsPrivacyController) { - Settings2FAController c = new Settings2FAController(context, tdlib); - c.setArguments(new Settings2FAController.Args((SettingsPrivacyController) prev, password, recoveryEmail)); - navigateTo(c); - } else if ((mode == MODE_CONFIRM || mode == MODE_TRANSFER_OWNERSHIP_CONFIRM) && getArguments() != null && getArguments().onSuccessListener != null) { - navigateBack(); - getArgumentsStrict().onSuccessListener.runWithData(password); - } + if (success) { + ViewController prev = navigationController != null ? navigationController.getPreviousStackItem() : null; + if (prev instanceof SettingsPrivacyController) { + Settings2FAController c = new Settings2FAController(context, tdlib); + c.setArguments(new Settings2FAController.Args((SettingsPrivacyController) prev, password, recoveryEmail)); + navigateTo(c); + } else if ((mode == MODE_CONFIRM || mode == MODE_TRANSFER_OWNERSHIP_CONFIRM) && getArguments() != null && getArguments().onSuccessListener != null) { + navigateBack(); + getArgumentsStrict().onSuccessListener.runWithData(password); + } else if (mode == MODE_CUSTOM_CONFIRM) { + confirmDelegate.onPasswordConfirmed(this, password); } } })); @@ -1404,23 +1433,12 @@ private void setNewRecoveryEmail (String email) { setInProgress(true); final String oldPassword = getArguments() != null ? getArguments().oldPassword : null; - tdlib.client().send(new TdApi.SetRecoveryEmailAddress(oldPassword, email), object -> tdlib.ui().post(() -> { - if (!isDestroyed()) { - setInProgress(false); - switch (object.getConstructor()) { - case TdApi.PasswordState.CONSTRUCTOR: { - processNewPasswordState((TdApi.PasswordState) object, oldPassword); - break; - } - case TdApi.Error.CONSTRUCTOR: { - setHintText(TD.toErrorString(object), true); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.SetRecoveryEmailAddress.class, TdApi.PasswordState.class); - break; - } - } + tdlib.send(new TdApi.SetRecoveryEmailAddress(oldPassword, email), (passwordState, error) -> runOnUiThreadOptional(() -> { + setInProgress(false); + if (error != null) { + setHintText(TD.toErrorString(error), true); + } else { + processNewPasswordState(passwordState, oldPassword); } })); } @@ -1612,6 +1630,7 @@ private void proceed () { switch (mode) { case MODE_CONFIRM: case MODE_TRANSFER_OWNERSHIP_CONFIRM: + case MODE_CUSTOM_CONFIRM: case MODE_UNLOCK_EDIT: { if (!input.isEmpty()) { unlockEdit(input); @@ -1689,6 +1708,7 @@ private void proceedForgot () { } case MODE_CONFIRM: case MODE_TRANSFER_OWNERSHIP_CONFIRM: + case MODE_CUSTOM_CONFIRM: case MODE_UNLOCK_EDIT: case MODE_LOGIN: { requestRecovery(); @@ -1732,23 +1752,12 @@ private void setPassword (final String password, final String passwordHint, Stri setInProgress(true); - tdlib.client().send(new TdApi.SetPassword(mode == MODE_NEW || getArguments() == null ? null : getArguments().oldPassword, password, passwordHint, mode != MODE_EDIT, email), object -> tdlib.ui().post(() -> { - if (!isDestroyed()) { - setInProgress(false); - switch (object.getConstructor()) { - case TdApi.PasswordState.CONSTRUCTOR: { - processNewPasswordState((TdApi.PasswordState) object, password); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.SetPassword.class, TdApi.PasswordState.class); - break; - } - } + tdlib.send(new TdApi.SetPassword(mode == MODE_NEW || getArguments() == null ? null : getArguments().oldPassword, password, passwordHint, mode != MODE_EDIT, email), (passwordState, error) -> runOnUiThreadOptional(() -> { + setInProgress(false); + if (error != null) { + UI.showError(error); + } else { + processNewPasswordState(passwordState, password); } })); } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/PhoneController.java b/app/src/main/java/org/thunderdog/challegram/ui/PhoneController.java index 9877c14016..999d712aff 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/PhoneController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/PhoneController.java @@ -49,6 +49,7 @@ import org.thunderdog.challegram.telegram.TdlibAccount; import org.thunderdog.challegram.telegram.TdlibManager; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Intents; import org.thunderdog.challegram.tool.Keyboard; @@ -71,6 +72,7 @@ import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.lambda.RunnableBool; +import me.vkryl.td.Td; public class PhoneController extends EditBaseController implements SettingsAdapter.TextChangeListener, MaterialEditTextGroup.FocusListener, MaterialEditTextGroup.TextChangeListener, View.OnClickListener, Menu { @@ -791,7 +793,7 @@ private void setInCountryMode (final boolean inCountryMode) { private void updateHint (boolean useOffsetLeft, CharSequence text, boolean isError) { int offsetLeft = useOffsetLeft ? Screen.dp(89f) : 0; - int textColorId = isError ? ColorId.textNegative : ColorId.textLight; + @PorterDuffColorId int textColorId = isError ? ColorId.textNegative : ColorId.textLight; if (offsetLeft != hintItem.getTextPaddingLeft() || hintItem.getTextColorId(ColorId.background_textLight) != textColorId || !StringUtils.equalsOrBothEmpty(hintItem.getString(), text)) { hintItem.setTextPaddingLeft(offsetLeft); hintItem.setTextColorId(textColorId); @@ -935,10 +937,10 @@ private boolean makeRequest () { if (message instanceof Spannable) { CustomTypefaceSpan[] spans = ((Spannable) message).getSpans(0, message.length(), CustomTypefaceSpan.class); for (CustomTypefaceSpan span : spans) { - if (span.getEntityType() != null && span.getEntityType().getConstructor() == TdApi.TextEntityTypeItalic.CONSTRUCTOR) { + if (span.getTextEntityType() != null && Td.isItalic(span.getTextEntityType())) { span.setTypeface(null); span.setColorId(ColorId.textLink); - span.setEntityType(new TdApi.TextEntityTypeEmailAddress()); + span.setTextEntityType(new TdApi.TextEntityTypeEmailAddress()); int start = ((Spannable) message).getSpanStart(span); int end = ((Spannable) message).getSpanEnd(span); ((Spannable) message).setSpan(new NoUnderlineClickableSpan() { diff --git a/app/src/main/java/org/thunderdog/challegram/ui/PollResultsController.java b/app/src/main/java/org/thunderdog/challegram/ui/PollResultsController.java index 12b12b4fea..e3446d3da8 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/PollResultsController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/PollResultsController.java @@ -31,6 +31,7 @@ import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.ListManager; import org.thunderdog.challegram.telegram.PollListener; +import org.thunderdog.challegram.telegram.SenderListManager; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.telegram.UserListManager; @@ -48,7 +49,7 @@ import java.util.List; import me.vkryl.android.AnimatorUtils; -import me.vkryl.td.ChatId; +import me.vkryl.td.Td; public class PollResultsController extends RecyclerViewController implements PollListener, UserListManager.ChangeListener, View.OnClickListener { public static class Args { @@ -107,9 +108,9 @@ private void updateHeader (boolean isUpdate) { private static final boolean NEED_CENTER_DECORATION = false; - private static class ListCache implements UserListManager.ChangeListener { + private static class ListCache implements SenderListManager.ChangeListener { private final Tdlib tdlib; - private final UserListManager voters; + private final SenderListManager voters; private final SettingsAdapter adapter; public ListCache (ViewController context, long chatId, long messageId, int optionId) { @@ -122,20 +123,27 @@ protected void setChatData (ListItem item, VerticalChatView chatView) { } }; this.adapter.setNoEmptyProgress(); - this.voters = new UserListManager(tdlib, 50, 50, this) { + this.voters = new SenderListManager(tdlib, 50, 50, this) { @Override - protected TdApi.Function nextLoadFunction (boolean reverse, int itemCount, int loadCount) { + protected TdApi.Function nextLoadFunction (boolean reverse, int itemCount, int loadCount) { return new TdApi.GetPollVoters(chatId, messageId, optionId, itemCount, loadCount); } }; this.voters.loadInitialChunk(null); } + private ListItem newItem (TdApi.MessageSender sender) { + TGFoundChat foundChat = new TGFoundChat(tdlib, sender, false) + .setNoUnread() + .setNoAnonymousBadge(); + return new ListItem(ListItem.TYPE_CHAT_VERTICAL, R.id.sender).setData(foundChat).setLongId(Td.getSenderId(sender)); + } + @Override - public void onItemsAdded (ListManager list, List items, int startIndex, boolean isInitialChunk) { + public void onItemsAdded (ListManager list, List items, int startIndex, boolean isInitialChunk) { List itemsToAdd = new ArrayList<>(items.size()); - for (long userId : items) { - itemsToAdd.add(new ListItem(ListItem.TYPE_CHAT_VERTICAL, R.id.user).setData(new TGFoundChat(tdlib, userId).setNoUnread()).setLongId(ChatId.fromUserId(userId))); + for (TdApi.MessageSender sender : items) { + itemsToAdd.add(newItem(sender)); } adapter.getItems().addAll(startIndex, itemsToAdd); adapter.notifyItemRangeInserted(startIndex, itemsToAdd.size()); @@ -145,23 +153,20 @@ public void onItemsAdded (ListManager list, List items, int startInd } @Override - public void onItemAdded (ListManager list, Long userId, int toIndex) { - adapter.addItem(toIndex, new ListItem(ListItem.TYPE_CHAT_VERTICAL).setData(new TGFoundChat(tdlib, userId).setNoUnread()).setLongId(ChatId.fromUserId(userId))); + public void onItemAdded (ListManager list, TdApi.MessageSender item, int toIndex) { + adapter.addItem(toIndex, newItem(item)); if (NEED_CENTER_DECORATION) { adapter.invalidateItemDecorations(); } } - - @Override - public void onItemChanged (ListManager list, Long item, int index, int cause) { - // Do nothing - } } @Override public void onClick (View v) { - if (v.getId() == R.id.user) { - tdlib.ui().openPrivateProfile(this, ((VerticalChatView) v).getUserId(), new TdlibUi.UrlOpenParameters().tooltip(context().tooltipManager().builder(v))); + if (v.getId() == R.id.sender) { + ListItem item = (ListItem) v.getTag(); + TdApi.MessageSender sender = tdlib.sender(item.getLongId()); + tdlib.ui().openSenderProfile(this, sender, new TdlibUi.UrlOpenParameters().tooltip(context().tooltipManager().builder(v))); } } @@ -255,7 +260,7 @@ public int getItemCount() { recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - UserListManager listManager = ((ListCache) ((ListItem) recyclerView.getTag()).getData()).voters; + SenderListManager listManager = ((ListCache) ((ListItem) recyclerView.getTag()).getData()).voters; int lastVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition(); if (lastVisiblePosition + 5 >= listManager.getCount()) { listManager.loadItems(false, null); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/PrivacyExceptionController.java b/app/src/main/java/org/thunderdog/challegram/ui/PrivacyExceptionController.java index fe055def59..11b496c91b 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/PrivacyExceptionController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/PrivacyExceptionController.java @@ -237,6 +237,9 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda case TdApi.UserPrivacySettingShowPhoneNumber.CONSTRUCTOR: view.setData(R.string.PrivacyShowNumberExceptionContacts); break; + case TdApi.UserPrivacySettingShowBio.CONSTRUCTOR: + view.setData(R.string.PrivacyShowBioExceptionContacts); + break; case TdApi.UserPrivacySettingShowProfilePhoto.CONSTRUCTOR: view.setData(R.string.PrivacyPhotoExceptionContacts); break; @@ -260,8 +263,10 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda break; case TdApi.UserPrivacySettingAllowFindingByPhoneNumber.CONSTRUCTOR: throw new IllegalStateException(); - default: - throw new UnsupportedOperationException(); + default: { + Td.assertUserPrivacySetting_21d3f4(); + throw Td.unsupported(setting); + } } } else { view.setData(matchingRule != null && (matchingRule.getConstructor() == TdApi.UserPrivacySettingRuleAllowContacts.CONSTRUCTOR || matchingRule.getConstructor() == TdApi.UserPrivacySettingRuleRestrictContacts.CONSTRUCTOR) ? R.string.PrivacyDefaultContacts : R.string.PrivacyDefault); @@ -272,6 +277,9 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda case TdApi.UserPrivacySettingShowPhoneNumber.CONSTRUCTOR: view.setData(isActive ? R.string.PrivacyShowNumberExceptionOn : R.string.PrivacyShowNumberExceptionOff); break; + case TdApi.UserPrivacySettingShowBio.CONSTRUCTOR: + view.setData(isActive ? R.string.PrivacyShowBioExceptionOn : R.string.PrivacyShowBioExceptionOff); + break; case TdApi.UserPrivacySettingShowProfilePhoto.CONSTRUCTOR: view.setData(isActive ? R.string.PrivacyPhotoExceptionOn : R.string.PrivacyPhotoExceptionOff); break; @@ -295,8 +303,10 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda break; case TdApi.UserPrivacySettingAllowFindingByPhoneNumber.CONSTRUCTOR: throw new IllegalStateException(); - default: - throw new UnsupportedOperationException(); + default: { + Td.assertUserPrivacySetting_21d3f4(); + throw Td.unsupported(setting); + } } } } else if (itemId == R.id.btn_newContact) { @@ -304,7 +314,7 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda view.setTextColorId(item.getTextColorId(ColorId.text)); view.setIcon(item.getIconResource()); if (!isMultiChat) { - view.setIconColorId(tdlib.cache().userContact(userId) ? ColorId.iconNegative : 0); + view.setIconColorId(tdlib.cache().userContact(userId) ? ColorId.iconNegative : ColorId.NONE); } } } @@ -333,12 +343,14 @@ protected void setChatData (ListItem item, int position, BetterChatView chatView new TdApi.UserPrivacySetting[] { new TdApi.UserPrivacySettingShowStatus(), new TdApi.UserPrivacySettingShowProfilePhoto(), + new TdApi.UserPrivacySettingShowBio(), new TdApi.UserPrivacySettingShowLinkInForwardedMessages(), } : new TdApi.UserPrivacySetting[] { new TdApi.UserPrivacySettingShowStatus(), new TdApi.UserPrivacySettingShowProfilePhoto(), + new TdApi.UserPrivacySettingShowBio(), new TdApi.UserPrivacySettingShowPhoneNumber(), new TdApi.UserPrivacySettingShowLinkInForwardedMessages(), diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ProfileController.java b/app/src/main/java/org/thunderdog/challegram/ui/ProfileController.java index 98b039d675..05b0dfcf90 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/ProfileController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/ProfileController.java @@ -118,7 +118,6 @@ import org.thunderdog.challegram.util.text.TextColorSets; import org.thunderdog.challegram.util.text.TextWrapper; import org.thunderdog.challegram.v.CustomRecyclerView; -import org.thunderdog.challegram.v.HeaderEditText; import org.thunderdog.challegram.widget.AvatarView; import org.thunderdog.challegram.widget.BetterChatView; import org.thunderdog.challegram.widget.CircleButton; @@ -366,8 +365,11 @@ private void replaceWithSupergroup (long supergroupId) { // Controller + private final TdlibUi.AvatarPickerManager avatarPickerManager; + public ProfileController (Context context, Tdlib tdlib) { super(context, tdlib); + avatarPickerManager = new TdlibUi.AvatarPickerManager(this); } @Override @@ -484,7 +486,7 @@ public void onMenuItemPressed (int id, View view) { @Override public final boolean shouldDisallowScreenshots () { - return ChatId.isSecret(chat.id) || (chat.hasProtectedContent && !isEditing()); + return ChatId.isSecret(chat.id) || (chat.hasProtectedContent && !isEditing()) || super.shouldDisallowScreenshots(); } @Override @@ -532,11 +534,17 @@ private void showMore () { case MODE_EDIT_CHANNEL: case MODE_EDIT_SUPERGROUP: { int count = 0; + if (canClearHistory()) + count++; if (canDestroyChat()) count++; if (count > 0) { IntList ids = new IntList(count); StringList strings = new StringList(count); + if (canClearHistory()) { + ids.append(R.id.btn_clearChatHistory); + strings.append(R.string.ClearHistory); + } if (canDestroyChat()) { ids.append(R.id.btn_destroyChat); strings.append(isChannel() ? R.string.DestroyChannel : R.string.DestroyGroup); @@ -586,9 +594,10 @@ private void showPrivateMore () { } //if (!isBot || !((TdApi.UserTypeBot) user.type).isInline) { - if (!user.isSupport || tdlib.chatBlocked(chatId)) { + boolean isFullyBlocked = tdlib.chatFullyBlocked(chatId); + if (!user.isSupport || isFullyBlocked) { ids.append(R.id.more_btn_block); - strings.append(tdlib.chatBlocked(chatId) ? isBot ? R.string.UnblockBot : R.string.Unblock : isBot ? R.string.BlockBot : R.string.BlockContact); + strings.append(isFullyBlocked ? isBot ? R.string.UnblockBot : R.string.Unblock : isBot ? R.string.BlockBot : R.string.BlockContact); } //} @@ -597,7 +606,7 @@ private void showPrivateMore () { strings.append(R.string.PasscodeTitle); } - if (!tdlib.chatBlocked(chatId)) { + if (!tdlib.chatFullyBlocked(chatId)) { ids.append(R.id.more_btn_privacy); strings.append(R.string.EditPrivacy); } @@ -645,7 +654,7 @@ private void showCommonMore () { ids.append(R.id.more_btn_viewAdmins); strings.append(R.string.ViewAdmins); } - if (!tdlib.chatBlocked(getChatId())) { + if (!tdlib.chatFullyBlocked(getChatId())) { ids.append(R.id.more_btn_privacy); strings.append(R.string.EditPrivacy); } @@ -695,12 +704,12 @@ public void onMoreItemPressed (int id) { } else if (id == R.id.more_btn_privacy) { openPrivacyExceptions(); } else if (id == R.id.more_btn_block) { - final boolean needBlock = !tdlib.chatBlocked(chat.id); + final boolean needBlock = !tdlib.chatFullyBlocked(chat.id); final boolean isBot = tdlib.isBotChat(chat.id); if (needBlock) { showOptions(Lang.getStringBold(isBot ? R.string.BlockBotConfirm : R.string.BlockUserConfirm, tdlib.chatTitle(chat.id)), new int[] {R.id.btn_blockSender, R.id.btn_cancel}, new String[] {Lang.getString(isBot ? R.string.BlockBot : R.string.BlockContact), Lang.getString(R.string.Cancel)}, new int[] {OPTION_COLOR_RED, OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_block_24, R.drawable.baseline_cancel_24}, (itemView, id1) -> { if (!isDestroyed() && id1 == R.id.btn_blockSender) { - tdlib.blockSender(tdlib.sender(chat.id), true, result -> { + tdlib.blockSender(tdlib.sender(chat.id), new TdApi.BlockListMain(), result -> { if (TD.isOk(result)) { UI.showToast(Lang.getStringBold(isBot ? R.string.BlockedBot : R.string.BlockedUser, tdlib.chatTitle(chat.id)), Toast.LENGTH_SHORT); } else { @@ -711,7 +720,7 @@ public void onMoreItemPressed (int id) { return true; }); } else { - tdlib.blockSender(tdlib.sender(chat.id), false, result -> { + tdlib.unblockSender(tdlib.sender(chat.id), result -> { if (TD.isOk(result)) { UI.showToast(Lang.getStringBold(isBot ? R.string.UnblockedBot : R.string.UnblockedUser, tdlib.chatTitle(chat.id)), Toast.LENGTH_SHORT); } else { @@ -759,6 +768,42 @@ private String getProfileUsername () { return tdlib.chatUsername(chat.id); } + private long getPeerId () { + if (secretChat != null) { + return secretChat.id; + } else if (group != null) { + return group.id; + } else if (supergroup != null) { + return supergroup.id; + } else if (user != null) { + return user.id; + } else { + return 0; + } + } + + private int getPeerTypeStringResourceId () { + if (secretChat != null) { + return R.string.SecretChatId; + } else if (group != null) { + return R.string.BasicGroupId; + } else if (supergroup != null) { + if (isChannel()) { + return R.string.ChannelId; + } else { + return R.string.SuperGroupId; + } + } else if (user != null) { + if (user.type.getConstructor() == TdApi.UserTypeBot.CONSTRUCTOR) { + return R.string.BotId; + } else { + return R.string.UserId; + } + } else { + return R.string.PeerId; + } + } + @Override public boolean onOptionItemPressed (View optionItemView, int id) { if (id == R.id.btn_copyText) { @@ -774,10 +819,12 @@ public boolean onOptionItemPressed (View optionItemView, int id) { if (!StringUtils.isEmpty(username)) { UI.copyText("@" + username, R.string.CopiedUsername); } + } else if (id == R.id.btn_peer_id_copy) { + UI.copyText(Long.toString(getPeerId()), R.string.CopiedPeerId); } else if (id == R.id.btn_copyLink) { String username = getProfileUsername(); if (!StringUtils.isEmpty(username)) { - UI.copyText(TD.getLink(username), R.string.CopiedLink); + UI.copyText(tdlib.tMeUrl(username), R.string.CopiedLink); } } else if (id == R.id.btn_share) { if (!share(false)) { @@ -1266,7 +1313,7 @@ private void setTopTranslationY (float y) { lickView.setTranslationY(y - headerHeight + getTopViewTopPadding()); lickShadow.setTranslationY(y - (headerHeight * expandFactor)); if (getSearchTransformFactor() != 0f) { - HeaderEditText editText = getSearchHeaderView(headerView); + View editText = getSearchHeaderView(headerView).view(); editText.setTranslationY(Math.max(0f, lickShadow.getTranslationY() - HeaderView.getSize(false))); } } @@ -1678,6 +1725,9 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda view.setData(Lang.plural(R.string.xGroups, userFull.groupInCommonCount)); } else if (itemId == R.id.btn_encryptionKey) { view.setData(R.string.PictureAndText); + } else if (itemId == R.id.btn_peer_id) { + view.setName(getPeerTypeStringResourceId()); + view.setData(Strings.buildCounter(getPeerId())); } else if (itemId == R.id.btn_description) { view.setText(aboutWrapper); if (canEditDescription() && !hasDescription()) { @@ -2222,7 +2272,7 @@ private static int getTextWidth (int width) { } private boolean setDescription (String text) { - TdApi.TextEntity[] entities = Td.findEntities(text, (e) -> e.type.getConstructor() != TdApi.TextEntityTypeBotCommand.CONSTRUCTOR); + TdApi.TextEntity[] entities = Td.findEntities(text, (e) -> !Td.isBotCommand(e.type)); return setDescription(new TdApi.FormattedText(text, entities)); } @@ -2285,6 +2335,10 @@ private ListItem newDescriptionItem () { return new ListItem(ListItem.TYPE_INFO_MULTILINE, R.id.btn_description, R.drawable.baseline_info_24, isUserMode() && !TD.isBot(user) ? R.string.UserBio : R.string.Description); } + private ListItem newPeerIdItem () { + return new ListItem(ListItem.TYPE_INFO_SETTING, R.id.btn_peer_id, R.drawable.baseline_identifier_24, R.string.PeerId); + } + private ListItem newEncryptionKeyItem () { return new ListItem(ListItem.TYPE_VALUED_SETTING, R.id.btn_encryptionKey, R.drawable.baseline_vpn_key_24, R.string.EncryptionKey); } @@ -2299,9 +2353,17 @@ private void buildUserCells () { items.add(new ListItem(ListItem.TYPE_EMPTY_OFFSET)); int addedCount = 0; + if (Settings.instance().showPeerIds()) { + items.add(newPeerIdItem()); + addedCount++; + } + if (Td.hasUsername(user)) { final ListItem usernameItem = newUsernameItem(); if (usernameItem != null) { + if (addedCount > 0) { + items.add(new ListItem(ListItem.TYPE_SEPARATOR)); + } items.add(usernameItem); addedCount++; } @@ -2411,6 +2473,7 @@ private void checkGroupsInCommon () { if (needGroups != hasGroups) { if (needGroups) { SharedChatsController c = new SharedChatsController(context, tdlib); + registerController(c); controllers.add(c); pagerAdapter.notifyItemInserted(controllers.size() - 1); if (Config.USE_ICON_TABS) { @@ -2422,9 +2485,11 @@ private void checkGroupsInCommon () { int i = controllers.indexOf(existingGroupsController); if (i == -1) return; - controllers.remove(i); + ViewController c = controllers.remove(i); pagerAdapter.notifyItemRemoved(i); + unregisterController(c); topCellView.getTopView().removeItemAt(i); + c.destroy(); } pagerAdapter.notifyDataSetChanged(); } @@ -2439,13 +2504,16 @@ private void checkPhone () { removeTopItem(index); } else { index = 0; + if (Settings.instance().showPeerIds() && baseAdapter.indexOfViewById(R.id.btn_peer_id) != -1) { + index++; + } if (baseAdapter.indexOfViewById(R.id.btn_username) != -1) { index++; } if (baseAdapter.indexOfViewById(R.id.btn_description) != -1) { index++; } - addTopItem(newPhoneItem(), index); // after username, if exists + addTopItem(newPhoneItem(), index); // after peer_id, username, description } } else if (hasPhone) { updateValuedItem(R.id.btn_phone); @@ -2527,6 +2595,7 @@ private void checkChannelMembers () { SharedMembersController c = new SharedMembersController(context, tdlib) .setSpecificFilter(new TdApi.SupergroupMembersFilterRecent()); controllers.add(c); + registerController(c); pagerAdapter.notifyItemInserted(controllers.size() - 1); if (Config.USE_ICON_TABS) { // topCellView.getTopView().addItem(c.getIcon()); @@ -2538,9 +2607,11 @@ private void checkChannelMembers () { if (i == -1) { return; } - controllers.remove(i); + ViewController c = controllers.remove(i); + unregisterController(c); topCellView.getTopView().removeLastItem(); pagerAdapter.notifyItemRemoved(i); + c.destroy(); } pagerAdapter.notifyDataSetChanged(); } @@ -2602,16 +2673,24 @@ private boolean setDescription () { private void checkDescription () { if (isEditing()) return; - int index = baseAdapter.indexOfViewById(R.id.btn_description); - boolean hadDescription = index != -1; + int foundIndex = baseAdapter.indexOfViewById(R.id.btn_description); + boolean hadDescription = foundIndex != -1; boolean hasDescription = hasDescription() || canEditDescription(); if (hadDescription != hasDescription) { if (hadDescription) { - removeTopItem(index); + removeTopItem(foundIndex); } else { ListItem descriptionItem = newDescriptionItem(); setDescription(); - addTopItem(descriptionItem, baseAdapter.indexOfViewById(R.id.btn_username) != -1 ? 1 : 0); + + int index = 0; + if (Settings.instance().showPeerIds() && baseAdapter.indexOfViewById(R.id.btn_peer_id) != -1) { + index++; + } + if (baseAdapter.indexOfViewById(R.id.btn_username) != -1) { + index++; + } + addTopItem(descriptionItem, index); // after peer_id, username } } else if (hasDescription) { if (setDescription()) { @@ -2631,14 +2710,21 @@ private void checkEasterEggs () { return; long chatId = getChatId(); if (chatId == Tdlib.TRENDING_STICKERS_CHAT_ID && Config.EXPLICIT_DICE_AVAILABLE) { - int index = baseAdapter.indexOfViewById(R.id.btn_useExplicitDice); + int foundIndex = baseAdapter.indexOfViewById(R.id.btn_useExplicitDice); boolean hasEasterEgg = isMember() && testerLevel >= Tdlib.TESTER_LEVEL_READER; - boolean hadEasterEgg = index != -1; + boolean hadEasterEgg = foundIndex != -1; if (hadEasterEgg != hasEasterEgg) { if (hadEasterEgg) { - removeTopItem(index); + removeTopItem(foundIndex); } else { - addTopItem(newExplicitDiceItem(), baseAdapter.indexOfViewById(R.id.btn_username) != -1 ? 1 : 0); + int index = 0; + if (Settings.instance().showPeerIds() && baseAdapter.indexOfViewById(R.id.btn_peer_id) != -1) { + index++; + } + if (baseAdapter.indexOfViewById(R.id.btn_username) != -1) { + index++; + } + addTopItem(newExplicitDiceItem(), index); // after peer_id, username } } if (isMember() && testerLevel == -1) { @@ -2670,12 +2756,12 @@ private void checkUsername () { return; } } - int index = baseAdapter.indexOfViewById(R.id.btn_username); - boolean hadUsername = index != -1; + int foundIndex = baseAdapter.indexOfViewById(R.id.btn_username); + boolean hadUsername = foundIndex != -1; boolean hasUsername = Td.hasUsername(usernames); if (hadUsername != hasUsername) { if (hadUsername) { - removeTopItem(index); + removeTopItem(foundIndex); switch (mode) { case MODE_SUPERGROUP: { if (tdlib.canCreateInviteLink(chat)) { @@ -2694,7 +2780,11 @@ private void checkUsername () { } else { ListItem usernameItem = newUsernameItem(); if (usernameItem != null) { - addTopItem(usernameItem, 0); + int index = 0; + if (Settings.instance().showPeerIds() && baseAdapter.indexOfViewById(R.id.btn_peer_id) != -1) { + index++; + } + addTopItem(usernameItem, index); switch (mode) { case MODE_SUPERGROUP: { @@ -2786,7 +2876,7 @@ private void checkEncryptionKey () { if (hadKey != hasKey) { if (hasKey) { - addTopItem(newEncryptionKeyItem(), 3); + addTopItem(newEncryptionKeyItem(), baseHeaderItemCount); } else { removeTopItem(index); } @@ -2837,9 +2927,17 @@ private void buildGroupCells () { // MODE_GROUP, MODE_SUPERGROUP int addedCount = 0; + if (Settings.instance().showPeerIds()) { + items.add(newPeerIdItem()); + addedCount++; + } + if (isPublic) { ListItem usernameItem = newUsernameItem(); if (usernameItem != null) { + if (addedCount > 0) { + items.add(new ListItem(ListItem.TYPE_SEPARATOR)); + } items.add(usernameItem); addedCount++; } @@ -2914,9 +3012,17 @@ private void buildChannelCells () { int addedCount = 0; + if (Settings.instance().showPeerIds()) { + items.add(newPeerIdItem()); + addedCount++; + } + if (isPublic) { ListItem usernameItem = newUsernameItem(); if (usernameItem != null) { + if (addedCount > 0) { + items.add(new ListItem(ListItem.TYPE_SEPARATOR)); + } items.add(usernameItem); addedCount++; } @@ -3642,7 +3748,7 @@ private void buildEditCells () { TdApi.ChatMemberStatus myStatus = supergroup != null ? supergroup.status : group.status; int itemCount = 0; - if ((supergroupFull != null && supergroupFull.canSetUsername) || (group != null && TD.isCreator(group.status))) { // TODO TDLib: canSetUsername for basicGroupFull + if (canSetUsername()) { items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); items.add(new ListItem(ListItem.TYPE_VALUED_SETTING, R.id.btn_channelType, 0, mode == MODE_EDIT_CHANNEL ? R.string.ChannelLink : R.string.GroupLink)); itemCount++; @@ -3737,7 +3843,7 @@ private void buildEditCells () { items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.HideMembersDesc)); } - if (tdlib.canRestrictMembers(chat.id) && (tdlib.isSupergroup(chat.id) || (ChatId.isBasicGroup(chat.id) && tdlib.canUpgradeChat(chat.id)))) { + if (tdlib.canEditSlowMode(chat.id)) { int slowModeValue = supergroupFull != null ? supergroupFull.slowModeDelay : 0; items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.SlowMode)); items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); @@ -4312,84 +4418,11 @@ private void joinChannel () { @Override public void onActivityResult (int requestCode, int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - return; - } - switch (requestCode) { - case Intents.ACTIVITY_RESULT_IMAGE_CAPTURE: { - File image = Intents.takeLastOutputMedia(); - if (image != null) { - // TODO show editor - U.addToGallery(image); - UI.showToast(R.string.UploadingPhotoWait, Toast.LENGTH_SHORT); - tdlib.client().send(new TdApi.SetChatPhoto(chat.id, new TdApi.InputChatPhotoStatic(new TdApi.InputFileGenerated(image.getPath(), SimpleGenerationInfo.makeConversion(image.getPath()), 0))), tdlib.okHandler()); - } - - break; - } - case Intents.ACTIVITY_RESULT_GALLERY: { - Uri image = data.getData(); - if (image == null) break; - final Uri path = data.getData(); - String imagePath = U.tryResolveFilePath(path); - - if (imagePath == null) break; - - if (imagePath.endsWith(".webp")) { - UI.showToast("Webp is not supported for profile photos", Toast.LENGTH_LONG); - break; - } - - UI.showToast(R.string.UploadingPhotoWait, Toast.LENGTH_SHORT); - tdlib.client().send(new TdApi.SetChatPhoto(chat.id, new TdApi.InputChatPhotoStatic(new TdApi.InputFileGenerated(imagePath, SimpleGenerationInfo.makeConversion(imagePath), 0))), tdlib.okHandler()); - - break; - } - } + avatarPickerManager.handleActivityResult(requestCode, resultCode, data, TdlibUi.AvatarPickerManager.MODE_CHAT, chat, null); } private void changeProfilePhoto () { - IntList ids = new IntList(4); - StringList strings = new StringList(4); - IntList colors = new IntList(4); - IntList icons = new IntList(4); - - if (chat != null && chat.photo != null && !isEditing()) { - ids.append(R.id.btn_open); - strings.append(R.string.Open); - icons.append(R.drawable.baseline_visibility_24); - colors.append(OPTION_COLOR_NORMAL); - } - - ids.append(R.id.btn_changePhotoCamera); - strings.append(R.string.ChatCamera); - icons.append(R.drawable.baseline_camera_alt_24); - colors.append(OPTION_COLOR_NORMAL); - - ids.append(R.id.btn_changePhotoGallery); - strings.append(R.string.Gallery); - icons.append(R.drawable.baseline_image_24); - colors.append(OPTION_COLOR_NORMAL); - - if (chat != null && chat.photo != null) { - ids.append(R.id.btn_changePhotoDelete); - strings.append(R.string.Delete); - icons.append(R.drawable.baseline_delete_24); - colors.append(OPTION_COLOR_RED); - } - - showOptions(null, ids.get(), strings.get(), colors.get(), icons.get(), (itemView, id) -> { - if (id == R.id.btn_open) { - openPhoto(); - } else if (id == R.id.btn_changePhotoCamera) { - UI.openCameraDelayed(context); - } else if (id == R.id.btn_changePhotoGallery) { - UI.openGalleryDelayed(context, false); - } else if (id == R.id.btn_changePhotoDelete) { - tdlib.client().send(new TdApi.SetChatPhoto(chat.id, null), tdlib.okHandler()); - } - return true; - }); + avatarPickerManager.showMenuForChat(chat, headerCell, !isEditing()); } @Override @@ -4408,13 +4441,17 @@ private void openPhoto () { } private boolean hasMoreItems () { - return canDestroyChat() && !tdlib.isUserChat(chat); + return (canDestroyChat() || canClearHistory()) && !tdlib.isUserChat(chat); } private boolean canDestroyChat () { return chat != null && chat.canBeDeletedForAllUsers; } + private boolean canClearHistory () { + return isChannel() && tdlib.canClearHistory(getChatId()); + } + private void destroyChat () { if (!canDestroyChat()) { return; @@ -4487,8 +4524,10 @@ public void onClick (View v) { tdlib.ui().handleProfileClick(this, v, v.getId(), user, false); } else if (viewId == R.id.btn_useExplicitDice) { Settings.instance().setNewSetting(((ListItem) v.getTag()).getLongId(), baseAdapter.toggleView(v)); + } else if (viewId == R.id.btn_peer_id) { + showOptions(Long.toString(getPeerId()), new int[]{R.id.btn_peer_id_copy}, new String[]{Lang.getString(R.string.Copy)}, null, new int[]{R.drawable.baseline_content_copy_24}); } else if (viewId == R.id.btn_username) { - boolean canSetUsername = supergroupFull != null && supergroupFull.canSetUsername; + boolean canSetUsername = canSetUsername(); boolean canInviteUsers = chat != null && tdlib.canManageInviteLinks(chat); int size = 3; @@ -4592,6 +4631,10 @@ public void onClick (View v) { } } + private boolean canSetUsername () { + return (supergroup != null && TD.isCreator(supergroup.status)) || (group != null && TD.isCreator(group.status)); + } + private TranslationControllerV2.Wrapper translationPopup; private void showDescriptionOptions (boolean showTranslate, String descriptionLang) { @@ -5467,29 +5510,35 @@ private ArrayList> getControllers () { break; } } + for (ViewController controller : controllers) { + registerController(controller); + } } return controllers; } + private void registerController (ViewController controller) { + controller.setParentWrapper(this); + controller.bindThemeListeners(this); + } + + private void unregisterController (ViewController controller) { + // TODO + } + @Override public boolean needAsynchronousAnimation () { return buildingTabsCount > 0; } private void getMessageCount (TdApi.SearchMessagesFilter filter, boolean returnLocal) { - tdlib.client().send(new TdApi.GetChatMessageCount(getChatId(), filter, returnLocal), result -> { + tdlib.send(new TdApi.GetChatMessageCount(getChatId(), filter, returnLocal), (messageCount, error) -> { int count; - switch (result.getConstructor()) { - case TdApi.Count.CONSTRUCTOR: - count = ((TdApi.Count) result).count; - break; - case TdApi.Error.CONSTRUCTOR: - Log.e("TDLib error getMessageCount chatId:%d, filter:%s, returnLocal:%b", getChatId(), filter, returnLocal); - count = -1; - break; - default: - Log.unexpectedTdlibResponse(result, TdApi.GetChatMessageCount.class, TdApi.Count.class, TdApi.Error.class); - return; + if (error != null) { + Log.e("TDLib error getMessageCount chatId:%d, filter:%s, returnLocal:%b: %s", getChatId(), filter, returnLocal, TD.toErrorString(error)); + count = -1; + } else { + count = messageCount.count; } if (returnLocal) { tdlib.ui().post(() -> { @@ -5576,6 +5625,7 @@ private void addMediaTab (TdApi.SearchMessagesFilter filter) { if (append) { SharedBaseController c = SharedBaseController.valueOf(context, tdlib, filter); controllers.add(c); + registerController(c); pagerAdapter.notifyItemInserted(controllers.size() - 1); topCellView.getTopView().addItem(c.getName().toString().toUpperCase()); } else { @@ -5585,6 +5635,7 @@ private void addMediaTab (TdApi.SearchMessagesFilter filter) { } c = SharedBaseController.valueOf(context, tdlib, filter); controllers.add(visualIndex, c); + registerController(c); pagerAdapter.notifyItemInserted(visualIndex); topCellView.getTopView().addItemAtIndex(c.getName().toString().toUpperCase(), visualIndex); } @@ -5614,6 +5665,7 @@ private boolean hasTab (TdApi.SearchMessagesFilter filter, int indexGuess) { private static TdApi.SearchMessagesFilter[] filtersOrder, filtersOrder2; private static boolean isSyncTab (TdApi.SearchMessagesFilter filter) { + //noinspection SwitchIntDef switch (filter.getConstructor()) { case TdApi.SearchMessagesFilterPhotoAndVideo.CONSTRUCTOR: case TdApi.SearchMessagesFilterPhoto.CONSTRUCTOR: @@ -6251,7 +6303,7 @@ public void onMessageSendSucceeded (final TdApi.Message message, long oldMessage } @Override - public void onMessageSendFailed (final TdApi.Message message, final long oldMessageId, int errorCode, String errorMessage) { + public void onMessageSendFailed (final TdApi.Message message, final long oldMessageId, TdApi.Error error) { runOnUiThreadOptional(() -> { if (chat.id == message.chatId) { removeMessages(new long[]{oldMessageId}); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ReactionsPickerController.java b/app/src/main/java/org/thunderdog/challegram/ui/ReactionsPickerController.java new file mode 100644 index 0000000000..493369c4b2 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/ui/ReactionsPickerController.java @@ -0,0 +1,1008 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 31/05/2023 + */ +package org.thunderdog.challegram.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.Log; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.core.graphics.ColorUtils; +import androidx.recyclerview.widget.RecyclerView; + +import org.drinkless.tdlib.Client; +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.U; +import org.thunderdog.challegram.component.attach.CustomItemAnimator; +import org.thunderdog.challegram.component.emoji.MediaStickersAdapter; +import org.thunderdog.challegram.component.sticker.StickerSmallView; +import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGReaction; +import org.thunderdog.challegram.data.TGStickerSetInfo; +import org.thunderdog.challegram.loader.gif.GifFile; +import org.thunderdog.challegram.navigation.BackHeaderButton; +import org.thunderdog.challegram.navigation.HeaderView; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.StickersListener; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibUi; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.ColorState; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.util.StickerSetsDataProvider; +import org.thunderdog.challegram.v.CustomRecyclerView; +import org.thunderdog.challegram.widget.EmojiLayout; +import org.thunderdog.challegram.widget.ShadowView; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutRecyclerController; +import org.thunderdog.challegram.widget.emoji.header.EmojiCategoriesRecyclerView; +import org.thunderdog.challegram.widget.emoji.header.EmojiHeaderView; +import org.thunderdog.challegram.widget.emoji.section.EmojiSection; +import org.thunderdog.challegram.widget.emoji.section.EmojiSectionView; +import org.thunderdog.challegram.widget.emoji.section.StickerSectionView; + +import java.util.ArrayList; +import java.util.Arrays; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.BoolAnimator; +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.BitwiseUtils; +import me.vkryl.core.StringUtils; + +public class ReactionsPickerController extends ViewController + implements StickersListener, EmojiLayoutRecyclerController.Callback, + StickerSmallView.StickerMovementCallback, FactorAnimator.Target { + + private MessageOptionsPagerController.State state; + private EmojiLayoutRecyclerController reactionsController; + private CustomRecyclerView recyclerView; + private MediaStickersAdapter adapter; + + public ReactionsPickerController (Context context, Tdlib tdlib) { + super(context, tdlib); + } + + @Override + protected View onCreateView (Context context) { + ArrayList emojiSections = new ArrayList<>(3); + if (state.needShowCustomEmojiInsidePicker) { + emojiSections.add(new EmojiSection(this, -14, R.drawable.baseline_search_24, R.drawable.baseline_search_24)/*.setFactor(1f, false)*/.setMakeFirstTransparent().setOffsetHalf(false)); + } + emojiSections.add(new EmojiSection(this, 0, R.drawable.baseline_favorite_24, R.drawable.baseline_favorite_24)/*.setFactor(1f, false)*/.setMakeFirstTransparent()); + if (state.hasNonSelectedCustomReactions && state.isPremium) { + emojiSections.add(new EmojiSection(this, 1, R.drawable.baseline_access_time_24, R.drawable.baseline_watch_later_24)/*.setFactor(1f, false)*/.setMakeFirstTransparent()); + } + bottomHeaderCell = new EmojiHeaderView(context, this, this, emojiSections, null, false); + bottomHeaderCell.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, HeaderView.getSize(false))); + bottomHeaderCell.setIsPremium(true, false); + bottomHeaderCell.setSectionsOnLongClickListener(this::onEmojiHeaderLongClick); + bottomHeaderCell.setSectionsOnClickListener(this::onStickerSectionClick); + + recyclerView = onCreateRecyclerView(); + recyclerView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> reactionsController.invalidateStickerObjModifiers()); + recyclerView.setItemAnimator(null); + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) {} + + @Override + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (!reactionsController.isNeedIgnoreScroll() && !isIgnoreMovement) { + setCurrentStickerSectionByPosition(EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID, reactionsController.getStickerSetSection(HeaderView.getSize(true) + EmojiLayout.getHeaderPadding()), true, true); + } + } + }); + + genTopHeader(); + genBottomHeader(); + buildCells(); + loadStickers(); + return recyclerView; + } + + public static final float RECYCLER_VIEW_LEFT_RIGHT_PADDING = 9.5f; + private static final float DEFAULT_STICKER_PADDING_DP = 5.5f; + + public CustomRecyclerView onCreateRecyclerView () { + reactionsController = new EmojiLayoutRecyclerController(context, tdlib, R.id.controller_emojiLayoutReactions); + reactionsController.setStickerObjModifier(this::modifyStickerObj); + adapter = new MediaStickersAdapter(this, this, false, this) { + @Override + public void onBindViewHolder (StickerHolder holder, int position) { + super.onBindViewHolder(holder, position); + int type = getItemViewType(position); + if (type == StickerHolder.TYPE_STICKER) { + TGStickerObj stickerObj = getSticker(position); + ((StickerSmallView) holder.itemView).setPadding(Screen.dp(stickerObj != null && stickerObj.isEmojiReaction() ? 0: DEFAULT_STICKER_PADDING_DP)); + ((StickerSmallView) holder.itemView).setChosen(stickerObj != null && state.chosenReactions != null && stickerObj.getReactionType() != null && state.chosenReactions.contains(TD.makeReactionKey(stickerObj.getReactionType()))); + } + + holder.itemView.setVisibility(position <= reactionsController.getSpanCount() || isFullyVisible ? View.VISIBLE: View.INVISIBLE); + } + }; + + + adapter.setLayoutParams(new MediaStickersAdapter.LayoutParams( + (int) (HeaderView.getSize(true) + MessageOptionsPagerController.getPickerTopPadding()), + Screen.dp(RECYCLER_VIEW_LEFT_RIGHT_PADDING), + Screen.dp(8f), + Screen.dp(21 - 9.5f), + getItemHeight() + )); + adapter.setRepaintingColorId(ColorId.text); + + reactionsController.setArguments(this); + reactionsController.setAdapter(adapter); + reactionsController.setItemWidth(9, 38); + + CustomRecyclerView recyclerView = (CustomRecyclerView) reactionsController.getValue(); + recyclerView.setPadding(Screen.dp(RECYCLER_VIEW_LEFT_RIGHT_PADDING), 0, Screen.dp(RECYCLER_VIEW_LEFT_RIGHT_PADDING), 0); + return recyclerView; + } + + public int measureItemsHeight () { + return reactionsController.getItemsHeight(false); + } + + public int getItemWidth () { + return (recyclerView.getMeasuredWidth() - recyclerView.getPaddingLeft() - recyclerView.getPaddingRight()) / reactionsController.getSpanCount(); + } + + public int getItemHeight () { + return Screen.dp(45); + } + + public float getTopHeaderVisibility () { + return topHeaderVisibility.getFloatValue(); + } + + public CustomRecyclerView getRecyclerView () { + return recyclerView; + } + + private void buildCells () { + ArrayList emojiPacks = new ArrayList<>(); + ArrayList emojiItems = new ArrayList<>(); + ArrayList emojiItemsCustom = new ArrayList<>(); + + TdApi.AvailableReaction[] reactions = state.availableReactions; + if (reactions != null) { + emojiItems.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_KEYBOARD_TOP)); + + TGStickerSetInfo pack = TGStickerSetInfo.fromEmojiSection(tdlib, 0, -1, reactions.length); + pack.setStartIndex(emojiItems.size()); + pack.setIsRecent(); + emojiItems.ensureCapacity(reactions.length); + emojiPacks.add(pack); + + for (TdApi.AvailableReaction reaction: reactions) { + final boolean isClassicEmojiReaction = reaction.type.getConstructor() == TdApi.ReactionTypeEmoji.CONSTRUCTOR; + TGReaction reactionObj = tdlib.getReaction(reaction.type); + TGStickerObj stickerObj = reactionObj != null ? reactionObj.newCenterAnimationSicker(): null; + if (stickerObj != null) { + stickerObj.setPreviewOptimizationMode(GifFile.OptimizationMode.EMOJI_PREVIEW); + if (isClassicEmojiReaction) { + if (stickerObj.getPreviewAnimation() != null) { + stickerObj.getPreviewAnimation().setPlayOnce(true); + stickerObj.getPreviewAnimation().setLooped(false); + } + } + } else { + Log.i("WTF_DEBUG", "Can't load sticker"); + } + if (isClassicEmojiReaction || state.chosenReactions.contains(TD.makeReactionKey(reaction.type)) || !state.isPremium) { + emojiItems.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, stickerObj)); + } else { + emojiItemsCustom.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, stickerObj)); + } + } + } + + if (!emojiItemsCustom.isEmpty()) { + TGStickerSetInfo pack = TGStickerSetInfo.fromEmojiSection(tdlib, 1, R.string.Recent, emojiItemsCustom.size()); + pack.setStartIndex(emojiItems.size()); + pack.setIsDefaultEmoji(); + + emojiPacks.get(0).setSize(reactions.length - emojiItemsCustom.size()); + + emojiPacks.add(pack); + emojiItems.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER, pack)); + emojiItems.addAll(emojiItemsCustom); + } + + emojiItems.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_EMPTY)); + + reactionsController.setStickers(emojiPacks, emojiItems); + } + + private void onStickerSectionClick (View v) { + /*if (scrollState != androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE) { + return; + }*/ + + final int viewId = v.getId(); + if (viewId == R.id.btn_stickerSet) { + TGStickerSetInfo info = ((StickerSectionView) v).getStickerSet(); + if (info != null) { + int index = reactionsController.indexOfStickerSet(info); + reactionsController.scrollToStickerSet(index, HeaderView.getSize(true), false, true); + } + } else if (viewId == R.id.btn_section) { + EmojiSection section = ((EmojiSectionView) v).getSection(); + if (section.index == -14) { + bottomHeaderView.openSearchMode(true, false); + } else if (section.index >= 0 && section.index < reactionsController.stickerSets.size()) { + reactionsController.scrollToStickerSet(section.index == 0 ? 0 : reactionsController.stickerSets.get(section.index).getStartIndex(), HeaderView.getSize(true), false, true); + } + } + } + + public void scrollToDefaultPosition (int offset) { + reactionsController.scrollToStickerSet(0, offset, -offset, false, true); + } + + /* * */ + + public void prepareToShow () { + if (recyclerView.getItemAnimator() == null) { + recyclerView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180L)); + } + setIsFullyVisible(true); + } + + private boolean loadingStickers; + + private void loadStickers () { + if (loadingStickers || !state.needShowCustomEmojiInsidePicker) { + return; + } + + loadingStickers = true; + tdlib.client().send(new TdApi.GetInstalledStickerSets(new TdApi.StickerTypeCustomEmoji()), stickerSetsHandler()); + } + + private Client.ResultHandler stickerSetsHandler () { + return object -> { + switch (object.getConstructor()) { + case TdApi.StickerSets.CONSTRUCTOR: { + TdApi.StickerSetInfo[] rawStickerSets = ((TdApi.StickerSets) object).sets; + + final ArrayList stickerSets = new ArrayList<>(rawStickerSets.length); + final ArrayList items = new ArrayList<>(); + + if (rawStickerSets.length > 0) { + int startIndex = this.adapter.getItemCount(); + + for (TdApi.StickerSetInfo rawInfo : rawStickerSets) { + TGStickerSetInfo info = new TGStickerSetInfo(tdlib, rawInfo); + if (info.getSize() == 0) { + continue; + } + stickerSets.add(info); + info.setStartIndex(startIndex); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER, info)); + for (int i = 0; i < rawInfo.size; i++) { + TGStickerObj sticker = new TGStickerObj(tdlib, i < rawInfo.covers.length ? rawInfo.covers[i] : null, null, rawInfo.stickerType); + sticker.setStickerSetId(rawInfo.id, null); + sticker.setDataProvider(stickerSetsDataProvider()); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, sticker)); + } + startIndex += rawInfo.size + 1; + } + } + + runOnUiThreadOptional(() -> { + /*if (getArguments() != null) { + getArguments().setEmojiPacks(stickerSets); + }*/ + setStickers(stickerSets, items); + }); + + break; + } + case TdApi.Error.CONSTRUCTOR: { + UI.showError(object); + break; + } + } + }; + } + + private void setStickers (ArrayList stickerSets, ArrayList items) { + this.reactionsController.addStickers(stickerSets, items); + this.loadingStickers = false; + if (stickerSetsDataProvider != null) { + this.stickerSetsDataProvider.clear(); + } + bottomHeaderCell.setStickerSets(stickerSets); + recyclerView.invalidateItemDecorations(); + } + + /* * */ + + @Override + public int getId () { + return R.id.controller_reactionsPicker; + } + + @Override + public CharSequence getName () { + return Lang.getString(R.string.ReactionsPickerHeader); + } + + @Override + public boolean onBackPressed (boolean fromTop) { + return fakeControllerForBottomHeader != null && fakeControllerForBottomHeader.onBackPressed(fromTop) || super.onBackPressed(fromTop); + } + + @Override + protected int getBackButton () { + return BackHeaderButton.TYPE_CLOSE; + } + + @Override + protected boolean allowMenuReuse () { + return false; + } + + @Override + protected int getHeaderTextColorId () { + return ColorId.text; + } + + @Override + protected int getHeaderColorId () { + return ColorId.filling; + } + + @Override + protected int getHeaderIconColorId () { + return ColorId.icon; + } + + @Override + public boolean needsTempUpdates () { + return true; + } + + @Override + public void setArguments (MessageOptionsPagerController.State args) { + this.state = args; + super.setArguments(args); + } + + @Override + public void destroy () { + super.destroy(); + reactionsController.destroy(); + } + + @Override + public boolean onStickerClick (StickerSmallView view, View clickView, TGStickerObj sticker, boolean isMenuClick, TdApi.MessageSendOptions sendOptions) { + TdApi.ReactionType reactionType = sticker.isCustomEmoji() ? + new TdApi.ReactionTypeCustomEmoji(sticker.getCustomEmojiId()): sticker.getReactionType(); + + TGReaction reaction = tdlib.getReaction(reactionType); + if (reaction == null && sticker.isCustomEmoji() && sticker.getSticker() != null) { + reaction = new TGReaction(tdlib, sticker.getSticker()); + } + + if (reaction != null) { + getArgumentsStrict().onReactionClickListener.onReactionClick(clickView, reaction, false); + return true; + } + return false; + } + + @Override + public boolean onStickerLongClick (StickerSmallView view, TGStickerObj sticker) { + TdApi.ReactionType reactionType = sticker.isCustomEmoji() ? + new TdApi.ReactionTypeCustomEmoji(sticker.getCustomEmojiId()): sticker.getReactionType(); + + TGReaction reaction = tdlib.getReaction(reactionType); + if (reaction == null && sticker.isCustomEmoji() && sticker.getSticker() != null) { + reaction = new TGReaction(tdlib, sticker.getSticker()); + } + + if (reaction != null) { + getArgumentsStrict().onReactionClickListener.onReactionClick(view, reaction, true); + } + + return true; + } + + @Override + public long getStickerOutputChatId () { + return 0; + } + + @Override + public void setStickerPressed (StickerSmallView view, TGStickerObj sticker, boolean isPressed) { + + } + + @Override + public boolean canFindChildViewUnder (StickerSmallView view, int recyclerX, int recyclerY) { + return false; + } + + @Override + public boolean needsLongDelay (StickerSmallView view) { + return false; + } + + @Override + public int getStickersListTop () { + return 0; + } + + @Override + public int getViewportHeight () { + return 0; + } + + + + /* Emoji Layout Callbacks */ + + private boolean isIgnoreMovement; + + public boolean isIgnoreMovement () { + return isIgnoreMovement; + } + + @Override + public void setIgnoreMovement (boolean silent) { + isIgnoreMovement = silent; + } + + @Override + public void resetScrollState (boolean silent) { + + } + + @Override + public void moveHeader (int totalDy) { + + } + + @Override + public void setHasNewHots (int controllerId, boolean hasHots) { + + } + + @Override + public boolean onStickerClick (int controllerId, StickerSmallView view, View clickView, TGStickerSetInfo stickerSet, TGStickerObj sticker, boolean isMenuClick, TdApi.MessageSendOptions sendOptions) { + return false; + } + + @Override + public boolean canFindChildViewUnder (int controllerId, StickerSmallView view, int recyclerX, int recyclerY) { + return true; + } + + @Override + public Context getContext () { + return context; + } + + @Override + public boolean isUseDarkMode () { + return false; + } + + @Override + public long findOutputChatId () { + return 0; + } + + @Override + public void onSectionInteracted (int mediaType, boolean interactionFinished) { + + } + + @Override + public void onSectionInteractedScroll (int mediaType, boolean moved) { + + } + + @Override + public void setCurrentStickerSectionByPosition (int controllerId, int i, boolean isStickerSection, boolean animated) { + bottomHeaderCell.setCurrentStickerSectionByPosition(i + (state.needShowCustomEmojiInsidePicker ? 1: 0), animated); + } + + @Override + public void onAddStickerSection (int controllerId, int section, TGStickerSetInfo info) { + bottomHeaderCell.addStickerSection(section, info); + } + + @Override + public void onMoveStickerSection (int controllerId, int fromSection, int toSection) { + bottomHeaderCell.moveStickerSection(fromSection, toSection); + } + + @Override + public void onRemoveStickerSection (int controllerId, int section) { + bottomHeaderCell.removeStickerSection(section + 1); + } + + @Override + public boolean isAnimatedEmojiOnly () { + return false; + } + + @Override + public float getHeaderHideFactor () { + return 0; + } + + @Override + public void hideSoftwareKeyboard () { + if (fakeControllerForBottomHeader != null) { + fakeControllerForBottomHeader.hideSoftwareKeyboard(); + } + super.hideSoftwareKeyboard(); + } + + + @Override + public void onThemeColorsChanged (boolean areTemp, ColorState state) { + super.onThemeColorsChanged(areTemp, state); + if (headerView != null) { + headerView.resetColors(this, null); + } + if (bottomHeaderView != null) { + bottomHeaderView.resetColors(this, null); + } + } + + + + /* Data provider */ + + private StickerSetsDataProvider stickerSetsDataProvider; + + private StickerSetsDataProvider stickerSetsDataProvider() { + if (stickerSetsDataProvider != null) { + return stickerSetsDataProvider; + } + + return stickerSetsDataProvider = new StickerSetsDataProvider(tdlib) { + @Override + protected boolean needIgnoreRequests (long stickerSetId, TGStickerObj stickerObj) { + return reactionsController.isIgnoreRequests(stickerSetId); + } + + @Override + protected int getLoadingFlags (long stickerSetId, TGStickerObj stickerObj) { + return FLAG_REGULAR; + } + + @Override + protected void applyStickerSet (TdApi.StickerSet stickerSet, int flags) { + if (BitwiseUtils.hasFlag(flags, FLAG_REGULAR)) { + reactionsController.applyStickerSet(stickerSet, this, false); + } + } + }; + } + + + /* Visibility control */ + + private boolean isFullyVisible = false; + + public void setIsFullyVisible (boolean isFullyVisible) { + if (this.isFullyVisible == isFullyVisible) { + return; + } + + this.isFullyVisible = isFullyVisible; + + int itemsCount = adapter.getItemCount(); + int start = reactionsController.getSpanCount() + 1; + if (itemsCount > start) { + adapter.notifyItemRangeChanged(start, itemsCount - start); + } + } + + + + /* Top Header */ + + private LinearLayout topHeaderViewGroup; + + public HeaderView getTopHeaderView () { + return headerView; + } + + public ViewGroup getTopHeaderViewGroup () { + return topHeaderViewGroup; + } + + private boolean topHeaderVisibilityValue; + + public void setTopHeaderVisibility (boolean isVisible) { + if (topHeaderVisibilityValue == isVisible) { + return; + } + topHeaderVisibilityValue = isVisible; + UI.post(() -> { + topHeaderViewGroup.setVisibility(View.VISIBLE); + topHeaderVisibility.setValue(isVisible, true); + }); + } + + + + private void genTopHeader () { + headerView = new HeaderView(context) { + @Override + public boolean onTouchEvent (MotionEvent e) { + super.onTouchEvent(e); + return true; + } + }; + headerView.initWithSingleController(this, false); + headerView.setBackgroundHeight(Screen.dp(56)); + headerView.getBackButton().setIsReverse(true); + addThemeInvalidateListener(headerView); + + View lickView = new View(context) { + @Override + protected void dispatchDraw (Canvas canvas) { + canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), + Paints.fillingPaint(ColorUtils.compositeColors(Theme.getColor(ColorId.statusBar), Theme.getColor(ColorId.headerLightBackground)))); + } + }; + lickView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, HeaderView.getTopOffset())); + addThemeInvalidateListener(lickView); + + topHeaderViewGroup = new LinearLayout(context); + topHeaderViewGroup.setOrientation(LinearLayout.VERTICAL); + topHeaderViewGroup.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, HeaderView.getSize(true) + ShadowView.simpleBottomShadowHeight())); + topHeaderViewGroup.addView(lickView); + topHeaderViewGroup.addView(headerView); + topHeaderViewGroup.setVisibility(View.GONE); + topHeaderViewGroup.setAlpha(0f); + } + + + + /* Bottom Header */ + + private FrameLayout bottomHeaderViewGroup; + private FakeControllerForBottomHeader fakeControllerForBottomHeader; + private HeaderView bottomHeaderView; + private EmojiHeaderView bottomHeaderCell; + private EmojiCategoriesRecyclerView emojiTypesRecyclerView; + private boolean ignoreSearchInputUpdates; + + public HeaderView getBottomHeaderView () { + return bottomHeaderView; + } + + public FrameLayout getBottomHeaderViewGroup () { + return bottomHeaderViewGroup; + } + + protected void onBottomHeaderEnterSearchMode () { + + } + + protected void onBottomHeaderLeaveSearchMode () { + + } + + public void closeBottomHeaderSearchMode (boolean animated) { + bottomHeaderView.closeSearchMode(animated, null); + } + + private void genBottomHeader () { + fakeControllerForBottomHeader = new FakeControllerForBottomHeader(context, tdlib) { + @Override + public View getCustomHeaderCell () { + return bottomHeaderCell; + } + + @Override + protected void onEnterSearchMode () { + super.onEnterSearchMode(); + + emojiTypesRecyclerView.reset(); + emojiTypesRecyclerView.scrollToPosition(0); + emojiTypesRecyclerView.setVisibility(View.VISIBLE); + emojiTypesRecyclerView.setAlpha(bottomHeaderSearchModeVisibility.getFloatValue()); + bottomHeaderSearchModeVisibility.setValue(true, true); + searchEmojiImpl(null, false); + onBottomHeaderEnterSearchMode(); + } + + @Override + protected void onSearchInputChanged (String query) { + super.onSearchInputChanged(query); + if (!ignoreSearchInputUpdates) { + emojiTypesRecyclerView.reset(); + searchEmojiImpl(query, false); + } + } + + @Override + protected void onLeaveSearchMode () { + super.onLeaveSearchMode(); + bottomHeaderSearchModeVisibility.setValue(false, true); + searchEmojiImpl(null, false); + } + + @Override + protected void onAfterLeaveSearchMode () { + super.onAfterLeaveSearchMode(); + onBottomHeaderLeaveSearchMode(); + } + }; + + bottomHeaderView = new HeaderView(context) { + @Override + public boolean onTouchEvent (MotionEvent e) { + super.onTouchEvent(e); + return true; + } + }; + bottomHeaderView.initWithSingleController(fakeControllerForBottomHeader, false); + bottomHeaderView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.BOTTOM)); + fakeControllerForBottomHeader.attachHeaderViewWithoutNavigation(bottomHeaderView); + + emojiTypesRecyclerView = new EmojiCategoriesRecyclerView(context); + emojiTypesRecyclerView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.RIGHT | Gravity.BOTTOM, Screen.dp(56), 0, 0, 0)); + emojiTypesRecyclerView.setAlpha(0); + emojiTypesRecyclerView.setVisibility(View.GONE); + emojiTypesRecyclerView.init(this, this::searchEmojiSection); + emojiTypesRecyclerView.setMinimalLeftPadding(((int) U.measureText(Lang.getString(R.string.Search), Paints.getRegularTextPaint(16))) + Screen.dp(68 - 56)); + + bottomHeaderViewGroup = new FrameLayout(context); + bottomHeaderViewGroup.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, HeaderView.getSize(false), Gravity.BOTTOM)); + bottomHeaderViewGroup.addView(bottomHeaderView); + bottomHeaderViewGroup.addView(emojiTypesRecyclerView); + } + + private String lastEmojiSearchRequest; + + private void searchEmojiSection (String request) { + ignoreSearchInputUpdates = true; + fakeControllerForBottomHeader.clearSearchInput(); + ignoreSearchInputUpdates = false; + + searchEmojiImpl(request, true); + } + + + + /* */ + + private boolean onEmojiHeaderLongClick (View v) { + int viewId = v.getId(); + + if (v instanceof StickerSectionView) { + StickerSectionView sectionView = (StickerSectionView) v; + TGStickerSetInfo info = sectionView.getStickerSet(); + removeStickerSet(info); + return true; + } else if (viewId == R.id.btn_section) { + EmojiSection section = ((EmojiSectionView) v).getSection(); + if (section.index == 1) { + showOptions(null, new int[] {R.id.btn_done, R.id.btn_cancel}, new String[] { + Lang.getString(R.string.ClearRecentReactionsAction), + Lang.getString(R.string.Cancel) + }, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_auto_delete_24, R.drawable.baseline_cancel_24}, (itemView, id) -> { + if (id == R.id.btn_done) { + TGStickerSetInfo info = reactionsController.getStickerSetBySectionIndex(1); + if (info != null) { + reactionsController.removeStickerSet(info); + } + tdlib.client().send(new TdApi.ClearRecentReactions(), tdlib.okHandler()); + } + return true; + }); + return true; + } + } + return false; + } + + private void removeStickerSet (final TGStickerSetInfo info) { + showOptions(null, new int[] {R.id.btn_copyLink, R.id.btn_archive, R.id.more_btn_delete}, new String[] {Lang.getString(R.string.CopyLink), Lang.getString(R.string.ArchivePack), Lang.getString(R.string.DeletePack)}, new int[] {ViewController.OPTION_COLOR_NORMAL, ViewController.OPTION_COLOR_NORMAL, ViewController.OPTION_COLOR_RED}, new int[] {R.drawable.baseline_link_24, R.drawable.baseline_archive_24, R.drawable.baseline_delete_24}, (itemView, id) -> { + if (id == R.id.more_btn_delete) { + showOptions(Lang.getStringBold(R.string.RemoveEmojiSet, info.getTitle()), new int[] {R.id.btn_delete, R.id.btn_cancel}, new String[] {Lang.getString(R.string.RemoveStickerSetAction), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_24, R.drawable.baseline_cancel_24}, (resultItemView, resultId) -> { + if (resultId == R.id.btn_delete) { + reactionsController.removeStickerSet(info); + tdlib().client().send(new TdApi.ChangeStickerSet(info.getId(), false, false), tdlib().okHandler()); + } + return true; + }); + } else if (id == R.id.btn_archive) { + showOptions(Lang.getStringBold(R.string.ArchiveEmojiSet, info.getTitle()), new int[] {R.id.btn_delete, R.id.btn_cancel}, new String[] {Lang.getString(R.string.ArchiveStickerSetAction), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_archive_24, R.drawable.baseline_cancel_24}, (resultItemView, resultId) -> { + if (resultId == R.id.btn_delete) { + reactionsController.removeStickerSet(info); + tdlib().client().send(new TdApi.ChangeStickerSet(info.getId(), false, true), tdlib().okHandler()); + } + return true; + }); + } else if (id == R.id.btn_copyLink) { + TdApi.StickerSetInfo stickerSetInfo = info.getInfo(); + if (stickerSetInfo != null) { + String url = tdlib().tMeStickerSetUrl(stickerSetInfo); + UI.copyText(url, R.string.CopiedLink); + } + } + return true; + }); + } + + + + /* Search */ + + private TdlibUi.EmojiStickers lastEmojiStickers; + + private ArrayList emojiPacksSaved; + private ArrayList emojiItemsSaved; + + private void searchEmojiImpl (final String request, boolean isEmojiString) { + if (StringUtils.equalsOrBothEmpty(lastEmojiSearchRequest, request)) { + return; + } + + if (StringUtils.isEmpty(lastEmojiSearchRequest) && !StringUtils.isEmpty(request)) { + emojiPacksSaved = new ArrayList<>(reactionsController.stickerSets); + emojiItemsSaved = new ArrayList<>(adapter.getItems()); + } + + lastEmojiSearchRequest = request; + + reactionsController.clearAllItems(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_PROGRESS)); + + if (!StringUtils.isEmpty(request)) { + if (lastEmojiStickers == null || !StringUtils.equalsOrBothEmpty(lastEmojiStickers.query, request)) { + lastEmojiStickers = tdlib.ui().getEmojiStickers(new TdApi.StickerTypeCustomEmoji(), request, !isEmojiString, 2000, findOutputChatId()); + } + lastEmojiStickers.getStickers((context, installedStickers, recommendedStickers, b) -> { + if (StringUtils.equalsOrBothEmpty(lastEmojiSearchRequest, context.query)) { + final ArrayList stickers = new ArrayList<>(Arrays.asList(installedStickers)); + if (recommendedStickers != null) { + stickers.addAll(Arrays.asList(recommendedStickers)); + } + + final ArrayList items = new ArrayList<>(1 + stickers.size()); + final ArrayList packs = new ArrayList<>(1); + + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_KEYBOARD_TOP)); + + TGStickerSetInfo pack = TGStickerSetInfo.fromEmojiSection(tdlib, -1, -1, stickers.size()); + pack.setStartIndex(items.size()); + pack.setIsRecent(); + packs.add(pack); + + for (TdApi.Sticker value : stickers) { + TGStickerObj sticker = new TGStickerObj(tdlib, value, null, value.fullType); + sticker.setReactionType(new TdApi.ReactionTypeCustomEmoji(value.id)); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, sticker)); + } + + if (stickers.size() == 0) { + reactionsController.clearAllItems(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_NO_EMOJISETS)); + } else { + reactionsController.clearAllItems(); + reactionsController.setStickers(packs, items); + } + } + }, 0); + } else { + reactionsController.clearAllItems(); + reactionsController.setStickers(emojiPacksSaved, emojiItemsSaved); + emojiPacksSaved = null; + emojiItemsSaved = null; + } + } + + public TGStickerObj modifyStickerObj (TGStickerObj sticker) { + sticker.setDisplayScale(1f); + sticker.setPreviewOptimizationMode(GifFile.OptimizationMode.EMOJI_PREVIEW); + return sticker; + } + + + /* * */ + + private static class FakeControllerForBottomHeader extends ViewController { + public FakeControllerForBottomHeader (@NonNull Context context, Tdlib tdlib) { + super(context, tdlib); + } + + @Override + protected int getHeaderColorId () { + return ColorId.filling; + } + + @Override + protected boolean useGraySearchHeader () { + return true; + } + + @Override + protected View onCreateView (Context context) { + return null; + } + + @Override + public int getId () { + return 0; + } + + @Override + public boolean onBackPressed (boolean fromTop) { + if (inSearchMode()) { + closeSearchMode(null); + return true; + } + return false; + } + } + + + + /* Animations */ + + private static final int BOTTOM_HEADER_IN_SEARCH_MODE = 0; + private static final int TOP_HEADER_IS_VISIBILITY = 1; + + private final BoolAnimator bottomHeaderSearchModeVisibility = new BoolAnimator(BOTTOM_HEADER_IN_SEARCH_MODE, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 220L, false); + private final BoolAnimator topHeaderVisibility = new BoolAnimator(TOP_HEADER_IS_VISIBILITY, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 220L, false); + + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + if (id == BOTTOM_HEADER_IN_SEARCH_MODE) { + emojiTypesRecyclerView.setAlpha(factor); + } else if (id == TOP_HEADER_IS_VISIBILITY) { + topHeaderViewGroup.setTranslationY(-HeaderView.getSize(true) * (1f - factor)); + topHeaderViewGroup.setAlpha(factor); + } + } + + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + if (id == BOTTOM_HEADER_IN_SEARCH_MODE) { + if (finalFactor == 0) { + emojiTypesRecyclerView.setVisibility(View.GONE); + } + } else if (id == TOP_HEADER_IS_VISIBILITY) { + if (finalFactor == 0) { + topHeaderViewGroup.setVisibility(View.GONE); + } + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/ui/RecyclerViewController.java b/app/src/main/java/org/thunderdog/challegram/ui/RecyclerViewController.java index b4f25971c6..93c5fae83a 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/RecyclerViewController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/RecyclerViewController.java @@ -107,9 +107,8 @@ protected View onCreateView (Context context) { ViewSupport.setThemedBackground(wrap, getRecyclerBackground(), this); } wrap.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - recyclerView = (CustomRecyclerView) Views.inflate(context(), R.layout.recycler_custom, null); + recyclerView = onCreateRecyclerView(); Views.setScrollBarPosition(recyclerView); - recyclerView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180l)); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { @@ -119,6 +118,20 @@ public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newSta } } }); + onCreateView(context, recyclerView); + wrap.addView(recyclerView); + if (needPersistentScrollPosition()) { + restorePersistentScrollPosition(); + } + if (needSearch()) { + generateChatSearchView(wrap); + } + return wrap; + } + + protected CustomRecyclerView onCreateRecyclerView () { + CustomRecyclerView recyclerView = (CustomRecyclerView) Views.inflate(context(), R.layout.recycler_custom, null); + recyclerView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180L)); recyclerView.setLayoutManager(new LinearLayoutManager(context, RecyclerView.VERTICAL, false) { @Override public int scrollVerticallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { @@ -131,15 +144,7 @@ public int scrollVerticallyBy(int dx, RecyclerView.Recycler recycler, RecyclerVi } }); recyclerView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - onCreateView(context, recyclerView); - wrap.addView(recyclerView); - if (needPersistentScrollPosition()) { - restorePersistentScrollPosition(); - } - if (needSearch()) { - generateChatSearchView(wrap); - } - return wrap; + return recyclerView; } protected final void restorePersistentScrollPosition () { @@ -239,6 +244,10 @@ protected int findFirstVisiblePosition () { return ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); } + protected int findLastVisiblePosition () { + return ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition(); + } + protected int getViewTop (int position) { View view = recyclerView.getLayoutManager().findViewByPosition(position); return view != null ? view.getTop() : 0; diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SelectChatsController.java b/app/src/main/java/org/thunderdog/challegram/ui/SelectChatsController.java new file mode 100644 index 0000000000..e0cd33fdd6 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/ui/SelectChatsController.java @@ -0,0 +1,1100 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 06/01/2023 + */ +package org.thunderdog.challegram.ui; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; +import android.os.Build; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.IdRes; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.RecyclerView; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.U; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.AvatarPlaceholder; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGFoundChat; +import org.thunderdog.challegram.loader.ComplexReceiver; +import org.thunderdog.challegram.loader.ImageFile; +import org.thunderdog.challegram.loader.ImageReceiver; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.ChatListListener; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccentColor; +import org.thunderdog.challegram.telegram.TdlibChatList; +import org.thunderdog.challegram.telegram.TdlibChatListSlice; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Drawables; +import org.thunderdog.challegram.tool.Icons; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.util.DrawableProvider; +import org.thunderdog.challegram.util.FlowListAnimator; +import org.thunderdog.challegram.util.text.Text; +import org.thunderdog.challegram.util.text.TextColorSet; +import org.thunderdog.challegram.v.CustomRecyclerView; +import org.thunderdog.challegram.widget.AttachDelegate; +import org.thunderdog.challegram.widget.BetterChatView; +import org.thunderdog.challegram.widget.SparseDrawableView; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.BoolAnimator; +import me.vkryl.android.util.ClickHelper; +import me.vkryl.core.ArrayUtils; +import me.vkryl.core.MathUtils; +import me.vkryl.core.StringUtils; +import me.vkryl.core.lambda.Destroyable; +import me.vkryl.td.ChatId; +import me.vkryl.td.ChatPosition; + +public class SelectChatsController extends RecyclerViewController implements View.OnClickListener, ChatListListener { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({MODE_SELECT_CHATS, MODE_FOLDER_INCLUDE_CHATS, MODE_FOLDER_EXCLUDE_CHATS}) + public @interface Mode { + } + + public static final int MODE_SELECT_CHATS = 0; + public static final int MODE_FOLDER_INCLUDE_CHATS = 1; + public static final int MODE_FOLDER_EXCLUDE_CHATS = 2; + + public static class Arguments { + private final @Mode int mode; + private final int chatFolderId; + private final @Nullable TdApi.ChatFolder chatFolder; + private final @Nullable Delegate delegate; + private final Set selectedChatIds; + private final Set selectedChatTypes; + + private Arguments (@Mode int mode, @Nullable Delegate delegate, int chatFolderId, @Nullable TdApi.ChatFolder chatFolder, Set selectedChatIds, Set selectedChatTypes) { + this.mode = mode; + this.delegate = delegate; + this.chatFolder = chatFolder; + this.chatFolderId = chatFolderId; + this.selectedChatIds = selectedChatIds; + this.selectedChatTypes = selectedChatTypes; + } + + public static Arguments includedChats (int chatFolderId, TdApi.ChatFolder chatFolder) { + return includedChats(null, chatFolderId, chatFolder); + } + + public static Arguments includedChats (@Nullable Delegate delegate, int chatFolderId, TdApi.ChatFolder chatFolder) { + Set selectedChatIds = unmodifiableLinkedHashSetOf(chatFolder.pinnedChatIds, chatFolder.includedChatIds); + Set selectedChatTypes = U.unmodifiableTreeSetOf(TD.includedChatTypes(chatFolder)); + return new Arguments(MODE_FOLDER_INCLUDE_CHATS, delegate, chatFolderId, chatFolder, selectedChatIds, selectedChatTypes); + } + + public static Arguments excludedChats (@Nullable Delegate delegate, int chatFolderId, TdApi.ChatFolder chatFolder) { + Set selectedChatIds = unmodifiableLinkedHashSetOf(chatFolder.excludedChatIds); + Set selectedChatTypes = U.unmodifiableTreeSetOf(TD.excludedChatTypes(chatFolder)); + return new Arguments(MODE_FOLDER_EXCLUDE_CHATS, delegate, chatFolderId, chatFolder, selectedChatIds, selectedChatTypes); + } + + private static Set unmodifiableLinkedHashSetOf (long[]... arrays) { + int count = 0; + for (long[] array : arrays) { + count += array.length; + } + LinkedHashSet set = new LinkedHashSet<>(count); + for (long[] array : arrays) { + for (long value : array) { + set.add(value); + } + } + return Collections.unmodifiableSet(set); + } + } + + private @Mode int mode; + private @Nullable Delegate delegate; + private SettingsAdapter adapter; + private TdlibChatListSlice chatListSlice; + private boolean loadingMore, chatListInitialized; + + private final @IdRes int chatsHeaderId = ViewCompat.generateViewId(); + private final @IdRes int chatsFooterId = ViewCompat.generateViewId(); + + private Set selectedChatIds = Collections.emptySet(); + private Set selectedChatTypes = Collections.emptySet(); + + private int secretChatCount; + private int nonSecretChatCount; + + private final BoolAnimator chipGroupVisibilityAnimator = new BoolAnimator(0, (id, factor, fraction, callee) -> { + RecyclerView recyclerView = getRecyclerView(); + recyclerView.post(recyclerView::invalidateItemDecorations); + }, AnimatorUtils.DECELERATE_INTERPOLATOR, 180l); + + public SelectChatsController (@NonNull Context context, Tdlib tdlib) { + super(context, tdlib); + setNeedSearch(); + } + + @Override + public void setArguments (Arguments args) { + super.setArguments(args); + mode = args.mode; + delegate = args.delegate; + selectedChatIds = new LinkedHashSet<>(args.selectedChatIds); + selectedChatTypes = new TreeSet<>(args.selectedChatTypes); + + secretChatCount = 0; + nonSecretChatCount = 0; + for (long selectedChatId : selectedChatIds) { + if (ChatId.isSecret(selectedChatId)) { + secretChatCount++; + } else { + nonSecretChatCount++; + } + } + } + + @Override + public int getId () { + return R.id.controller_selectChats; + } + + @Override + public CharSequence getName () { + Arguments arguments = getArgumentsStrict(); + switch (arguments.mode) { + case MODE_FOLDER_INCLUDE_CHATS: + return Lang.getString(R.string.IncludeChats); + case MODE_FOLDER_EXCLUDE_CHATS: + return Lang.getString(R.string.ExcludeChats); + case MODE_SELECT_CHATS: + return Lang.getString(R.string.SelectChats); + default: + throw new IllegalArgumentException("mode=" + arguments.mode); + } + } + + @Override + public boolean needAsynchronousAnimation () { + return !chatListInitialized; + } + + @Override + public long getAsynchronousAnimationTimeout (boolean fastAnimation) { + return 500l; + } + + @Override + protected void onCreateView (Context context, CustomRecyclerView recyclerView) { + Arguments arguments = getArgumentsStrict(); + adapter = new Adapter(this); + + ArrayList items = new ArrayList<>(); + items.add(new ListItem(ListItem.TYPE_CUSTOM, R.id.input)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + if (arguments.mode == MODE_FOLDER_INCLUDE_CHATS || arguments.mode == MODE_FOLDER_EXCLUDE_CHATS) { + items.add(new ListItem(ListItem.TYPE_EMPTY_OFFSET_SMALL)); + if (mode == MODE_FOLDER_INCLUDE_CHATS) { + CharSequence description = Lang.pluralBold(R.string.IncludeChatsInfo, tdlib.chatFolderChosenChatCountMax()); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, R.id.description, 0, description)); + } else if (mode == MODE_FOLDER_EXCLUDE_CHATS) { + CharSequence description = Lang.pluralBold(R.string.ExcludeChatsInfo, tdlib.chatFolderChosenChatCountMax()); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, R.id.description, 0, description)); + } + + items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.ChatTypes)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + if (arguments.mode == MODE_FOLDER_INCLUDE_CHATS) { + for (int chatType : TD.CHAT_TYPES_TO_INCLUDE) { + items.add(chatTypeItem(chatType)); + } + } + if (arguments.mode == MODE_FOLDER_EXCLUDE_CHATS) { + for (int chatType : TD.CHAT_TYPES_TO_EXCLUDE) { + items.add(chatTypeItem(chatType)); + } + } + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + + items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.Chats)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP, chatsHeaderId)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM, chatsFooterId)); + } + + adapter.setItems(items, false); + recyclerView.setAdapter(adapter); + + int initialChunkSize = Screen.calculateLoadingItems(Screen.dp(72f), 5) + 5; + int chunkSize = Screen.calculateLoadingItems(Screen.dp(72f), 25); + loadingMore = true; + chatListSlice = new TdlibChatListSlice(tdlib, ChatPosition.CHAT_LIST_MAIN, null, true); + + + chatListSlice.initializeList(this, this::processChats, initialChunkSize, () -> { + runOnUiThreadOptional(() -> { + chatListInitialized = true; + executeScheduledAnimation(); + }); + }); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0 && !loadingMore && !inSearchMode() && chatListSlice.canLoad()) { + int lastVisiblePosition = findLastVisiblePosition(); + if (lastVisiblePosition == adapter.getItemCount() - 1) { + chatListSlice.loadMore(chunkSize, /* after */ null); + } + } + } + }); + recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { + @Override + public void getItemOffsets (@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + if (view instanceof ChipGroup) { + int height = ((ChipGroup) view).measureHeight(ViewCompat.isLaidOut(parent) ? parent.getWidth() : Screen.currentWidth()); + int totalHeight = height + SettingHolder.measureHeightForType(ListItem.TYPE_SHADOW_BOTTOM); + int offsetTop = -Math.round(totalHeight * (1f - chipGroupVisibilityAnimator.getFloatValue())); + outRect.set(0, offsetTop, 0, 0); + } else { + outRect.setEmpty(); + } + } + }); + } + + @Override + public void destroy () { + super.destroy(); + chatListSlice.unsubscribeFromUpdates(this); + } + + @Override + public void onClick (View v) { + int id = v.getId(); + if (id == R.id.chat) { + ListItem item = (ListItem) v.getTag(); + long chatId = item.getLongId(); + toggleChatSelection(chatId, v, /* removeOnly */ false); + } else if (ArrayUtils.contains(TD.CHAT_TYPES, id)) { + toggleChatTypeSelection(id, v, /* removeOnly */ false); + } + } + + @Override + protected void onDoneClick () { + if (inSearchMode()) { + closeSearchMode(null); + } else { + saveChanges(this::navigateBack); + } + } + + @Override + public boolean onBackPressed (boolean fromTop) { + if (hasChanges()) { + showUnsavedChangesPromptBeforeLeaving(null); + return true; + } + return super.onBackPressed(fromTop); + } + + private void updateDoneButton () { + setDoneVisible(hasChanges(), true); + } + + private void processChats (List entries) { + if (entries.isEmpty()) { + return; + } + List chats = new ArrayList<>(entries.size()); + for (TdlibChatListSlice.Entry entry : entries) { + chats.add(foundChat(entry)); + } + runOnUiThreadOptional(() -> { + loadingMore = false; + displayChats(chats); + }); + } + + private void displayChats (List chats) { + if (chats.isEmpty()) { + return; + } + List chatItems = new ArrayList<>(chats.size() * 2); + for (TGFoundChat chat : chats) { + chatItems.add(chatItem(chat)); + } + adapter.addItems(indexOfLastChat() + 1, chatItems.toArray(new ListItem[0])); + } + + private ListItem chatTypeItem (@IdRes int id) { + TdlibAccentColor accentColor = tdlib.accentColor(TD.chatTypeAccentColorId(id)); + return new ListItem(ListItem.TYPE_CHAT_BETTER, id, TD.chatTypeIcon24(id), TD.chatTypeName(id)) + .setAccentColor(accentColor); + } + + private ListItem chatItem (TGFoundChat foundChat) { + ListItem item = new ListItem(ListItem.TYPE_CHAT_BETTER, R.id.chat); + item.setLongId(foundChat.getChatId()); + item.setData(foundChat); + return item; + } + + private TGFoundChat foundChat (TdlibChatListSlice.Entry entry) { + return foundChat(entry.chatList, entry.chat); + } + + private TGFoundChat foundChat (TdApi.ChatList chatList, TdApi.Chat chat) { + TGFoundChat foundChat = new TGFoundChat(tdlib, chatList, chat, true, null); + modifyChat(foundChat); + return foundChat; + } + + private int indexOfFistChat () { + return adapter.indexOfViewById(chatsHeaderId) + 1; + } + + private int indexOfLastChat () { + return adapter.indexOfViewById(chatsFooterId) - 1; + } + + private boolean hasChanges () { + Arguments arguments = getArgumentsStrict(); + return !selectedChatTypes.equals(arguments.selectedChatTypes) || !selectedChatIds.equals(arguments.selectedChatIds); + } + + private void saveChanges (@Nullable Runnable after) { + if (delegate != null) { + delegate.onSelectedChatsChanged(mode, selectedChatIds, selectedChatTypes); + if (after != null) { + after.run(); + } + } else { + Arguments arguments = getArgumentsStrict(); + if (arguments.chatFolder != null && (mode == MODE_FOLDER_INCLUDE_CHATS || mode == MODE_FOLDER_EXCLUDE_CHATS)) { + int chatFolderId = arguments.chatFolderId; + TdApi.ChatFolder chatFolder = TD.copyOf(arguments.chatFolder); + if (mode == MODE_FOLDER_INCLUDE_CHATS) { + TD.updateIncludedChats(chatFolder, selectedChatIds); + TD.updateIncludedChatTypes(chatFolder, selectedChatTypes); + } else { + TD.updateExcludedChats(chatFolder, selectedChatIds); + TD.updateExcludedChatTypes(chatFolder, selectedChatTypes); + } + tdlib.send(new TdApi.EditChatFolder(chatFolderId, chatFolder), (chatFolderInfo, error) -> { + if (after != null) { + executeOnUiThreadOptional(after); + } + }); + } + } + } + + private boolean toggleChatSelection (long chatId, @Nullable View view, boolean removeOnly) { + boolean selected = selectedChatIds.contains(chatId); + if (!selected && removeOnly) { + return false; + } + boolean isSecretChat = ChatId.isSecret(chatId); + if (selected) { + selectedChatIds.remove(chatId); + if (isSecretChat) { + secretChatCount--; + } else { + nonSecretChatCount--; + } + } else { + long chosenChatCountMax = tdlib.chatFolderChosenChatCountMax(); + long chosenChatCount = isSecretChat ? secretChatCount : nonSecretChatCount; + if (chosenChatCount >= chosenChatCountMax) { + if (tdlib.hasPremium()) { + CharSequence text = Lang.getMarkdownString(this, R.string.ChatsInFolderLimitReached, chosenChatCountMax); + UI.showCustomToast(text, Toast.LENGTH_LONG, 0); + } else { + tdlib.send(new TdApi.GetPremiumLimit(new TdApi.PremiumLimitTypeChatFolderChosenChatCount()), (premiumLimit, error) -> runOnUiThreadOptional(() -> { + CharSequence text; + if (premiumLimit != null) { + text = Lang.getMarkdownString(this, R.string.PremiumRequiredChatsInFolder, premiumLimit.defaultValue, premiumLimit.premiumValue); + } else { + text = Lang.getMarkdownString(this, R.string.ChatsInFolderLimitReached, chosenChatCountMax); + } + UI.showCustomToast(text, Toast.LENGTH_LONG, 0); + })); + } + return false; + } + selectedChatIds.add(chatId); + if (isSecretChat) { + secretChatCount++; + } else { + nonSecretChatCount++; + } + } + updateDoneButton(); + if (view instanceof BetterChatView) { + ((BetterChatView) view).setIsChecked(!selected, true); + } else { + adapter.updateCheckOptionByLongId(chatId, !selected); + } + adapter.updateSimpleItemById(R.id.input); + return !selected; + } + + private void toggleChatTypeSelection (@IdRes int chatType, @Nullable View view, boolean removeOnly) { + boolean selected = selectedChatTypes.contains(chatType); + if (!selected && removeOnly) { + return; + } + if (selected) { + selectedChatTypes.remove(chatType); + } else { + selectedChatTypes.add(chatType); + } + if (view instanceof BetterChatView) { + ((BetterChatView) view).setIsChecked(!selected, true); + } else { + adapter.updateCheckOptionById(chatType, !selected); + } + updateDoneButton(); + adapter.updateSimpleItemById(R.id.input); + } + + @Override + protected boolean onFoundChatClick (View view, TGFoundChat chat) { + boolean isChatSelected = toggleChatSelection(chat.getChatId(), null, /* removeOnly */ false); + if (view instanceof BetterChatView) { + ((BetterChatView) view).setIsChecked(isChatSelected, true); + } else { + closeSearchMode(null); + } + return true; + } + + @Override + protected void modifyFoundChat (TGFoundChat chat) { + modifyChat(chat); + } + + @Override + protected void modifyFoundChatView (ListItem item, int position, BetterChatView chatView) { + modifyChatView((TGFoundChat) item.getData(), chatView); + } + + @Override + public void onChatAdded (TdlibChatList chatList, TdApi.Chat chat, int atIndex, Tdlib.ChatChange changeInfo) { + runOnUiThreadOptional(() -> { + TGFoundChat foundChat = foundChat(chatList.chatList(), chat); + adapter.addItems(indexOfFistChat() + atIndex, chatItem(foundChat)); + }); + } + + @Override + public void onChatRemoved (TdlibChatList chatList, TdApi.Chat chat, int fromIndex, Tdlib.ChatChange changeInfo) { + runOnUiThreadOptional(() -> { + adapter.removeItem(indexOfFistChat() + fromIndex); + }); + } + + @Override + public void onChatMoved (TdlibChatList chatList, TdApi.Chat chat, int fromIndex, int toIndex, Tdlib.ChatChange changeInfo) { + runOnUiThreadOptional(() -> { + int firstChatIndex = indexOfFistChat(); + adapter.moveItem(firstChatIndex + fromIndex, firstChatIndex + toIndex); + }); + } + + private void modifyChat (TGFoundChat chat) { + chat.setNoUnread(); + if (mode == MODE_FOLDER_INCLUDE_CHATS || mode == MODE_FOLDER_EXCLUDE_CHATS) { + chat.setForcedSubtitle(buildFolderListSubtitle(tdlib, chat)); + } + } + + private void modifyChatView (TGFoundChat chat, BetterChatView chatView) { + chatView.setAllowMaximizePreview(false); + chatView.setIsChecked(selectedChatIds.contains(chat.getChatId()), false); + if (mode == MODE_FOLDER_INCLUDE_CHATS || mode == MODE_FOLDER_EXCLUDE_CHATS) { + chatView.setNoSubtitle(StringUtils.isEmpty(chat.getForcedSubtitle())); + } + } + + private static @Nullable String buildFolderListSubtitle (Tdlib tdlib, TGFoundChat foundChat) { + TdApi.Chat chat = foundChat.getChat(); + if (chat == null) { + chat = tdlib.chat(foundChat.getChatId()); + } + return chat != null ? buildFolderListSubtitle(tdlib, chat) : null; + } + + private static @Nullable String buildFolderListSubtitle (Tdlib tdlib, TdApi.Chat chat) { + TdApi.ChatPosition[] chatPositions = chat.positions; + if (chatPositions != null && chatPositions.length > 0) { + StringBuilder sb = new StringBuilder(); + for (TdApi.ChatPosition chatPosition : chatPositions) { + if (!TD.isChatListFolder(chatPosition.list)) + continue; + TdApi.ChatListFolder chatListFilter = (TdApi.ChatListFolder) chatPosition.list; + TdApi.ChatFolderInfo chatFolderInfo = tdlib.chatFolderInfo(chatListFilter.chatFolderId); + if (chatFolderInfo == null || StringUtils.isEmptyOrBlank(chatFolderInfo.title)) + continue; + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(chatFolderInfo.title); + } + return sb.toString(); + } + return null; + } + + public interface Delegate { + void onSelectedChatsChanged (@Mode int mode, Set chatIds, Set chatTypes); + } + + private class Adapter extends SettingsAdapter { + public Adapter (ViewController context) { + super(context); + } + + @Override + protected SettingHolder initCustom (ViewGroup parent, int customViewType) { + int spacing = Screen.dp(8f); + ChipGroup chipGroup = new ChipGroup(parent.getContext()); + chipGroup.setSpacing(spacing); + chipGroup.setPadding(spacing, spacing, spacing, spacing); + chipGroup.setDelegate(new ChipGroup.Delegate() { + @Override + public void onCrossClick (Chip chip) { + if (chip.type == Chip.TYPE_CHAT_TYPE) { + int chatType = (int) chip.id; + toggleChatTypeSelection(chatType, null, /* removeOnly */ true); + } else if (chip.type == Chip.TYPE_CHAT) { + long chatId = chip.id; + toggleChatSelection(chatId, null, /* removeOnly */ true); + } else { + throw new UnsupportedOperationException(); + } + } + }); + + return new SettingHolder(chipGroup); + } + + @Override + protected void modifyCustom (SettingHolder holder, int position, ListItem item, int customViewType, View view, boolean isUpdate) { + ChipGroup chipGroup = (ChipGroup) view; + List chips = new ArrayList<>(selectedChatIds.size() + selectedChatTypes.size()); + for (int selectedChatType : selectedChatTypes) { + chips.add(chipGroup.chatType(tdlib, selectedChatType)); + } + for (long selectedChatId : selectedChatIds) { + chips.add(chipGroup.chat(tdlib, selectedChatId)); + } + chipGroup.setChips(chips); + chipGroupVisibilityAnimator.setValue(!chips.isEmpty(), isFocused()); + } + + @Override + protected void setChatData (ListItem item, int position, BetterChatView chatView) { + if (item.getId() == R.id.chat) { + TGFoundChat foundChat = (TGFoundChat) item.getData(); + chatView.setChat(foundChat); + SelectChatsController.this.modifyChatView(foundChat, chatView); + } else if (ArrayUtils.contains(TD.CHAT_TYPES, item.getId())) { + chatView.setTitle(item.getString()); + chatView.setSubtitle(null); + chatView.setNoSubtitle(true); + chatView.setAvatar(null, new AvatarPlaceholder.Metadata(item.getAccentColor(), item.getIconResource())); + chatView.setIsChecked(selectedChatTypes.contains(item.getId()), false); + chatView.clearPreviewChat(); + } else { + throw new IllegalArgumentException(); + } + } + } +} + +class Chip extends Drawable implements FlowListAnimator.Measurable, Drawable.Callback, TextColorSet { + public static final int TYPE_CHAT = 1; + public static final int TYPE_CHAT_TYPE = 2; + + private static final float AVATAR_RADIUS = 12f; + + private static final int[] STATE_DEFAULT = new int[] {android.R.attr.state_enabled}; + private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_enabled, android.R.attr.state_pressed}; + + public final long id; + public final int type; + private final Text label; + private final AvatarPlaceholder avatarPlaceholder; + private final @Nullable ImageFile avatarFile; + private final ComplexReceiver complexReceiver; + private final DrawableProvider drawableProvider; + private final boolean isSecretChat; + + private Drawable crossIcon; + private Drawable crossIconRipple; + + private int alpha = 0xFF; + + public Chip (DrawableProvider drawableProvider, ComplexReceiver complexReceiver, Tdlib tdlib, long chatId) { + this.id = chatId; + this.type = TYPE_CHAT; + this.label = buildLabel(tdlib.chatTitle(chatId)); + this.isSecretChat = ChatId.isSecret(chatId); + if (tdlib.isSelfChat(chatId)) { + this.avatarFile = null; + this.avatarPlaceholder = new AvatarPlaceholder(AVATAR_RADIUS, new AvatarPlaceholder.Metadata(tdlib.accentColor(TdlibAccentColor.InternalId.ARCHIVE), R.drawable.baseline_bookmark_16), drawableProvider); + } else if (tdlib.isRepliesChat(chatId)) { + this.avatarFile = null; + this.avatarPlaceholder = new AvatarPlaceholder(AVATAR_RADIUS, new AvatarPlaceholder.Metadata(tdlib.accentColor(TdlibAccentColor.InternalId.REPLIES), R.drawable.baseline_reply_16), drawableProvider); + } else { + this.avatarFile = tdlib.chatAvatar(chatId, Screen.dp(AVATAR_RADIUS * 2)); + this.avatarPlaceholder = tdlib.chatPlaceholder(chatId, tdlib.chat(chatId), true, AVATAR_RADIUS, drawableProvider); + } + this.drawableProvider = drawableProvider; + this.complexReceiver = complexReceiver; + initCrossDrawable(); + } + + public Chip (DrawableProvider drawableProvider, @IdRes int chatType, Tdlib tdlib) { + this.id = chatType; + this.type = TYPE_CHAT_TYPE; + this.label = buildLabel(Lang.getString(TD.chatTypeName(chatType))); + this.isSecretChat = false; + this.avatarFile = null; + this.avatarPlaceholder = new AvatarPlaceholder(AVATAR_RADIUS, new AvatarPlaceholder.Metadata(tdlib.accentColor(TD.chatTypeAccentColorId(chatType)), TD.chatTypeIcon16(chatType)), drawableProvider); + this.drawableProvider = drawableProvider; + this.complexReceiver = null; + initCrossDrawable(); + } + + public boolean inCrossIconTouchBounds (float x, float y) { + Rect bounds = getBounds(); + return bounds.contains(Math.round(x), Math.round(y)) && x >= bounds.right - Screen.dp(34f); + } + + public void setCrossIconPressed (boolean pressed) { + crossIconRipple.setState(pressed ? STATE_PRESSED : STATE_DEFAULT); + } + + private Text buildLabel (String text) { + int maxWidth = (Screen.currentWidth() - Screen.dp(8f) * 3) / 2 - getIntrinsicWidth(/* labelWidth */ 0, /* hasIcon */ isSecretChat); // (´・ᴗ・ ` ) + return new Text.Builder(text, maxWidth, Paints.robotoStyleProvider(14f), this) + .noClickable() + .ignoreNewLines() + .ignoreContinuousNewLines() + .clipTextArea() + .singleLine() + .allBold() + .build(); + } + + private void initCrossDrawable () { + crossIcon = drawableProvider.getSparseDrawable(R.drawable.baseline_close_18, ColorId.NONE); + ShapeDrawable mask = new ShapeDrawable(new OvalShape()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mask.setTint(Color.WHITE); + crossIconRipple = new RippleDrawable(ColorStateList.valueOf(Theme.RIPPLE_COLOR), /* content */ null, mask); + } else { + mask.getPaint().setColor(Theme.RIPPLE_COLOR); + crossIconRipple = Drawables.getColorSelector(null, mask); + } + crossIconRipple.setCallback(this); + crossIconRipple.setState(STATE_DEFAULT); + } + + public void requestFiles () { + if (complexReceiver != null && avatarFile != null) { + ImageReceiver imageReceiver = complexReceiver.getImageReceiver(id); + imageReceiver.setRadius(Screen.dp(AVATAR_RADIUS)); + imageReceiver.requestFile(avatarFile); + } + } + + @Override + public boolean equals (Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Chip that = (Chip) o; + return id == that.id && type == that.type; + } + + @Override + public int hashCode () { + int result = (int) (id ^ (id >>> 32)); + result = 31 * result + type; + return result; + } + + @Override + public int getIntrinsicHeight () { + return Screen.dp(32f); + } + + @Override + public int getIntrinsicWidth () { + int width = getIntrinsicWidth(label.getWidth(), isSecretChat); + int minWidth = Screen.dp(48f); + return Math.max(width, minWidth); + } + + private static int getIntrinsicWidth (int labelWidth, boolean hasIcon) { + int width = Screen.dp(4f + AVATAR_RADIUS * 2 + 8f + 8f + 18f + 8f) + labelWidth; + if (hasIcon) { + width += Screen.dp(15f); + } + return width; + } + + @Override + public void draw (Canvas canvas) { + Rect bounds = getBounds(); + if (bounds.isEmpty() || alpha == 0) { + return; + } + int saveCount; + if (alpha < 0xFF) { + saveCount = canvas.saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, alpha, Canvas.ALL_SAVE_FLAG); + } else { + saveCount = Integer.MIN_VALUE; + } + + int outlineColor = Theme.inlineOutlineColor(false); + Paint outlinePaint = Paints.strokeSmallPaint(outlineColor); + float outlineInset = outlinePaint.getStrokeWidth() / 2f; + int radius = Screen.dp(8f); + RectF roundRect = Paints.getRectF(); + roundRect.set(bounds.left + outlineInset, bounds.top + outlineInset, bounds.right - outlineInset, bounds.bottom - outlineInset); + canvas.drawRoundRect(roundRect, radius, radius, Paints.fillingPaint(Theme.fillingColor())); + canvas.drawRoundRect(roundRect, radius, radius, outlinePaint); + + int avatarRadius = Screen.dp(AVATAR_RADIUS); + int avatarX = bounds.left + avatarRadius + Screen.dp(4f); + int avatarY = bounds.centerY(); + ImageReceiver imageReceiver = avatarFile != null && complexReceiver != null ? complexReceiver.getImageReceiver(id) : null; + if (imageReceiver != null) { + imageReceiver.setBounds(avatarX - avatarRadius, avatarY - avatarRadius, avatarX + avatarRadius, avatarY + avatarRadius); + if (imageReceiver.needPlaceholder()) { + imageReceiver.drawPlaceholderRounded(canvas, avatarRadius, Theme.placeholderColor()); + } + imageReceiver.draw(canvas); + } else { + avatarPlaceholder.draw(canvas, avatarX, avatarY); + } + + int labelX = bounds.left + avatarRadius * 2 + Screen.dp(8f + 4f); + int labelY = bounds.centerY() - label.getLineCenterY(); + if (isSecretChat) { + Drawable secureDrawable = Icons.getSecureDrawable(); + int secureIconX = labelX - Screen.dp(7f); + int secureIconY = bounds.centerY() - secureDrawable.getMinimumHeight() / 2; + Drawables.draw(canvas, secureDrawable, secureIconX, secureIconY, Paints.getGreenPorterDuffPaint()); + labelX += Screen.dp(15f); + } + label.draw(canvas, labelX, labelY); + + int iconX = bounds.right - Screen.dp(17f); + int iconY = bounds.centerY(); + if (crossIconRipple != null) { + int rippleRadius = Screen.dp(28f) / 2; + crossIconRipple.setBounds(iconX - rippleRadius, iconY - rippleRadius, iconX + rippleRadius, iconY + rippleRadius); + crossIconRipple.draw(canvas); + } + Drawables.drawCentered(canvas, crossIcon, iconX, iconY, PorterDuffPaint.get(ColorId.inlineIcon)); + + if (alpha < 0xFF) { + canvas.restoreToCount(saveCount); + } + } + + @Override + public void setAlpha (int alpha) { + if (this.alpha != alpha) { + this.alpha = alpha; + invalidateSelf(); + } + } + + @Override + public int getAlpha () { + return alpha; + } + + @Override + public void setColorFilter (@Nullable ColorFilter colorFilter) { + } + + @Override + public int getOpacity () { + return PixelFormat.TRANSLUCENT; + } + + @Override + public int getWidth () { + return getIntrinsicWidth(); + } + + @Override + public int getHeight () { + return getIntrinsicHeight(); + } + + @Override + public void invalidateDrawable (@NonNull Drawable who) { + Callback callback = getCallback(); + if (callback != null) { + callback.invalidateDrawable(this); + } + } + + @Override + public void scheduleDrawable (@NonNull Drawable who, @NonNull Runnable what, long when) { + Callback callback = getCallback(); + if (callback != null) { + callback.scheduleDrawable(this, what, when); + } + } + + @Override + public void unscheduleDrawable (@NonNull Drawable who, @NonNull Runnable what) { + Callback callback = getCallback(); + if (callback != null) { + callback.unscheduleDrawable(this, what); + } + } + + @Override + public int defaultTextColor () { + return Theme.textAccentColor(); + } +} + +class ChipGroup extends SparseDrawableView implements ClickHelper.Delegate, AttachDelegate, Destroyable { + private final ComplexReceiver complexReceiver = new ComplexReceiver(this); + private final FlowListAnimator animator = new FlowListAnimator<>(animator -> { + if (getHeight() != animator.getMetadata().getTotalHeight()) { + requestLayout(); + } + invalidate(); + }, AnimatorUtils.DECELERATE_INTERPOLATOR, 200l); + private final ClickHelper clickHelper = new ClickHelper(this); + + private List chips = Collections.emptyList(); + private int spacing; + private Delegate delegate; + + public interface Delegate { + default void onCrossClick (Chip chip) {} + } + + public ChipGroup (Context context) { + super(context); + setWillNotDraw(false); + animator.setLineSpacing(Screen.dp(8f)); + animator.setItemSpacing(Screen.dp(8f)); + } + + public void setSpacing (int spacing) { + if (this.spacing != spacing) { + this.spacing = spacing; + requestLayout(); + } + } + + public void setDelegate (Delegate delegate) { + this.delegate = delegate; + } + + public ComplexReceiver getComplexReceiver () { + return complexReceiver; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent (MotionEvent event) { + return delegate != null && clickHelper.onTouchEvent(this, event); + } + + public Chip chat (Tdlib tdlib, long chatId) { + for (Chip chip : chips) { + if (chip.type == Chip.TYPE_CHAT && chip.id == chatId) { + return chip; + } + } + return new Chip(this, complexReceiver, tdlib, chatId); + } + + public Chip chatType (Tdlib tdlib, @IdRes int chatType) { + for (Chip chip : chips) { + if (chip.type == Chip.TYPE_CHAT_TYPE && chip.id == chatType) { + return chip; + } + } + return new Chip(this, chatType, tdlib); + } + + public void setChips (List chips) { + for (Chip chip : this.chips) { + chip.setCallback(null); + } + this.chips = chips; + for (Chip chip : this.chips) { + chip.setCallback(this); + chip.requestFiles(); + } + animator.reset(chips, ViewCompat.isLaidOut(this)); + } + + public int measureHeight (int maxWidth) { + int contentWidth = maxWidth - getPaddingLeft() - getPaddingRight(); + if (contentWidth != animator.getMaxWidth()) { + animator.setMaxWidth(contentWidth); + animator.measure(ViewCompat.isLaidOut(this)); + } + int contentHeight = Math.max(Screen.dp(32f), (int) animator.getMetadata().getTotalHeight()); + return contentHeight + getPaddingTop() + getPaddingBottom(); + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + int measuredHeight = measureHeight(measuredWidth); + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + protected void onDraw (Canvas canvas) { + canvas.drawRect(0, 0, getWidth(), getHeight(), Paints.fillingPaint(Theme.fillingColor())); + canvas.translate(getPaddingLeft(), getPaddingTop()); + for (FlowListAnimator.Entry entry : animator) { + int alpha = Math.round(entry.getVisibility() * 0xFF); + Rect bounds = Paints.getRect(); + entry.getBounds(bounds); + entry.item.setAlpha(MathUtils.clamp(alpha, 0x00, 0xFF)); + entry.item.setBounds(bounds); + entry.item.draw(canvas); + } + canvas.translate(-getPaddingLeft(), -getPaddingTop()); + } + + @Override + public boolean needClickAt (View view, float x, float y) { + if (delegate != null) { + Chip chip = findChipAt(x, y); + return chip != null && chip.inCrossIconTouchBounds(x, y); + } + return false; + } + + @Override + public void onClickAt (View view, float x, float y) { + if (delegate != null) { + Chip chip = findChipAt(x, y); + if (chip != null /*&& chip.inCrossIconTouchBounds(x, y)*/) { + delegate.onCrossClick(chip); + } + } + } + + private @Nullable Chip pressedChip; + + @Override + public void onClickTouchDown (View view, float x, float y) { + pressedChip = findChipAt(x, y); + if (pressedChip != null && pressedChip.inCrossIconTouchBounds(x, y)) { + pressedChip.setCrossIconPressed(true); + } + } + + @Override + public void onClickTouchUp (View view, float x, float y) { + if (pressedChip != null) { + pressedChip.setCrossIconPressed(false); + pressedChip = null; + } + } + + private @Nullable Chip findChipAt (float x, float y) { + int rx = Math.round(x), ry = Math.round(y); + for (FlowListAnimator.Entry entry : animator) { + Rect bounds = entry.item.getBounds(); + if (bounds.contains(rx, ry)) { + return entry.item; + } + } + return null; + } + + @Override + public void attach () { + complexReceiver.attach(); + } + + @Override + public void detach () { + complexReceiver.detach(); + } + + + @Override + public void performDestroy () { + complexReceiver.performDestroy(); + } + + @Override + protected boolean verifyDrawable (@NonNull Drawable who) { + if (super.verifyDrawable(who)) { + return true; + } + for (FlowListAnimator.Entry entry : animator) { + if (entry.item == who) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SetSenderControllerPage.java b/app/src/main/java/org/thunderdog/challegram/ui/SetSenderControllerPage.java index 0284478633..0bbbe3eb03 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SetSenderControllerPage.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SetSenderControllerPage.java @@ -81,7 +81,7 @@ protected void onCreateView (Context context, CustomRecyclerView recyclerView) { @Override public void getItemOffsets (@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); - int bottom = view instanceof EmptySmartView && getKeyboardState() ? -Keyboard.getSize(Keyboard.getSize()): 0; + int bottom = view instanceof EmptySmartView && getKeyboardState() ? -Keyboard.getSize(Keyboard.getSize()) : 0; outRect.set(0, bottom, 0, 0); } }); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingHolder.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingHolder.java index 9d290ac173..0851f24a13 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingHolder.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingHolder.java @@ -1260,6 +1260,7 @@ public void getItemOffsets (Rect outRect, View view, RecyclerView parent, Recycl settingView.setOnClickListener(onClickListener); if (viewType == ListItem.TYPE_RADIO_SETTING_WITH_NEGATIVE_STATE) { settingView.getToggler().setUseNegativeState(true); + adapter.modifySettingView(viewType, settingView); } return new SettingHolder(settingView); } @@ -1489,7 +1490,7 @@ public void getItemOffsets (Rect outRect, View view, RecyclerView parent, Recycl secretState.setLayoutParams(params); secretState.setCompoundDrawablesWithIntrinsicBounds(secretIcon, null, null, null); secretState.setCompoundDrawablePadding(Screen.dp(8)); - secretState.setText(R.string.SessionSecretChats); + secretState.setText(Lang.getString(R.string.SessionSecretChats)); secretState.setTextColor(Theme.getColor(ColorId.textSecure)); secretState.setAllCaps(true); secretState.setGravity(Gravity.CENTER_VERTICAL); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsAdapter.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsAdapter.java index aa8de60f37..265585eaf2 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsAdapter.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsAdapter.java @@ -35,7 +35,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import org.thunderdog.challegram.R; import org.thunderdog.challegram.U; import org.thunderdog.challegram.charts.BaseChartView; import org.thunderdog.challegram.charts.Chart; @@ -931,9 +930,17 @@ public void updateCheckOptionById (int id, boolean isChecked) { } } + public void updateCheckOptionByLongId (long id, boolean isChecked) { + int index = indexOfViewByLongId(id); + if (index != -1) { + setCheckInternal(index, isChecked); + } + } + private void setCheckInternal (int index, boolean isChecked) { boolean needNotify = false; - items.get(index).setSelected(isChecked); + ListItem item = items.get(index); + item.setSelected(isChecked); for (RecyclerView parentView : parentViews) { View view = parentView.getLayoutManager().findViewByPosition(index); if (view == null) { @@ -941,7 +948,12 @@ private void setCheckInternal (int index, boolean isChecked) { continue; } - if (view instanceof SettingView && ((SettingView) view).getChildCount() > 0 && view.getId() == items.get(index).getId()) { + if (view instanceof BetterChatView && view.getId() == item.getId()) { + ((BetterChatView) view).setIsChecked(isChecked, true); + continue; + } + + if (view instanceof SettingView && ((SettingView) view).getChildCount() > 0 && view.getId() == item.getId()) { View child = ((SettingView) view).getChildAt(0); if (child instanceof CheckBoxView) { ((CheckBoxView) child).setChecked(isChecked, true); @@ -955,7 +967,7 @@ private void setCheckInternal (int index, boolean isChecked) { if (view instanceof FrameLayoutFix && ((FrameLayoutFix) view).getChildCount() == 2 && - view.getId() == items.get(index).getId()) { + view.getId() == item.getId()) { switch (items.get(index).getViewType()) { case ListItem.TYPE_DRAWER_ITEM_WITH_RADIO: case ListItem.TYPE_DRAWER_ITEM_WITH_RADIO_SEPARATED: { @@ -970,7 +982,7 @@ private void setCheckInternal (int index, boolean isChecked) { } } - if (view instanceof DrawerItemView && items.get(index).getViewType() == ListItem.TYPE_DRAWER_ITEM_WITH_AVATAR) { + if (view instanceof DrawerItemView && item.getViewType() == ListItem.TYPE_DRAWER_ITEM_WITH_AVATAR) { ((DrawerItemView) view).setChecked(isChecked, true); } @@ -1305,7 +1317,8 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca // adapter stuff @Override - public SettingHolder onCreateViewHolder (ViewGroup parent, int viewType) { + @NonNull + public SettingHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { switch (viewType) { case ListItem.TYPE_CUSTOM_SINGLE: { return initCustom(parent); @@ -1316,7 +1329,19 @@ public SettingHolder onCreateViewHolder (ViewGroup parent, int viewType) { } } } - return SettingHolder.create(context, tdlib, viewType, this, onClickListener, onLongClickListener, themeProvider, innerOnScrollListener, clickHelperDelegate); + SettingHolder holder = SettingHolder.create(context, tdlib, viewType, this, onClickListener, onLongClickListener, themeProvider, innerOnScrollListener, clickHelperDelegate); + View modifiedView = createModifiedView(parent, viewType, holder.itemView); + if (modifiedView != null) { + SettingHolder modifiedHolder = new SettingHolder(modifiedView); + modifiedHolder.setIsRecyclable(holder.isRecyclable()); + return modifiedHolder; + } + return holder; + } + + protected @Nullable View createModifiedView (ViewGroup parent, int viewType, View view) { + // Must be consistent for the adapter instance (not per-item). + return null; } @Override @@ -1771,6 +1796,7 @@ public void updateView (SettingHolder holder, int position, int viewType) { case ListItem.TYPE_RADIO_SETTING_WITH_NEGATIVE_STATE: { SettingView settingView = (SettingView) holder.itemView; settingView.setName(item.getString()); + settingView.setIcon(item.getIconResource()); settingView.getToggler().checkRtl(true); holder.itemView.setEnabled(true); setValuedSetting(item, (SettingView) holder.itemView, false); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsArchiveChatListController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsArchiveChatListController.java new file mode 100644 index 0000000000..a647ee7502 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsArchiveChatListController.java @@ -0,0 +1,160 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 29/09/2023 + */ +package org.thunderdog.challegram.ui; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.IdRes; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.base.SettingView; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.telegram.NotificationSettingsListener; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.v.CustomRecyclerView; + +import java.util.ArrayList; +import java.util.List; + +public class SettingsArchiveChatListController extends RecyclerViewController implements View.OnClickListener, NotificationSettingsListener { + public SettingsArchiveChatListController (Context context, Tdlib tdlib) { + super(context, tdlib); + } + + @Override + public int getId () { + return R.id.controller_archiveSettings; + } + + @Override + public boolean needAsynchronousAnimation () { + return true; + } + + @Override + public CharSequence getName () { + return Lang.getString(R.string.ArchiveSettings); + } + + private TdApi.ArchiveChatListSettings archiveChatListSettings; + private TdApi.Error error; + + private SettingsAdapter adapter; + + @Override + protected void onCreateView (Context context, CustomRecyclerView recyclerView) { + adapter = new SettingsAdapter(this) { + @Override + protected void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) { + @IdRes int id = item.getId(); + if (id == R.id.btn_keepUnmutedChatsArchived) { + view.getToggler().setRadioEnabled(archiveChatListSettings != null && archiveChatListSettings.keepUnmutedChatsArchived, isUpdate); + } else if (id == R.id.btn_keepFolderChatsArchived) { + view.getToggler().setRadioEnabled(archiveChatListSettings != null && archiveChatListSettings.keepChatsFromFoldersArchived, isUpdate); + } else if (id == R.id.btn_archiveMuteNonContacts) { + view.getToggler().setRadioEnabled(archiveChatListSettings != null && archiveChatListSettings.archiveAndMuteNewChatsFromUnknownUsers, isUpdate); + } + } + }; + tdlib.send(new TdApi.GetArchiveChatListSettings(), (settings, error) -> runOnUiThreadOptional(() -> { + this.archiveChatListSettings = settings; + this.error = error; + buildCells(); + executeScheduledAnimation(); + })); + recyclerView.setAdapter(adapter); + tdlib.listeners().subscribeToSettingsUpdates(this); + } + + private void buildCells () { + if (error != null) { + adapter.setItems(new ListItem[] { + new ListItem(ListItem.TYPE_EMPTY, 0, 0, TD.toErrorString(error), false) + }, false); + } else { + List items = new ArrayList<>(); + + items.add(new ListItem(ListItem.TYPE_HEADER_PADDED, 0, 0, R.string.ArchiveSettingUnmutedChatsTitle)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_keepUnmutedChatsArchived, 0, R.string.ArchiveSettingUnmutedChats)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.ArchiveSettingUnmutedChatsDesc)); + + if (tdlib.hasFolders() || archiveChatListSettings.keepChatsFromFoldersArchived) { + items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.ArchiveSettingFolderChatsTitle)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_keepFolderChatsArchived, 0, R.string.ArchiveSettingFolderChats)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.ArchiveSettingFolderChatsDesc)); + } + + if (tdlib.autoArchiveAvailable() || archiveChatListSettings.archiveAndMuteNewChatsFromUnknownUsers) { + items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.UnknownChats)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_archiveMuteNonContacts, 0, R.string.ArchiveNonContacts)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.ArchiveNonContactsInfo)); + } + + adapter.setItems(items, false); + } + } + + @Override + public void onArchiveChatListSettingsChanged (TdApi.ArchiveChatListSettings settings) { + runOnUiThreadOptional(() -> { + if (archiveChatListSettings != null) { + archiveChatListSettings = settings; + adapter.updateValuedSettingById(R.id.btn_keepFolderChatsArchived); + adapter.updateValuedSettingById(R.id.btn_keepUnmutedChatsArchived); + adapter.updateValuedSettingById(R.id.btn_archiveMuteNonContacts); + } + }); + } + + @Override + public void onClick (View v) { + @IdRes int id = v.getId(); + if (archiveChatListSettings == null) { + return; + } + if (id == R.id.btn_keepUnmutedChatsArchived || + id == R.id.btn_keepFolderChatsArchived || + id == R.id.btn_archiveMuteNonContacts) { + boolean value = adapter.toggleView(v); + if (id == R.id.btn_keepUnmutedChatsArchived) { + archiveChatListSettings.keepUnmutedChatsArchived = value; + } else if (id == R.id.btn_keepFolderChatsArchived) { + archiveChatListSettings.keepChatsFromFoldersArchived = value; + } else if (id == R.id.btn_archiveMuteNonContacts) { + archiveChatListSettings.archiveAndMuteNewChatsFromUnknownUsers = value; + } + tdlib.send(new TdApi.SetArchiveChatListSettings(archiveChatListSettings), (ok, error) -> { + if (ok != null) { + tdlib.listeners().notifyArchiveChatListSettingsChanged(archiveChatListSettings); + } + }); + } + } + + @Override + public void destroy () { + super.destroy(); + tdlib.listeners().unsubscribeFromSettingsUpdates(this); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsBlockedController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsBlockedController.java index e9c37d7e10..c5feeca9ca 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsBlockedController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsBlockedController.java @@ -18,6 +18,7 @@ import android.view.View; import android.widget.LinearLayout; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -46,8 +47,9 @@ import me.vkryl.core.ArrayUtils; import me.vkryl.td.ChatId; +import me.vkryl.td.Td; -public class SettingsBlockedController extends RecyclerViewController implements View.OnClickListener, Menu, TdlibCache.UserDataChangeListener, TdlibCache.UserStatusChangeListener, SenderPickerDelegate, Client.ResultHandler, ChatListener { +public class SettingsBlockedController extends RecyclerViewController implements View.OnClickListener, Menu, TdlibCache.UserDataChangeListener, TdlibCache.UserStatusChangeListener, SenderPickerDelegate, Client.ResultHandler, ChatListener { public SettingsBlockedController (Context context, Tdlib tdlib) { super(context, tdlib); } @@ -123,7 +125,7 @@ public void onSenderConfirm (ContactsController context, TdApi.MessageSender sen public void onFocus () { super.onFocus(); if (senderToBlock != null) { - tdlib.blockSender(senderToBlock, true, tdlib.okHandler()); + tdlib.blockSender(senderToBlock, getArgumentsStrict(), tdlib.okHandler()); senderToBlock = null; } } @@ -131,7 +133,11 @@ public void onFocus () { public void unblockSender (TGUser user) { showOptions(Lang.getStringBold(R.string.QUnblockX, tdlib.senderName(user.getSenderId())), new int[]{R.id.btn_unblockSender, R.id.btn_cancel}, new String[]{Lang.getString(R.string.Unblock), Lang.getString(R.string.Cancel)}, new int[]{OPTION_COLOR_RED, OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_block_24, R.drawable.baseline_cancel_24}, (itemView, id) -> { if (id == R.id.btn_unblockSender) { - tdlib.blockSender(user.getSenderId(), false, tdlib.okHandler()); + tdlib.blockSender(user.getSenderId(), null, tdlib.okHandler(() -> { + runOnUiThreadOptional(() -> { + removeSender(user.getSenderId()); + }); + })); } return true; }); @@ -147,7 +153,7 @@ private void loadMore () { return; } isLoadingMore = true; - tdlib.client().send(new TdApi.GetBlockedMessageSenders(loadOffset, 50), this); + tdlib.client().send(new TdApi.GetBlockedMessageSenders(getArgumentsStrict(), loadOffset, 50), this); } @Override @@ -191,7 +197,8 @@ private static TGUser parseSender (Tdlib tdlib, TdApi.MessageSender sender, Arra break; } default: { - throw new UnsupportedOperationException(sender.toString()); + Td.assertMessageSender_439d4c9c(); + throw Td.unsupported(sender); } } parsedUser.setNoBotState(); @@ -214,7 +221,7 @@ protected void setUser (ListItem item, int position, UserView userView, boolean } }; buildCells(); - ViewSupport.setThemedBackground(recyclerView, ColorId.filling, this); + // ViewSupport.setThemedBackground(recyclerView, ColorId.filling, this); RemoveHelper.attach(recyclerView, new RemoveHelper.Callback() { @Override public boolean canRemove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int position) { @@ -239,7 +246,7 @@ public void onScrolled (RecyclerView recyclerView, int dx, int dy) { }); recyclerView.setAdapter(adapter); - tdlib.client().send(new TdApi.GetBlockedMessageSenders(0, 20), result -> { + tdlib.client().send(new TdApi.GetBlockedMessageSenders(getArgumentsStrict(), 0, 20), result -> { if (result.getConstructor() == TdApi.MessageSenders.CONSTRUCTOR) { TdApi.MessageSenders senders = (TdApi.MessageSenders) result; ArrayList list = new ArrayList<>(senders.senders.length); @@ -258,14 +265,14 @@ public void onScrolled (RecyclerView recyclerView, int dx, int dy) { } }); tdlib.cache().addGlobalUsersListener(this); - tdlib.listeners().subscribeForAnyUpdates(this); + tdlib.listeners().subscribeForGlobalUpdates(this); } @Override public void destroy () { super.destroy(); tdlib.cache().removeGlobalUsersListener(this); - tdlib.listeners().unsubscribeFromAnyUpdates(this); + tdlib.listeners().unsubscribeFromGlobalUpdates(this); } private void buildCells () { @@ -335,6 +342,13 @@ private void addSender (TdApi.MessageSender sender) { } } + private void removeSender (TdApi.MessageSender sender) { + int index = indexOfSender(ChatId.fromSender(sender)); + if (index != -1) { + removeSender(index); + } + } + private void removeSender (int position) { if (senders.size() == 1) { senders.clear(); @@ -359,12 +373,13 @@ private int indexOfSender (long chatId) { } @Override - public void onChatBlocked (long chatId, boolean isBlocked) { + public void onChatBlockListChanged (long chatId, @Nullable TdApi.BlockList blockList) { if (ChatId.isSecret(chatId)) { return; } tdlib.ui().post(() -> { if (!isDestroyed() && senders != null) { + final boolean isBlocked = blockList != null && blockList.getConstructor() == getArgumentsStrict().getConstructor(); int index = indexOfSender(chatId); if (isBlocked && index == -1) { long userId = tdlib.chatUserId(chatId); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsBugController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsBugController.java index ab1838f80a..a99a8dac01 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsBugController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsBugController.java @@ -16,18 +16,16 @@ import android.annotation.SuppressLint; import android.content.Context; -import android.icu.util.VersionInfo; +import android.os.Bundle; import android.os.SystemClock; import android.util.SparseIntArray; import android.view.View; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.IdRes; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import com.google.firebase.FirebaseOptions; - import org.drinkless.tdlib.TdApi; import org.drinkmore.Tracer; import org.thunderdog.challegram.BuildConfig; @@ -49,8 +47,10 @@ import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibAccount; import org.thunderdog.challegram.telegram.TdlibManager; +import org.thunderdog.challegram.telegram.TdlibNotificationUtils; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.tool.Intents; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Strings; @@ -58,16 +58,18 @@ import org.thunderdog.challegram.ui.camera.CameraController; import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.unsorted.Test; +import org.thunderdog.challegram.util.AppInstallationUtil; import org.thunderdog.challegram.util.Crash; import org.thunderdog.challegram.util.StringList; import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.voip.VoIP; import org.thunderdog.challegram.voip.VoIPController; import org.thunderdog.challegram.widget.BetterChatView; -import org.thunderdog.challegram.widget.CheckBoxView; import org.thunderdog.challegram.widget.MaterialEditTextGroup; import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -80,6 +82,7 @@ import me.vkryl.core.lambda.RunnableBool; import me.vkryl.core.unit.ByteUnit; import me.vkryl.td.ChatPosition; +import me.vkryl.td.Td; public class SettingsBugController extends RecyclerViewController implements View.OnClickListener, @@ -88,19 +91,33 @@ public class SettingsBugController extends RecyclerViewController= Tdlib.TESTER_LEVEL_TESTER || Settings.instance().isExperimentEnabled(Settings.EXPERIMENT_FLAG_SHOW_PEER_IDS)) { + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_experiment, 0, R.string.Experiment_PeerIds).setLongValue(Settings.EXPERIMENT_FLAG_SHOW_PEER_IDS)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.Experiment_PeerIdsInfo)); + } + + break; + } + case Section.UTILITIES: { if (!items.isEmpty()) { items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); } @@ -813,7 +902,7 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); break; } - case SECTION_TDLIB: { + case Section.TDLIB: { if (items.isEmpty()) items.add(new ListItem(ListItem.TYPE_EMPTY_OFFSET_SMALL)); @@ -874,7 +963,7 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda adapter.setItems(items, false); switch (section) { - case SECTION_MAIN: { + case Section.MAIN: { if (tdlib != null) { getLogFiles(); Log.addOutputListener(this); @@ -908,7 +997,7 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda } break; } - case SECTION_PUSH: { + case Section.PUSH: { tdlib.context().global().addTokenStateListener(this); Settings.instance().addPushStatsListener(this); break; @@ -939,7 +1028,7 @@ public void onNewPushReceived () { public void destroy () { super.destroy(); Log.removeOutputListener(this); - if (section == SECTION_PUSH) { + if (section == Section.PUSH) { tdlib.context().global().removeTokenStateListener(this); Settings.instance().removePushStatsListener(this); } @@ -1059,14 +1148,19 @@ public void runWithBool (boolean arg) { navigateTo(c); } else if (viewId == R.id.btn_switchRtl) { Settings.instance().setNeedRtl(Lang.packId(), adapter.toggleView(v)); + } else if (viewId == R.id.btn_experiment) { + ListItem item = (ListItem) v.getTag(); + if (Settings.instance().setExperimentEnabled(item.getLongValue(), adapter.toggleView(v))) { + scheduleActivityRestart(); + } } else if (viewId == R.id.btn_secret_pushToken) { if (tdlib.context().getTokenState() == TdlibManager.TokenState.OK) { - UI.copyText("Firebase: " + tdlib.context().getToken(), R.string.CopiedText); + UI.copyText(toHumanRepresentation(tdlib.context().getToken()), R.string.CopiedText); } } else if (viewId == R.id.btn_secret_pushConfig) { - FirebaseOptions options = FirebaseOptions.fromResource(UI.getAppContext()); - if (options != null) { - UI.copyText("Firebase config: " + options, R.string.CopiedText); + String configuration = TdlibNotificationUtils.getTokenRetriever().getConfiguration(); + if (!StringUtils.isEmpty(configuration)) { + UI.copyText(configuration, R.string.CopiedText); } } else if (viewId == R.id.btn_secret_appFingerprint) { UI.copyText(U.getApkFingerprint("SHA1"), R.string.CopiedText); @@ -1181,17 +1275,17 @@ public void runWithBool (boolean arg) { openTdlibLogs(testerLevel, crash); } else if (viewId == R.id.btn_pushService) { SettingsBugController c = new SettingsBugController(context, tdlib); - c.setArguments(new Args(SECTION_PUSH, crash).setTesterLevel(testerLevel)); + c.setArguments(new Args(Section.PUSH, crash).setTesterLevel(testerLevel)); navigateTo(c); } else if (viewId == R.id.btn_appLogs) { SettingsBugController c = new SettingsBugController(context, tdlib); - c.setArguments(new Args(SECTION_MAIN, crash).setTesterLevel(testerLevel)); + c.setArguments(new Args(Section.MAIN, crash).setTesterLevel(testerLevel)); navigateTo(c); } else if (viewId == R.id.btn_testingUtils) { RunnableBool callback = proceed -> { if (proceed) { SettingsBugController c = new SettingsBugController(context, tdlib); - c.setArguments(new Args(SECTION_UTILITIES, crash).setTesterLevel(testerLevel)); + c.setArguments(new Args(Section.UTILITIES, crash).setTesterLevel(testerLevel)); navigateTo(c); } }; diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsCloudController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsCloudController.java index a6d4c8527b..069ea8fd17 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsCloudController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsCloudController.java @@ -65,6 +65,7 @@ public SettingsCloudController (Context context, Tdlib tdlib, long tutorialFlag, public static class Args { T applySetting; SettingsThemeController parentController; + SettingsStickersAndEmojiController parentControllerStickers; public Args (T applySetting) { this.applySetting = applySetting; @@ -73,12 +74,20 @@ public Args (T applySetting) { public Args (SettingsThemeController parentController) { this.parentController = parentController; } + + public Args (SettingsStickersAndEmojiController parentController) { + this.parentControllerStickers = parentController; + } } protected final SettingsThemeController getThemeController () { return getArguments() != null ? getArguments().parentController : null; } + protected final SettingsStickersAndEmojiController getStickersAndEmojiController () { + return getArguments() != null ? getArguments().parentControllerStickers : null; + } + protected abstract T getCurrentSetting (); protected abstract void getSettings (RunnableData> callback); protected abstract void applySetting (T setting); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsCloudEmojiController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsCloudEmojiController.java index ce3236672a..344b61cc4f 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsCloudEmojiController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsCloudEmojiController.java @@ -54,8 +54,8 @@ protected void getSettings (RunnableData> callback) { @Override protected void applySetting (Settings.EmojiPack setting) { Emoji.instance().changeEmojiPack(setting); - if (getThemeController() != null) { - getThemeController().updateSelectedEmoji(); + if (getStickersAndEmojiController() != null) { + getStickersAndEmojiController().updateSelectedEmoji(); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsController.java index c43181afb6..aa7ca5fb2b 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsController.java @@ -23,15 +23,15 @@ import android.view.ViewGroup; import android.widget.LinearLayout; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.collection.SparseArrayCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.BuildConfig; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.U; import org.thunderdog.challegram.component.attach.MediaLayout; @@ -43,7 +43,6 @@ import org.thunderdog.challegram.data.TGStickerSetInfo; import org.thunderdog.challegram.loader.AvatarReceiver; import org.thunderdog.challegram.loader.ImageGalleryFile; -import org.thunderdog.challegram.mediaview.MediaViewController; import org.thunderdog.challegram.navigation.ActivityResultHandler; import org.thunderdog.challegram.navigation.BackHeaderButton; import org.thunderdog.challegram.navigation.ComplexHeaderView; @@ -73,6 +72,7 @@ import org.thunderdog.challegram.unsorted.Settings; import org.thunderdog.challegram.unsorted.Size; import org.thunderdog.challegram.util.AppBuildInfo; +import org.thunderdog.challegram.util.AppInstallationUtil; import org.thunderdog.challegram.util.OptionDelegate; import org.thunderdog.challegram.util.PullRequest; import org.thunderdog.challegram.util.StringList; @@ -90,19 +90,23 @@ import me.vkryl.core.StringUtils; import me.vkryl.core.collection.IntList; import me.vkryl.core.lambda.CancellableRunnable; +import me.vkryl.core.reference.ReferenceList; import me.vkryl.td.Td; public class SettingsController extends ViewController implements View.OnClickListener, ComplexHeaderView.Callback, Menu, MoreDelegate, OptionDelegate, TdlibCache.MyUserDataChangeListener, ConnectionListener, StickersListener, MediaLayout.MediaGalleryCallback, - ActivityResultHandler, Client.ResultHandler, View.OnLongClickListener, SessionListener, GlobalTokenStateListener { + ActivityResultHandler, View.OnLongClickListener, SessionListener, GlobalTokenStateListener { + + private final TdlibUi.AvatarPickerManager avatarPickerManager; private ComplexHeaderView headerCell; private ComplexRecyclerView contentView; private SettingsAdapter adapter; public SettingsController (Context context, Tdlib tdlib) { super(context, tdlib); + avatarPickerManager = new TdlibUi.AvatarPickerManager(this); } @Override @@ -121,7 +125,8 @@ protected int getBackButton () { public void onFocus () { super.onFocus(); contentView.setFactorLocked(false); - preloadStickers(); + stickerSetsPreloader.preloadStickers(); + emojiPacksPreloader.preloadStickers(); if (!oneShot) { oneShot = true; tdlib.listeners().subscribeToStickerUpdates(this); @@ -245,56 +250,12 @@ public View getCustomHeaderCell () { } private void changeProfilePhoto () { - IntList ids = new IntList(4); - StringList strings = new StringList(4); - IntList colors = new IntList(4); - IntList icons = new IntList(4); - - final TdApi.User user = tdlib.myUser(); - if (user != null && user.profilePhoto != null) { - ids.append(R.id.btn_open); - strings.append(R.string.Open); - icons.append(R.drawable.baseline_visibility_24); - colors.append(OPTION_COLOR_NORMAL); - } - - ids.append(R.id.btn_changePhotoCamera); - strings.append(R.string.ChatCamera); - icons.append(R.drawable.baseline_camera_alt_24); - colors.append(OPTION_COLOR_NORMAL); - - ids.append(R.id.btn_changePhotoGallery); - strings.append(R.string.Gallery); - icons.append(R.drawable.baseline_image_24); - colors.append(OPTION_COLOR_NORMAL); - - final long profilePhotoToDelete = user != null && user.profilePhoto != null ? user.profilePhoto.id : 0; - if (user != null && user.profilePhoto != null) { - ids.append(R.id.btn_changePhotoDelete); - strings.append(R.string.Delete); - icons.append(R.drawable.baseline_delete_24); - colors.append(OPTION_COLOR_RED); - } - - showOptions(null, ids.get(), strings.get(), colors.get(), icons.get(), (itemView, id) -> { - if (id == R.id.btn_open) { - MediaViewController.openFromProfile(SettingsController.this, user, headerCell); - } else if (id == R.id.btn_changePhotoCamera) { - UI.openCameraDelayed(context); - } else if (id == R.id.btn_changePhotoGallery) { - UI.openGalleryDelayed(context, false); - } else if (id == R.id.btn_changePhotoDelete) { - tdlib.client().send(new TdApi.DeleteProfilePhoto(profilePhotoToDelete), tdlib.okHandler()); - } - return true; - }); + avatarPickerManager.showMenuForProfile(headerCell, false); } @Override public void onActivityResult (int requestCode, int resultCode, Intent data) { - if (resultCode == Activity.RESULT_OK) { - tdlib.ui().handlePhotoChange(requestCode, data, null); - } + avatarPickerManager.handleActivityResult(requestCode, resultCode, data, TdlibUi.AvatarPickerManager.MODE_PROFILE, null, null); } private boolean hasNotificationError; @@ -567,16 +528,21 @@ public void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) } else { view.setData("@" + myUsernames.editableUsername); // TODO multi-username support } + } else if (itemId == R.id.btn_peer_id) { + view.setData(Strings.buildCounter(tdlib.myUserId(true))); } else if (itemId == R.id.btn_phone) { view.setData(myPhone); } else if (itemId == R.id.btn_bio) { TdApi.FormattedText text; if (about == null) { text = TD.toFormattedText(Lang.getString(R.string.LoadingInformation), false); - } else if (Td.isEmpty(about)) { - text = TD.toFormattedText(Lang.getString(R.string.BioNone), false); } else { - text = about; + TdApi.FormattedText about = SettingsController.this.about; + if (Td.isEmpty(about)) { + text = TD.toFormattedText(Lang.getString(R.string.BioNone), false); + } else { + text = about; + } } view.setText(obtainWrapper(text, ID_BIO)); } @@ -588,6 +554,10 @@ public void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) ArrayUtils.ensureCapacity(items, 27); items.add(new ListItem(ListItem.TYPE_EMPTY_OFFSET)); + if (Settings.instance().showPeerIds()) { + items.add(new ListItem(ListItem.TYPE_INFO_SETTING, R.id.btn_peer_id, R.drawable.baseline_identifier_24, R.string.UserId).setContentStrings(R.string.LoadingInformation, R.string.LoadingInformation)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR)); + } items.add(new ListItem(ListItem.TYPE_INFO_SETTING, R.id.btn_username, R.drawable.baseline_alternate_email_24, R.string.Username).setContentStrings(R.string.LoadingUsername, R.string.SetUpUsername)); items.add(new ListItem(ListItem.TYPE_SEPARATOR)); items.add(new ListItem(ListItem.TYPE_INFO_SETTING, R.id.btn_phone, R.drawable.baseline_phone_24, R.string.Phone)); @@ -630,12 +600,16 @@ public void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) items.add(new ListItem(ListItem.TYPE_SEPARATOR)); items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_privacySettings, R.drawable.baseline_lock_24, R.string.PrivacySettings)); items.add(new ListItem(ListItem.TYPE_SEPARATOR)); + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_stickerSettingsAndEmoji, R.drawable.deproko_baseline_stickers_filled_24, R.string.StickersAndEmoji)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR)); items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_themeSettings, R.drawable.baseline_palette_24, R.string.ThemeSettings)); items.add(new ListItem(ListItem.TYPE_SEPARATOR)); items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_tweakSettings, R.drawable.baseline_extension_24, R.string.TweakSettings)); items.add(new ListItem(ListItem.TYPE_SEPARATOR)); - items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_stickerSettings, R.drawable.deproko_baseline_stickers_filled_24, R.string.Stickers)); - items.add(new ListItem(ListItem.TYPE_SEPARATOR)); + if (Settings.instance().chatFoldersEnabled()) { + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_chatFolders, R.drawable.baseline_folder_24, R.string.ChatFolders)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR)); + } items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_languageSettings, R.drawable.baseline_language_24, R.string.Language)); items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); @@ -648,8 +622,47 @@ public void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); - items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_checkUpdates, R.drawable.baseline_google_play_24, U.isAppSideLoaded() ? R.string.AppOnGooglePlay : R.string.CheckForUpdates)); - if (!U.isAppSideLoaded()) { + AppInstallationUtil.DownloadUrl downloadUrl = AppInstallationUtil.getDownloadUrl(null); + @DrawableRes int downloadIconRes; + @StringRes int downloadStringRes = R.string.CheckForUpdates; + if (tdlib.hasUrgentInAppUpdate() && tdlib.isProduction()) { + downloadIconRes = R.drawable.baseline_warning_24; + downloadUrl = new AppInstallationUtil.DownloadUrl(downloadUrl.installerId, tdlib.tMeUrl(BuildConfig.TELEGRAM_UPDATES_CHANNEL)); + } else { + switch (downloadUrl.installerId) { + case AppInstallationUtil.InstallerId.UNKNOWN: { + if (!StringUtils.isEmpty(BuildConfig.GOOGLE_PLAY_URL)) { + downloadUrl = new AppInstallationUtil.DownloadUrl(AppInstallationUtil.InstallerId.GOOGLE_PLAY, BuildConfig.GOOGLE_PLAY_URL); + downloadIconRes = R.drawable.baseline_google_play_24; + downloadStringRes = R.string.AppOnGooglePlay; + } else { + downloadIconRes = R.drawable.baseline_update_24; + } + break; + } + case AppInstallationUtil.InstallerId.GOOGLE_PLAY: { + downloadIconRes = R.drawable.baseline_google_play_24; + break; + } + case AppInstallationUtil.InstallerId.GALAXY_STORE: { + downloadIconRes = R.drawable.baseline_galaxy_store_24; + break; + } + case AppInstallationUtil.InstallerId.HUAWEI_APPGALLERY: { + downloadIconRes = R.drawable.baseline_huawei_24; + break; + } + case AppInstallationUtil.InstallerId.AMAZON_APPSTORE: { + downloadIconRes = R.drawable.baseline_amazon_24; + break; + } + default: + throw new UnsupportedOperationException(); + } + } + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_checkUpdates, downloadIconRes, downloadStringRes) + .setData(downloadUrl)); + if (downloadUrl.installerId == AppInstallationUtil.InstallerId.GOOGLE_PLAY) { items.add(new ListItem(ListItem.TYPE_SEPARATOR)); items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_subscribeToBeta, R.drawable.templarian_baseline_flask_24, R.string.SubscribeToBeta)); } @@ -811,18 +824,6 @@ private void setBio (@Nullable TdApi.FormattedText about) { } } - @Override - public void onResult (TdApi.Object object) { - switch (object.getConstructor()) { - case TdApi.Error.CONSTRUCTOR: - UI.showError(object); - break; - default: - Log.unexpectedTdlibResponse(object, TdApi.GetUserFullInfo.class, TdApi.UserFullInfo.class, TdApi.Error.class); - break; - } - } - @Override public void destroy () { super.destroy(); @@ -942,8 +943,9 @@ public void onSendPhoto (ImageGalleryFile file, boolean isFirst) { } - private void viewGooglePlay () { - tdlib.ui().openUrl(this, BuildConfig.MARKET_URL, new TdlibUi.UrlOpenParameters().disableInstantView()); + private void openInstallerPage (@Nullable AppInstallationUtil.DownloadUrl downloadUrl) { + String url = downloadUrl != null ? downloadUrl.url : BuildConfig.DOWNLOAD_URL; + tdlib.ui().openUrl(this, url, new TdlibUi.UrlOpenParameters().disableInstantView()); } private void viewSourceCode (boolean isTdlib) { @@ -974,6 +976,16 @@ public void onClick (View v) { EditBioController c = new EditBioController(context, tdlib); c.setArguments(new EditBioController.Arguments(about != null ? about.text : "", 0)); navigateTo(c); + } else if (viewId == R.id.btn_peer_id) { + long selfId = tdlib.myUserId(true); + if (selfId == 0) return; + + showOptions(Long.toString(selfId), new int[]{R.id.btn_peer_id_copy}, new String[]{Lang.getString(R.string.Copy)}, null, new int[]{R.drawable.baseline_content_copy_24}, (itemView, id) -> { + if (id == R.id.btn_peer_id_copy) { + UI.copyText(Long.toString(selfId), R.string.CopiedMyUserId); + } + return true; + }); } else if (viewId == R.id.btn_languageSettings) { navigateTo(new SettingsLanguageController(context, tdlib)); } else if (viewId == R.id.btn_notificationSettings) { @@ -981,7 +993,7 @@ public void onClick (View v) { } else if (viewId == R.id.btn_devices) { navigateTo(new SettingsSessionsController(context, tdlib)); } else if (viewId == R.id.btn_checkUpdates) { - viewGooglePlay(); + openInstallerPage(((AppInstallationUtil.DownloadUrl) ((ListItem) v.getTag()).getData())); } else if (viewId == R.id.btn_subscribeToBeta) { tdlib.ui().subscribeToBeta(this); } else if (viewId == R.id.btn_sourceCodeChanges) {// TODO provide an ability to view changes in PRs if they are present in both builds @@ -1031,10 +1043,12 @@ public void onClick (View v) { navigateTo(new SettingsPrivacyController(context, tdlib)); } else if (viewId == R.id.btn_help) { supportOpen = tdlib.ui().openSupport(this); - } else if (viewId == R.id.btn_stickerSettings) { - SettingsStickersController c = new SettingsStickersController(context, tdlib); + } else if (viewId == R.id.btn_stickerSettingsAndEmoji) { + SettingsStickersAndEmojiController c = new SettingsStickersAndEmojiController(context, tdlib); c.setArguments(this); navigateTo(c); + } else if (viewId == R.id.btn_chatFolders) { + navigateTo(new SettingsFoldersController(context, tdlib)); } else if (viewId == R.id.btn_faq) { tdlib.ui().openUrl(this, Lang.getString(R.string.url_faq), new TdlibUi.UrlOpenParameters().forceInstantView()); } else if (viewId == R.id.btn_privacyPolicy) { @@ -1211,6 +1225,11 @@ private void showBuildOptions (boolean allowDebug) { strings.append(R.string.AppLogs); icons.append(R.drawable.baseline_build_24); colors.append(OPTION_COLOR_NORMAL); + + ids.append(R.id.btn_experiment); + strings.append(R.string.ExperimentalSettings); + icons.append(R.drawable.templarian_baseline_flask_24); + colors.append(OPTION_COLOR_NORMAL); } SpannableStringBuilder b = new SpannableStringBuilder(); @@ -1223,14 +1242,19 @@ private void showBuildOptions (boolean allowDebug) { UI.copyText(U.getUsefulMetadata(tdlib), R.string.CopiedText); } else if (id == R.id.btn_pushService) { SettingsBugController c = new SettingsBugController(context, tdlib); - c.setArguments(new SettingsBugController.Args(SettingsBugController.SECTION_PUSH)); + c.setArguments(new SettingsBugController.Args(SettingsBugController.Section.PUSH)); navigateTo(c); } else if (id == R.id.btn_build) { navigateTo(new SettingsBugController(context, tdlib)); - } else if (id == R.id.btn_tdlib) { - tdlib.getTesterLevel(level -> runOnUiThreadOptional(() -> - openTdlibLogs(level, null) - )); + } else if (id == R.id.btn_tdlib || id == R.id.btn_experiment) { + boolean isTdlib = id == R.id.btn_tdlib; + tdlib.getTesterLevel(level -> runOnUiThreadOptional(() -> { + if (isTdlib) { + openTdlibLogs(level, null); + } else { + openExperimentalSettings(level); + } + })); } return true; }); @@ -1238,68 +1262,111 @@ private void showBuildOptions (boolean allowDebug) { @Override public void onInstalledStickerSetsUpdated (long[] stickerSetIds, TdApi.StickerType stickerType) { - if (stickerType.getConstructor() == TdApi.StickerTypeRegular.CONSTRUCTOR) { + if (stickerType.getConstructor() == TdApi.StickerTypeRegular.CONSTRUCTOR || stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR) { runOnUiThreadOptional(() -> { - allStickerSets = null; - hasPreloadedStickers = false; - preloadStickers(); + stickerSetsPreloader.reloadStickersIfEqualTypes(stickerType); + emojiPacksPreloader.reloadStickersIfEqualTypes(stickerType); }); } } // Stickers preloading - private @Nullable ArrayList allStickerSets; + public interface StickerSetLoadListener { + void onStickerSetsLoaded (ArrayList stickerSets, TdApi.StickerType type); + } + + + private final StickerSetsPreloader stickerSetsPreloader = new StickerSetsPreloader(this, new TdApi.StickerTypeRegular()); + private final StickerSetsPreloader emojiPacksPreloader = new StickerSetsPreloader(this, new TdApi.StickerTypeCustomEmoji()); - public @Nullable ArrayList getStickerSets () { - return allStickerSets; + public void addStickerSetListener (boolean isEmoji, StickerSetLoadListener listener) { + (isEmoji ? emojiPacksPreloader : stickerSetsPreloader).addStickerSetListener(listener); } - public interface StickerSetLoadListener { - void onStickerSetsLoaded (ArrayList stickerSets); + public void removeStickerSetListener (boolean isEmoji, StickerSetLoadListener listener) { + (isEmoji ? emojiPacksPreloader : stickerSetsPreloader).removeStickerSetListener(listener); } - private @Nullable StickerSetLoadListener stickerSetListener; + public @Nullable ArrayList getStickerSets (boolean isEmoji) { + return (isEmoji ? emojiPacksPreloader : stickerSetsPreloader).getStickerSets(); + } - public void setStickerSetListener (@Nullable StickerSetLoadListener listener) { - this.stickerSetListener = listener; + public int getStickerSetsCount (boolean isEmoji) { + ArrayList sets = getStickerSets(isEmoji); + return sets != null ? sets.size() : -1; } - private void setStickerSets (@Nullable ArrayList stickerSets) { - this.allStickerSets = stickerSets; - if (stickerSetListener != null) { - stickerSetListener.onStickerSetsLoaded(stickerSets); + private static class StickerSetsPreloader { + private final ReferenceList listeners = new ReferenceList<>(false); + + private final ViewController context; + private final Tdlib tdlib; + private final TdApi.StickerType type; + + private @Nullable ArrayList allStickerSets; + private boolean hasPreloadedStickers; + + public StickerSetsPreloader (ViewController context, TdApi.StickerType type) { + this.context = context; + this.tdlib = context.tdlib(); + this.type = type; } - } - private boolean hasPreloadedStickers; + public @Nullable ArrayList getStickerSets () { + return allStickerSets; + } - private void preloadStickers () { - if (hasPreloadedStickers) { - return; + public void reloadStickersIfEqualTypes (TdApi.StickerType type) { + if (this.type.getConstructor() == type.getConstructor()) { + allStickerSets = null; + hasPreloadedStickers = false; + preloadStickers(); + } } - hasPreloadedStickers = true; - tdlib.client().send(new TdApi.GetInstalledStickerSets(new TdApi.StickerTypeRegular()), object -> { - if (!isDestroyed()) { - switch (object.getConstructor()) { - case TdApi.StickerSets.CONSTRUCTOR: { - TdApi.StickerSetInfo[] stickerSets = ((TdApi.StickerSets) object).sets; - final ArrayList parsedStickerSets = new ArrayList<>(stickerSets.length); - for (TdApi.StickerSetInfo stickerSet : stickerSets) { - parsedStickerSets.add(new TGStickerSetInfo(tdlib, stickerSet)); + + public void preloadStickers () { + if (hasPreloadedStickers) { + return; + } + hasPreloadedStickers = true; + tdlib.client().send(new TdApi.GetInstalledStickerSets(type), object -> { + if (!context.isDestroyed()) { + switch (object.getConstructor()) { + case TdApi.StickerSets.CONSTRUCTOR: { + TdApi.StickerSetInfo[] stickerSets = ((TdApi.StickerSets) object).sets; + final ArrayList parsedStickerSets = new ArrayList<>(stickerSets.length); + for (TdApi.StickerSetInfo stickerSet : stickerSets) { + parsedStickerSets.add(new TGStickerSetInfo(tdlib, stickerSet)); + } + parsedStickerSets.trimToSize(); + context.runOnUiThreadOptional(() -> + setStickerSets(parsedStickerSets) + ); + break; + } + case TdApi.Error.CONSTRUCTOR: { + UI.showError(object); + break; } - parsedStickerSets.trimToSize(); - runOnUiThreadOptional(() -> - setStickerSets(parsedStickerSets) - ); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; } } + }); + } + + private void setStickerSets (@Nullable ArrayList stickerSets) { + this.allStickerSets = stickerSets; + for (StickerSetLoadListener listener : listeners) { + listener.onStickerSetsLoaded(stickerSets, type); } - }); + } + + public void addStickerSetListener (StickerSetLoadListener listener) { + this.listeners.add(listener); + } + + public void removeStickerSetListener (StickerSetLoadListener listener) { + this.listeners.remove(listener); + } } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsFoldersController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsFoldersController.java new file mode 100644 index 0000000000..829dc8a855 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsFoldersController.java @@ -0,0 +1,959 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 06/01/2023 + */ +package org.thunderdog.challegram.ui; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.os.Bundle; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.BuildConfig; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.base.SettingView; +import org.thunderdog.challegram.component.user.RemoveHelper; +import org.thunderdog.challegram.config.Config; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.emoji.Emoji; +import org.thunderdog.challegram.navigation.SettingsWrapBuilder; +import org.thunderdog.challegram.telegram.ChatFolderStyle; +import org.thunderdog.challegram.telegram.ChatFoldersListener; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Strings; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.unsorted.Settings; +import org.thunderdog.challegram.util.AdapterSubListUpdateCallback; +import org.thunderdog.challegram.util.DrawModifier; +import org.thunderdog.challegram.util.ListItemDiffUtilCallback; +import org.thunderdog.challegram.util.text.Text; +import org.thunderdog.challegram.v.CustomRecyclerView; +import org.thunderdog.challegram.widget.NonMaterialButton; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.ArrayUtils; +import me.vkryl.core.BitwiseUtils; +import me.vkryl.core.ColorUtils; +import me.vkryl.core.MathUtils; +import me.vkryl.core.ObjectUtils; +import me.vkryl.core.collection.IntList; +import me.vkryl.core.lambda.RunnableBool; +import me.vkryl.td.ChatPosition; + +public class SettingsFoldersController extends RecyclerViewController implements View.OnClickListener, View.OnLongClickListener, ChatFoldersListener { + private static final long MAIN_CHAT_FOLDER_ID = Long.MIN_VALUE; + private static final long ARCHIVE_CHAT_FOLDER_ID = Long.MIN_VALUE + 1; + + private static final int TYPE_CHAT_FOLDER = 0; + private static final int TYPE_RECOMMENDED_CHAT_FOLDER = 1; + + private final @IdRes int chatFoldersPreviousItemId = ViewCompat.generateViewId(); + private final @IdRes int recommendedChatFoldersPreviousItemId = ViewCompat.generateViewId(); + + private int chatFolderGroupItemCount, recommendedChatFolderGroupItemCount; + private boolean recommendedChatFoldersInitialized; + + private @Nullable TdApi.RecommendedChatFolder[] recommendedChatFolders; + + private SettingsAdapter adapter; + private ItemTouchHelper itemTouchHelper; + + public SettingsFoldersController (Context context, Tdlib tdlib) { + super(context, tdlib); + } + + @Override + public int getId () { + return R.id.controller_chatFolders; + } + + @Override + public boolean needAsynchronousAnimation () { + return !recommendedChatFoldersInitialized; + } + + @Override + public long getAsynchronousAnimationTimeout (boolean fastAnimation) { + return 500l; + } + + @Override + public CharSequence getName () { + return Lang.getString(R.string.ChatFolders); + } + + @Override + protected void onCreateView (Context context, CustomRecyclerView recyclerView) { + itemTouchHelper = RemoveHelper.attach(recyclerView, new ItemTouchHelperCallback()); + + TdApi.ChatFolderInfo[] chatFolders = tdlib.chatFolders(); + int mainChatListPosition = tdlib.mainChatListPosition(); + int archiveChatListPosition = tdlib.settings().archiveChatListPosition(); + List chatFolderItemList = buildChatFolderItemList(chatFolders, mainChatListPosition, archiveChatListPosition); + chatFolderGroupItemCount = chatFolderItemList.size(); + + ArrayList items = new ArrayList<>(); + items.add(new ListItem(ListItem.TYPE_HEADER_PADDED, 0, 0, R.string.ChatFoldersSettings)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_chatFolderStyle, 0, R.string.ChatFoldersAppearance)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_appBadge, 0, R.string.BadgeCounter)); + // items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_countMutedChats, 0, R.string.CountMutedChats)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM, chatFoldersPreviousItemId)); + + items.addAll(chatFolderItemList); + items.add(new ListItem(ListItem.TYPE_PADDING).setHeight(Screen.dp(12f))); + + adapter = new SettingsAdapter(this) { + @SuppressLint("ClickableViewAccessibility") + @Override + protected SettingHolder initCustom (ViewGroup parent, int customViewType) { + switch (customViewType) { + case TYPE_CHAT_FOLDER: { + SettingView settingView = new SettingView(parent.getContext(), tdlib); + settingView.setType(SettingView.TYPE_SETTING); + settingView.addToggler(); + settingView.forcePadding(0, Screen.dp(66f)); + settingView.setOnTouchListener(new ChatFolderOnTouchListener()); + settingView.setOnClickListener(SettingsFoldersController.this); + settingView.setOnLongClickListener(SettingsFoldersController.this); + settingView.getToggler().setOnClickListener(v -> { + ListItem item = (ListItem) settingView.getTag(); + TdApi.ChatList chatList = getChatList(item); + if (Config.RESTRICT_HIDING_MAIN_LIST && isMainChatFolder(item) && settingView.getToggler().isEnabled()) { + return; + } + UI.forceVibrate(v, false); + boolean enabled = settingView.getToggler().toggle(true); + settingView.setVisuallyEnabled(enabled, true); + settingView.setIconColorId(enabled ? ColorId.icon : ColorId.iconLight); + tdlib.settings().setChatListEnabled(chatList, enabled); + }); + addThemeInvalidateListener(settingView); + return new SettingHolder(settingView); + } + case TYPE_RECOMMENDED_CHAT_FOLDER: + SettingView settingView = new SettingView(parent.getContext(), tdlib); + settingView.setType(SettingView.TYPE_INFO_COMPACT); + settingView.setSwapDataAndName(); + settingView.setOnClickListener(SettingsFoldersController.this); + addThemeInvalidateListener(settingView); + + FrameLayout.LayoutParams params = FrameLayoutFix.newParams(Screen.dp(29f), Screen.dp(28f), (Lang.rtl() ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL); + params.leftMargin = params.rightMargin = Screen.dp(17f); + NonMaterialButton button = new NonMaterialButton(parent.getContext()) { + @Override + protected void onSizeChanged (int width, int height, int oldWidth, int oldHeight) { + settingView.forcePadding(0, Math.max(0, width + params.leftMargin + params.rightMargin - Screen.dp(17f))); + } + }; + button.setId(R.id.btn_double); + button.setLayoutParams(params); + button.setText(R.string.PlusSign); + button.setOnClickListener(SettingsFoldersController.this); + settingView.addView(button); + + return new SettingHolder(settingView); + } + throw new IllegalArgumentException("customViewType=" + customViewType); + } + + @Override + protected void modifyCustom (SettingHolder holder, int position, ListItem item, int customViewType, View view, boolean isUpdate) { + if (customViewType == TYPE_CHAT_FOLDER) { + SettingView settingView = (SettingView) holder.itemView; + settingView.setIcon(item.getIconResource()); + settingView.setName(item.getString()); + settingView.setTextColorId(item.getTextColorId(ColorId.NONE)); + settingView.setIgnoreEnabled(true); + settingView.setEnabled(true); + settingView.setDrawModifier(item.getDrawModifier()); + + boolean isEnabled; + if (isMainChatFolder(item) || isArchiveChatFolder(item)) { + isEnabled = tdlib.settings().isChatListEnabled(getChatList(item)); + settingView.setClickable(false); + settingView.setLongClickable(false); + } else if (isChatFolder(item)) { + isEnabled = tdlib.settings().isChatFolderEnabled(item.getIntValue()); + settingView.setClickable(true); + settingView.setLongClickable(true); + } else { + throw new IllegalArgumentException(); + } + settingView.setVisuallyEnabled(isEnabled, false); + settingView.getToggler().setRadioEnabled(isEnabled, false); + settingView.setIconColorId(isEnabled ? ColorId.icon : ColorId.iconLight); + if (Config.RESTRICT_HIDING_MAIN_LIST) { + settingView.getToggler().setVisibility(isMainChatFolder(item) ? View.GONE : View.VISIBLE); + } + } else if (customViewType == TYPE_RECOMMENDED_CHAT_FOLDER) { + SettingView settingView = (SettingView) holder.itemView; + settingView.setIcon(item.getIconResource()); + settingView.setName(item.getString()); + settingView.setData(item.getStringValue()); + settingView.setTextColorId(item.getTextColorId(ColorId.NONE)); + settingView.setEnabled(true); + View button = settingView.findViewById(R.id.btn_double); + button.setEnabled(true); + button.setTag(item.getData()); + } else { + throw new IllegalArgumentException("customViewType=" + customViewType); + } + } + + @SuppressLint("ClickableViewAccessibility") + @Override + protected void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) { + if (item.getId() == R.id.btn_createNewFolder) { + boolean canCreateChatFolder = canCreateChatFolder(); + view.setIgnoreEnabled(true); + view.setVisuallyEnabled(canCreateChatFolder, isUpdate); + view.setIconColorId(canCreateChatFolder ? ColorId.inlineIcon : ColorId.iconLight); + } else { + view.setIgnoreEnabled(false); + view.setEnabledAnimated(true, isUpdate); + view.setIconColorId(ColorId.NONE); + } + if (item.getId() == R.id.btn_chatFolderStyle) { + int positionRes; + if (tdlib.settings().displayFoldersAtTop()) { + positionRes = R.string.ChatFoldersPositionTop; + } else { + positionRes = R.string.ChatFoldersPositionBottom; + } + int styleRes; + switch (tdlib.settings().chatFolderStyle()) { + case ChatFolderStyle.LABEL_AND_ICON: + styleRes = R.string.LabelAndIcon; + break; + case ChatFolderStyle.ICON_ONLY: + styleRes = R.string.IconOnly; + break; + default: + case ChatFolderStyle.LABEL_ONLY: + styleRes = R.string.LabelOnly; + break; + } + view.setData(Lang.getString(R.string.format_chatFoldersPositionAndStyle, Lang.getString(positionRes), Lang.getString(styleRes))); + } else if (item.getId() == R.id.btn_countMutedChats) { + boolean isEnabled = BitwiseUtils.hasFlag(tdlib.settings().getChatFolderBadgeFlags(), Settings.BADGE_FLAG_MUTED); + view.getToggler().setRadioEnabled(isEnabled, isUpdate); + } + } + }; + adapter.setItems(items, false); + recyclerView.setAdapter(adapter); + + tdlib.listeners().subscribeToChatFoldersUpdates(this); + updateRecommendedChatFolders(); + } + + @Override + public void destroy () { + super.destroy(); + tdlib.listeners().unsubscribeFromChatFoldersUpdates(this); + } + + @Override + public boolean saveInstanceState (Bundle outState, String keyPrefix) { + super.saveInstanceState(outState, keyPrefix); + return true; + } + + @Override + public boolean restoreInstanceState (Bundle in, String keyPrefix) { + super.restoreInstanceState(in, keyPrefix); + return true; + } + + private boolean shouldUpdateRecommendedChatFolders = false; + + @Override + protected void onFocusStateChanged () { + if (isFocused()) { + if (shouldUpdateRecommendedChatFolders) { + shouldUpdateRecommendedChatFolders = false; + updateRecommendedChatFolders(); + } + } else { + shouldUpdateRecommendedChatFolders = true; + } + } + + @Override + public void onChatFoldersChanged (TdApi.ChatFolderInfo[] chatFolders, int mainChatListPosition) { + runOnUiThreadOptional(() -> { + adapter.updateValuedSettingById(R.id.btn_createNewFolder); + updateChatFolders(chatFolders, mainChatListPosition, tdlib.settings().archiveChatListPosition()); + if (isFocused()) { + tdlib.ui().postDelayed(() -> { + if (!isDestroyed() && isFocused()) { + updateRecommendedChatFolders(); + } + }, /* ¯\_(ツ)_/¯ */ 500L); + } + }); + } + + @Override + public void onClick (View v) { + if (v.getId() == R.id.btn_createNewFolder) { + if (canCreateChatFolder()) { + navigateTo(EditChatFolderController.newFolder(context, tdlib)); + } else { + showChatFolderLimitReached(v); + } + } else if (v.getId() == R.id.chatFolder) { + ListItem item = (ListItem) v.getTag(); + if (isMainChatFolder(item) || isArchiveChatFolder(item)) { + return; + } + editChatFolder((TdApi.ChatFolderInfo) item.getData()); + } else if (v.getId() == R.id.recommendedChatFolder) { + if (canCreateChatFolder()) { + ListItem item = (ListItem) v.getTag(); + TdApi.ChatFolder chatFolder = (TdApi.ChatFolder) item.getData(); + chatFolder.icon = tdlib.chatFolderIcon(chatFolder); + navigateTo(EditChatFolderController.newFolder(context, tdlib, chatFolder)); + } else { + showChatFolderLimitReached(v); + } + } else if (v.getId() == R.id.btn_double) { + Object tag = v.getTag(); + if (tag instanceof TdApi.ChatFolder) { + if (canCreateChatFolder()) { + v.setEnabled(false); + TdApi.ChatFolder chatFolder = (TdApi.ChatFolder) tag; + WeakReference viewRef = new WeakReference<>(v); + createChatFolder(chatFolder, (ok) -> { + if (ok) { + removeRecommendedChatFolder(chatFolder); + } else { + View view = viewRef.get(); + if (view != null && view.getTag() == tag) { + view.setEnabled(true); + } + } + }); + } else { + showChatFolderLimitReached(v); + } + } + } else if (v.getId() == R.id.btn_chatFolderStyle) { + int chatFolderStyle = tdlib.settings().chatFolderStyle(); + ListItem[] items = new ListItem[] { + new ListItem(ListItem.TYPE_CHECKBOX_OPTION, R.id.btn_displayFoldersAtTop, 0, R.string.DisplayFoldersAtTheTop, tdlib.settings().displayFoldersAtTop()), + new ListItem(ListItem.TYPE_SHADOW_BOTTOM).setTextColorId(ColorId.background), + new ListItem(ListItem.TYPE_SHADOW_TOP).setTextColorId(ColorId.background), + new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_labelOnly, 0, R.string.LabelOnly, R.id.btn_chatFolderStyle, chatFolderStyle == ChatFolderStyle.LABEL_ONLY), + new ListItem(ListItem.TYPE_SEPARATOR_FULL), + new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_iconOnly, 0, R.string.IconOnly, R.id.btn_chatFolderStyle, chatFolderStyle == ChatFolderStyle.ICON_ONLY), + new ListItem(ListItem.TYPE_SEPARATOR_FULL), + new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_labelAndIcon, 0, R.string.LabelAndIcon, R.id.btn_chatFolderStyle, chatFolderStyle == ChatFolderStyle.LABEL_AND_ICON), + }; + SettingsWrapBuilder settings = new SettingsWrapBuilder(R.id.btn_chatFolderStyle) + .setRawItems(items) + .setNeedSeparators(false) + .setIntDelegate((id, result) -> { + int selection = result.get(R.id.btn_chatFolderStyle); + int style; + if (selection == R.id.btn_iconOnly) { + style = ChatFolderStyle.ICON_ONLY; + } else if (selection == R.id.btn_labelAndIcon) { + style = ChatFolderStyle.LABEL_AND_ICON; + } else { + style = ChatFolderStyle.LABEL_ONLY; + } + boolean displayFoldersAtTop = result.get(R.id.btn_displayFoldersAtTop) != 0; + tdlib.settings().setChatFolderStyle(style); + tdlib.settings().setDisplayFoldersAtTop(displayFoldersAtTop); + adapter.updateValuedSettingById(R.id.btn_chatFolderStyle); + }); + showSettings(settings); + } else if (v.getId() == R.id.btn_countMutedChats) { + boolean countMuted = adapter.toggleView(v); + int badgeFlags = BitwiseUtils.setFlag(tdlib.settings().getChatFolderBadgeFlags(), Settings.BADGE_FLAG_MUTED, countMuted); + tdlib.settings().setChatFolderBadgeFlags(badgeFlags); + } else if (v.getId() == R.id.btn_appBadge) { + SettingsNotificationController c = new SettingsNotificationController(context, tdlib); + c.setArguments(new SettingsNotificationController.Args(SettingsNotificationController.Section.APP_BADGE, 0)); + navigateTo(c); + } + } + + @Override + public boolean onLongClick (View v) { + if (v.getId() == R.id.chatFolder) { + ListItem item = (ListItem) v.getTag(); + if (isMainChatFolder(item) || isArchiveChatFolder(item)) { + return false; + } + showChatFolderOptions((TdApi.ChatFolderInfo) item.getData()); + return true; + } + return false; + } + + private void startDrag (RecyclerView.ViewHolder viewHolder) { + if (viewHolder == null) + return; + ListItem listItem = (ListItem) viewHolder.itemView.getTag(); + if (isMainChatFolder(listItem) && !tdlib.hasPremium()) { + UI.forceVibrateError(viewHolder.itemView); + CharSequence markdown = Lang.getString(R.string.PremiumRequiredMoveFolder, Lang.getString(R.string.CategoryMain)); + context() + .tooltipManager() + .builder(viewHolder.itemView) + .icon(R.drawable.dotvhs_baseline_folders_reorder_24) + .controller(this) + .show(tdlib, Strings.buildMarkdown(this, markdown)) + .hideDelayed(); + return; + } + itemTouchHelper.startDrag(viewHolder); + } + + private void showChatFolderLimitReached (View view) { + UI.forceVibrateError(view); + if (tdlib.hasPremium()) { + showTooltip(view, Lang.getMarkdownString(this, R.string.ChatFolderLimitReached, tdlib.chatFolderCountMax())); + } else { + Object viewTag = view.getTag(); + WeakReference viewRef = new WeakReference<>(view); + tdlib.send(new TdApi.GetPremiumLimit(new TdApi.PremiumLimitTypeChatFolderCount()), (premiumLimit, error) -> runOnUiThreadOptional(() -> { + View v = viewRef.get(); + if (v == null || !ViewCompat.isAttachedToWindow(v) || viewTag != v.getTag()) + return; + CharSequence text; + if (premiumLimit != null) { + text = Lang.getMarkdownString(this, R.string.PremiumRequiredCreateFolder, premiumLimit.defaultValue, premiumLimit.premiumValue); + } else { + text = Lang.getMarkdownString(this, R.string.ChatFolderLimitReached, tdlib.chatFolderCountMax()); + } + showTooltip(v, text); + })); + } + } + + private void showTooltip (View view, CharSequence text) { + context() + .tooltipManager() + .builder(view) + .controller(this) + .show(tdlib, text) + .hideDelayed(3500, TimeUnit.MILLISECONDS); + } + + private void showChatFolderOptions (TdApi.ChatFolderInfo chatFolderInfo) { + Options options = new Options.Builder() + .info(chatFolderInfo.title) + .item(new OptionItem(R.id.btn_edit, Lang.getString(R.string.EditFolder), OPTION_COLOR_NORMAL, R.drawable.baseline_edit_24)) + .item(new OptionItem(R.id.btn_delete, Lang.getString(R.string.RemoveFolder), OPTION_COLOR_RED, R.drawable.baseline_delete_24)) + .build(); + showOptions(options, (optionItemView, id) -> { + if (id == R.id.btn_edit) { + editChatFolder(chatFolderInfo); + } else if (id == R.id.btn_delete) { + showRemoveFolderConfirm(chatFolderInfo.id); + } + return true; + }); + } + + private void showRemoveFolderConfirm (int chatFolderId) { + showConfirm(Lang.getString(R.string.RemoveFolderConfirm), Lang.getString(R.string.Remove), R.drawable.baseline_delete_24, OPTION_COLOR_RED, () -> { + deleteChatFolder(chatFolderId); + }); + } + + private void editChatFolder (TdApi.ChatFolderInfo chatFolderInfo) { + tdlib.send(new TdApi.GetChatFolder(chatFolderInfo.id), (chatFolder, error) -> runOnUiThreadOptional(() -> { + if (error != null) { + UI.showError(error); + } else { + EditChatFolderController controller = new EditChatFolderController(context, tdlib); + controller.setArguments(new EditChatFolderController.Arguments(chatFolderInfo.id, chatFolder)); + navigateTo(controller); + } + })); + } + + private void createChatFolder (TdApi.ChatFolder chatFolder, RunnableBool after) { + tdlib.send(new TdApi.CreateChatFolder(chatFolder), (chatFolderInfo, error) -> runOnUiThreadOptional(() -> { + after.runWithBool(error == null); + if (error != null) { + UI.showError(error); + } + })); + } + + private void deleteChatFolder (int chatFolderId) { + int position = -1; + TdApi.ChatFolderInfo[] chatFolders = tdlib.chatFolders(); + for (int index = 0; index < chatFolders.length; index++) { + TdApi.ChatFolderInfo chatFolder = chatFolders[index]; + if (chatFolder.id == chatFolderId) { + position = index; + break; + } + } + if (position != -1) { + int archiveChatListPosition = tdlib.settings().archiveChatListPosition(); + if (position >= tdlib.mainChatListPosition()) position++; + if (position >= archiveChatListPosition) position++; + boolean affectsArchiveChatListPosition = position < archiveChatListPosition && archiveChatListPosition < chatFolders.length + 2; + tdlib.send(new TdApi.DeleteChatFolder(chatFolderId, null), tdlib.typedOkHandler(() -> { + if (affectsArchiveChatListPosition && archiveChatListPosition == tdlib.settings().archiveChatListPosition()) { + tdlib.settings().setArchiveChatListPosition(archiveChatListPosition - 1); + if (!isDestroyed()) { + updateChatFolders(); + } + } + })); + } + } + + private void reorderChatFolders () { + int firstIndex = indexOfFirstChatFolder(); + int lastIndex = indexOfLastChatFolder(); + if (firstIndex == RecyclerView.NO_POSITION || lastIndex == RecyclerView.NO_POSITION) + return; + int mainChatListPosition = 0; + int archiveChatListPosition = 0; + IntList chatFoldersIds = new IntList(tdlib.chatFoldersCount()); + int folderPosition = 0; + for (int index = firstIndex; index <= lastIndex; index++) { + ListItem item = adapter.getItem(index); + if (item == null) { + updateChatFolders(); + return; + } + if (isChatFolder(item)) { + if (isMainChatFolder(item)) { + mainChatListPosition = folderPosition; + } else if (isArchiveChatFolder(item)) { + archiveChatListPosition = folderPosition; + } else { + chatFoldersIds.append(item.getIntValue()); + } + folderPosition++; + } + } + if (mainChatListPosition > archiveChatListPosition) { + mainChatListPosition--; + } + if (archiveChatListPosition > chatFoldersIds.size()) { + archiveChatListPosition = Integer.MAX_VALUE; + } + if (mainChatListPosition != 0 && !tdlib.hasPremium()) { + updateChatFolders(); + return; + } + tdlib.settings().setArchiveChatListPosition(archiveChatListPosition); + if (chatFoldersIds.size() > 0) { + tdlib.send(new TdApi.ReorderChatFolders(chatFoldersIds.get(), mainChatListPosition), (ok, error) -> { + if (error != null) { + UI.showError(error); + runOnUiThreadOptional(this::updateChatFolders); + } + }); + } + } + + private boolean isChatFolder (ListItem item) { + return item.getId() == R.id.chatFolder; + } + + private TdApi.ChatList getChatList (ListItem item) { + if (!isChatFolder(item)) + throw new IllegalArgumentException(); + if (isMainChatFolder(item)) { + return ChatPosition.CHAT_LIST_MAIN; + } else if (isArchiveChatFolder(item)) { + return ChatPosition.CHAT_LIST_ARCHIVE; + } else { + return new TdApi.ChatListFolder(item.getIntValue()); + } + } + + private boolean isMainChatFolder (ListItem item) { + return isChatFolder(item) && item.getLongId() == MAIN_CHAT_FOLDER_ID; + } + + private boolean isArchiveChatFolder (ListItem item) { + return isChatFolder(item) && item.getLongId() == ARCHIVE_CHAT_FOLDER_ID; + } + + private boolean canMoveChatFolder (ListItem item) { + return isChatFolder(item) && (tdlib.hasPremium() || !isMainChatFolder(item)); + } + + private int indexOfFirstChatFolder () { + int index = indexOfChatFolderGroup(); + return index == RecyclerView.NO_POSITION ? RecyclerView.NO_POSITION : index + 2 /* header, shadowTop */; + } + + private int indexOfLastChatFolder () { + int index = indexOfChatFolderGroup(); + return index == RecyclerView.NO_POSITION ? RecyclerView.NO_POSITION : index + chatFolderGroupItemCount - 2 /* shadowBottom, separator */; + } + + private int indexOfChatFolderGroup () { + int index = adapter.indexOfViewById(chatFoldersPreviousItemId); + return index == RecyclerView.NO_POSITION ? RecyclerView.NO_POSITION : index + 1; + } + + private int indexOfRecommendedChatFolderGroup () { + int index = adapter.indexOfViewById(recommendedChatFoldersPreviousItemId); + return index == RecyclerView.NO_POSITION ? RecyclerView.NO_POSITION : index + 1; + } + + private List buildChatFolderItemList (TdApi.ChatFolderInfo[] chatFolders, int mainChatListPosition, int archiveChatListPosition) { + int chatFolderCount = chatFolders.length + 2; /* All Chats, Archived */ + int chatFolderIndex = 0; + mainChatListPosition = MathUtils.clamp(mainChatListPosition, 0, chatFolders.length); + archiveChatListPosition = MathUtils.clamp(archiveChatListPosition, 0, chatFolderCount - 1); + if (mainChatListPosition >= archiveChatListPosition) { + mainChatListPosition++; + } + List itemList = new ArrayList<>(chatFolderCount + 5); + itemList.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.ChatFolders)); + itemList.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + for (int position = 0; position < chatFolderCount; position++) { + if (position == mainChatListPosition) { + itemList.add(mainChatFolderItem()); + } else if (position == archiveChatListPosition) { + itemList.add(archiveChatFolderItem()); + } else if (chatFolderIndex < chatFolders.length) { + TdApi.ChatFolderInfo chatFolder = chatFolders[chatFolderIndex++]; + itemList.add(chatFolderItem(chatFolder)); + } else if (BuildConfig.DEBUG) { + throw new RuntimeException(); + } + } + itemList.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_createNewFolder, R.drawable.baseline_create_new_folder_24, R.string.CreateNewFolder).setTextColorId(ColorId.inlineText)); + itemList.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + itemList.add(new ListItem(ListItem.TYPE_DESCRIPTION, recommendedChatFoldersPreviousItemId, 0, R.string.ChatFoldersInfo)); + return itemList; + } + + private List buildRecommendedChatFolderItemList (TdApi.RecommendedChatFolder[] recommendedChatFolders) { + if (recommendedChatFolders.length == 0) { + return Collections.emptyList(); + } + List itemList = new ArrayList<>(recommendedChatFolders.length * 2 - 1 + 3); + itemList.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.RecommendedFolders)); + itemList.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + for (int index = 0; index < recommendedChatFolders.length; index++) { + if (index > 0) { + itemList.add(new ListItem(ListItem.TYPE_SEPARATOR)); + } + itemList.add(recommendedChatFolderItem(recommendedChatFolders[index])); + } + itemList.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + return itemList; + } + + private ListItem mainChatFolderItem () { + ListItem item = new ListItem(ListItem.TYPE_CUSTOM - TYPE_CHAT_FOLDER, R.id.chatFolder); + item.setString(R.string.CategoryMain); + item.setLongId(MAIN_CHAT_FOLDER_ID); + item.setIconRes(tdlib.hasPremium() ? R.drawable.baseline_drag_handle_24 : R.drawable.deproko_baseline_lock_24); + item.setDrawModifier(new FolderBadge(Lang.getString(R.string.MainListBadge))); + return item; + } + + private ListItem archiveChatFolderItem () { + ListItem item = new ListItem(ListItem.TYPE_CUSTOM - TYPE_CHAT_FOLDER, R.id.chatFolder); + item.setString(R.string.CategoryArchive); + item.setLongId(ARCHIVE_CHAT_FOLDER_ID); + item.setIconRes(R.drawable.baseline_drag_handle_24); + item.setDrawModifier(new FolderBadge(Lang.getString(R.string.LocalFolderBadge))); + return item; + } + + private ListItem chatFolderItem (TdApi.ChatFolderInfo chatFolderInfo) { + ListItem item = new ListItem(ListItem.TYPE_CUSTOM - TYPE_CHAT_FOLDER, R.id.chatFolder, R.drawable.baseline_drag_handle_24, Emoji.instance().replaceEmoji(chatFolderInfo.title)); + item.setIntValue(chatFolderInfo.id); + item.setLongId(chatFolderInfo.id); + item.setData(chatFolderInfo); + return item; + } + + private ListItem recommendedChatFolderItem (TdApi.RecommendedChatFolder recommendedChatFolder) { + ListItem item = new ListItem(ListItem.TYPE_CUSTOM - TYPE_RECOMMENDED_CHAT_FOLDER, R.id.recommendedChatFolder); + item.setData(recommendedChatFolder.folder); + item.setString(recommendedChatFolder.folder.title); + item.setStringValue(recommendedChatFolder.description); + item.setIconRes(tdlib.chatFolderIconDrawable(recommendedChatFolder.folder, R.drawable.baseline_folder_24)); + return item; + } + + private boolean canCreateChatFolder () { + return tdlib.chatFoldersCount() < tdlib.chatFolderCountMax(); + } + + private void updateChatFolders () { + updateChatFolders(tdlib.chatFolders(), tdlib.mainChatListPosition(), tdlib.settings().archiveChatListPosition()); + } + + private void updateChatFolders (TdApi.ChatFolderInfo[] chatFolders, int mainChatListPosition, int archiveChatListPosition) { + int fromIndex = indexOfChatFolderGroup(); + if (fromIndex == RecyclerView.NO_POSITION) + return; + List subList = adapter.getItems().subList(fromIndex, fromIndex + chatFolderGroupItemCount); + List newList = buildChatFolderItemList(chatFolders, mainChatListPosition, archiveChatListPosition); + chatFolderGroupItemCount = newList.size(); + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(chatFoldersDiff(subList, newList)); + subList.clear(); + subList.addAll(newList); + diffResult.dispatchUpdatesTo(new AdapterSubListUpdateCallback(adapter, fromIndex)); + } + + private void updateRecommendedChatFolders () { + tdlib.send(new TdApi.GetRecommendedChatFolders(), (recommendedChatFolders, error) -> { + runOnUiThreadOptional(() -> { + if (recommendedChatFolders != null) { + updateRecommendedChatFolders(recommendedChatFolders.chatFolders); + } + if (!recommendedChatFoldersInitialized) { + recommendedChatFoldersInitialized = true; + executeScheduledAnimation(); + } + }); + }); + } + + private void updateRecommendedChatFolders (TdApi.RecommendedChatFolder[] chatFolders) { + int fromIndex = indexOfRecommendedChatFolderGroup(); + if (fromIndex == RecyclerView.NO_POSITION) + return; + List subList = adapter.getItems().subList(fromIndex, fromIndex + recommendedChatFolderGroupItemCount); + List newList = buildRecommendedChatFolderItemList(chatFolders); + if (subList.isEmpty() && newList.isEmpty()) { + return; + } + recommendedChatFolders = chatFolders; + recommendedChatFolderGroupItemCount = newList.size(); + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(recommendedChatFoldersDiff(subList, newList)); + subList.clear(); + subList.addAll(newList); + diffResult.dispatchUpdatesTo(new AdapterSubListUpdateCallback(adapter, fromIndex)); + } + + private void removeRecommendedChatFolder (TdApi.ChatFolder chatFolder) { + if (recommendedChatFolders == null || recommendedChatFolders.length == 0) + return; + int indexToRemove = -1; + for (int i = 0; i < recommendedChatFolders.length; i++) { + if (chatFolder == recommendedChatFolders[i].folder) { + indexToRemove = i; + break; + } + } + if (indexToRemove != -1) { + TdApi.RecommendedChatFolder[] chatFolders = new TdApi.RecommendedChatFolder[recommendedChatFolders.length - 1]; + if (chatFolders.length > 0) { + ArrayUtils.removeElement(recommendedChatFolders, indexToRemove, chatFolders); + } + updateRecommendedChatFolders(chatFolders); + } + } + + private static DiffUtil.Callback chatFoldersDiff (List oldList, List newList) { + return new ListItemDiffUtilCallback(oldList, newList) { + @Override + public boolean areItemsTheSame (ListItem oldItem, ListItem newItem) { + return oldItem.getViewType() == newItem.getViewType() && + oldItem.getId() == newItem.getId() && + oldItem.getLongId() == newItem.getLongId(); + } + + @Override + public boolean areContentsTheSame (ListItem oldItem, ListItem newItem) { + CharSequence a = oldItem.getString(); + CharSequence b = newItem.getString(); + return ObjectUtils.equals(a, b); + } + }; + } + + private static DiffUtil.Callback recommendedChatFoldersDiff (List oldList, List newList) { + return new ListItemDiffUtilCallback(oldList, newList) { + @Override + public boolean areItemsTheSame (ListItem oldItem, ListItem newItem) { + if (oldItem.getViewType() == newItem.getViewType() && oldItem.getId() == newItem.getId()) { + if (oldItem.getId() == R.id.recommendedChatFolder) { + CharSequence a = oldItem.getString(); + CharSequence b = newItem.getString(); + return ObjectUtils.equals(a, b); + } + return true; + } + return false; + } + + @Override + public boolean areContentsTheSame (ListItem oldItem, ListItem newItem) { + if (oldItem.getId() == R.id.recommendedChatFolder) { + CharSequence a1 = oldItem.getString(); + CharSequence b1 = newItem.getString(); + if (oldItem.getIconResource() != newItem.getIconResource() || + !ObjectUtils.equals(a1, b1)) return false; + String a = oldItem.getStringValue(); + String b = newItem.getStringValue(); + return ObjectUtils.equals(a, b); + } + CharSequence a = oldItem.getString(); + CharSequence b = newItem.getString(); + return ObjectUtils.equals(a, b); + } + }; + } + + private class ItemTouchHelperCallback implements RemoveHelper.ExtendedCallback { + @Override + public boolean isLongPressDragEnabled () { + return false; + } + + @Override + public int makeDragFlags (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + ListItem item = (ListItem) viewHolder.itemView.getTag(); + return isChatFolder(item) ? ItemTouchHelper.UP | ItemTouchHelper.DOWN : 0; + } + + @Override + public boolean canRemove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int position) { + ListItem item = (ListItem) viewHolder.itemView.getTag(); + return isChatFolder(item) && !isMainChatFolder(item) && !isArchiveChatFolder(item); + } + + @Override + public void onRemove (RecyclerView.ViewHolder viewHolder) { + ListItem item = (ListItem) viewHolder.itemView.getTag(); + showRemoveFolderConfirm(item.getIntValue()); + } + + @Override + public boolean onMove (@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder source, @NonNull RecyclerView.ViewHolder target) { + int sourcePosition = source.getAbsoluteAdapterPosition(); + int targetPosition = target.getAbsoluteAdapterPosition(); + if (sourcePosition == RecyclerView.NO_POSITION || targetPosition == RecyclerView.NO_POSITION) { + return false; + } + int firstChatFolderIndex = indexOfFirstChatFolder(); + int lastChatFolderIndex = indexOfLastChatFolder(); + if (firstChatFolderIndex == RecyclerView.NO_POSITION || lastChatFolderIndex == RecyclerView.NO_POSITION) { + return false; + } + if (targetPosition < firstChatFolderIndex || targetPosition > lastChatFolderIndex) { + return false; + } + adapter.moveItem(sourcePosition, targetPosition, /* notify */ true); + return true; + } + + @Override + public boolean canDropOver (@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder source, @NonNull RecyclerView.ViewHolder target) { + ListItem sourceItem = (ListItem) source.itemView.getTag(); + ListItem targetItem = (ListItem) target.itemView.getTag(); + return isChatFolder(sourceItem) && isChatFolder(targetItem) && (canMoveChatFolder(targetItem) || BuildConfig.DEBUG && isArchiveChatFolder(sourceItem)); + } + + @Override + public void onCompleteMovement (int fromPosition, int toPosition) { + reorderChatFolders(); + } + } + + private class ChatFolderOnTouchListener implements View.OnTouchListener { + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch (View view, MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + float paddingStart = ((SettingView) view).getMeasuredNameStart(); + boolean shouldStartDrag = Lang.rtl() ? event.getX() > view.getWidth() - paddingStart : event.getX() < paddingStart; + if (shouldStartDrag) { + startDrag(getRecyclerView().getChildViewHolder(view)); + } + } + return false; + } + } + + private static class FolderBadge implements DrawModifier { + private final Text text; + + public FolderBadge (String badgeText) { + text = new Text.Builder(badgeText, Integer.MAX_VALUE, Paints.robotoStyleProvider(12f), Theme::textDecentColor) + .allBold() + .singleLine() + .noClickable() + .ignoreNewLines() + .ignoreContinuousNewLines() + .noSpacing() + .build(); + } + + @Override + public void afterDraw (View view, Canvas c) { + SettingView settingView = (SettingView) view; + float centerY = view.getHeight() / 2 + Screen.dp(.8f); + int startX = (int) (settingView.getMeasuredNameStart() + settingView.getMeasuredNameWidth()) + Screen.dp(8f) + Screen.dp(6f); + int startY = Math.round(centerY) - text.getLineCenterY(); + float alpha = 0.7f + settingView.getVisuallyEnabledFactor() * 0.3f; + text.draw(c, startX, startY, null, alpha); + + int strokeColor = ColorUtils.alphaColor(alpha, Theme.textDecentColor()); + Paint.FontMetricsInt fontMetrics = Paints.getFontMetricsInt(Paints.getTextPaint16()); + float height = fontMetrics.descent - fontMetrics.ascent - Screen.dp(2f); + + RectF rect = Paints.getRectF(startX - Screen.dp(6f), centerY - height / 2f, startX + text.getWidth() + Screen.dp(6f), centerY + height / 2f); + float radius = Screen.dp(4f); + c.drawRoundRect(rect, radius, radius, Paints.strokeSmallPaint(strokeColor)); + } + + @Override + public int getWidth () { + return Screen.dp(8f) + text.getWidth() + Screen.dp(6f) * 2; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsLanguageController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsLanguageController.java index b19ea4f7f2..0f88bd6b7f 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsLanguageController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsLanguageController.java @@ -298,7 +298,7 @@ private void showRemoveLanguagePrompt (ListItem item) { if (languageInfo == null || languageInfo.isOfficial) return; boolean isCustom = Td.isLocal(languageInfo); - showOptions(Lang.getStringBold(isCustom ? R.string.DeleteLanguageConfirm : R.string.LanguageDeleteConfirm, languageInfo.nativeName, languageInfo.name, TD.getLink(languageInfo)), new int[]{R.id.btn_delete, R.id.btn_cancel}, new String[]{Lang.getString(isCustom ? R.string.RemoveLanguage : R.string.LanguageDelete), Lang.getString(R.string.Cancel)}, new int[]{OPTION_COLOR_RED, OPTION_COLOR_NORMAL}, new int[]{R.drawable.baseline_delete_forever_24, R.drawable.baseline_cancel_24}, (itemView, id) -> { + showOptions(Lang.getStringBold(isCustom ? R.string.DeleteLanguageConfirm : R.string.LanguageDeleteConfirm, languageInfo.nativeName, languageInfo.name, tdlib.tMeLanguageUrl(languageInfo.id)), new int[]{R.id.btn_delete, R.id.btn_cancel}, new String[]{Lang.getString(isCustom ? R.string.RemoveLanguage : R.string.LanguageDelete), Lang.getString(R.string.Cancel)}, new int[]{OPTION_COLOR_RED, OPTION_COLOR_NORMAL}, new int[]{R.drawable.baseline_delete_forever_24, R.drawable.baseline_cancel_24}, (itemView, id) -> { if (id == R.id.btn_delete) { removeLanguage(item, languageInfo); } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsLogOutController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsLogOutController.java index 1d30ab53c4..6577495c20 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsLogOutController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsLogOutController.java @@ -17,19 +17,36 @@ import android.content.Context; import android.view.View; +import androidx.annotation.IdRes; +import androidx.annotation.IntDef; + import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.base.SettingView; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TGFoundChat; import org.thunderdog.challegram.navigation.DoubleHeaderView; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.unsorted.Passcode; import org.thunderdog.challegram.v.CustomRecyclerView; +import org.thunderdog.challegram.widget.BetterChatView; import java.util.ArrayList; import java.util.List; -public class SettingsLogOutController extends RecyclerViewController implements View.OnClickListener { +public class SettingsLogOutController extends RecyclerViewController implements View.OnClickListener { + @IntDef({ + Type.LOG_OUT, Type.DELETE_ACCOUNT + }) + public @interface Type { + int LOG_OUT = 0, DELETE_ACCOUNT = 1; + } + + private @Type int getType () { + return getArguments() == null ? Type.LOG_OUT : getArgumentsStrict(); + } + public SettingsLogOutController (Context context, Tdlib tdlib) { super(context, tdlib); } @@ -41,7 +58,13 @@ public int getId () { @Override public CharSequence getName () { - return Lang.getString(R.string.LogOut); + switch (getType()) { + case Type.LOG_OUT: + return Lang.getString(R.string.LogOut); + case Type.DELETE_ACCOUNT: + return Lang.getString(R.string.DeleteAccount); + } + throw new UnsupportedOperationException(); } private SettingsAdapter adapter; @@ -70,12 +93,25 @@ protected void onCreateView (Context context, CustomRecyclerView recyclerView) { adapter = new SettingsAdapter(this) { @Override protected void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) { - view.setIconColorId(item.getId() == R.id.btn_logout ? ColorId.iconNegative : 0); + @IdRes int itemId = item.getId(); + view.setIconColorId(itemId == R.id.btn_logout || itemId == R.id.btn_deleteAccount ? ColorId.iconNegative : ColorId.NONE); + } + + @Override + protected void setChatData (ListItem item, int position, BetterChatView chatView) { + chatView.setEnabled(false); + chatView.setChat((TGFoundChat) item.getData()); } }; List items = new ArrayList<>(); + @Type int type = getType(); + + TGFoundChat chat = new TGFoundChat(tdlib, tdlib.mySender(), true); + chat.setForcedSubtitle(Strings.formatPhone(tdlib.account().getPhoneNumber())); + items.add(new ListItem(ListItem.TYPE_CHAT_BETTER).setData(chat)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR)); items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_addAccount, R.drawable.baseline_person_add_24, R.string.SignOutAltAddAccount)); items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.SignOutAltAddAccountHint)); @@ -100,12 +136,19 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_help, R.drawable.baseline_help_24, R.string.SignOutAltHelp)); items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); - items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.SignOutAltHelpHint)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, type == Type.DELETE_ACCOUNT ? R.string.DeleteAccountHelpHint : R.string.SignOutAltHelpHint)); items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); - items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_logout, R.drawable.baseline_delete_forever_24, R.string.LogOut).setTextColorId(ColorId.textNegative)); + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_logout, R.drawable.baseline_logout_24, R.string.LogOut).setTextColorId(ColorId.textNegative)); items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); - items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.SignOutAltHint2)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, type == Type.DELETE_ACCOUNT ? R.string.DeleteAccountSignOutAltHint2 : R.string.SignOutAltHint2)); + + if (type == Type.DELETE_ACCOUNT) { + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_deleteAccount, R.drawable.baseline_delete_alert_24, R.string.DeleteAccountBtn).setTextColorId(ColorId.textNegative)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.DeleteAccountInfo)); + } adapter.setItems(items, false); @@ -129,6 +172,8 @@ public void onClick (View v) { tdlib.ui().openSupport(this); } else if (viewId == R.id.btn_logout) { tdlib.ui().logOut(this, false); + } else if (viewId == R.id.btn_deleteAccount) { + tdlib.ui().permanentlyDeleteAccount(this, false); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsNotificationController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsNotificationController.java index e60183114a..f58a227cd6 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsNotificationController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsNotificationController.java @@ -36,13 +36,12 @@ import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.collection.SparseArrayCompat; import androidx.recyclerview.widget.LinearLayoutManager; -import com.google.firebase.FirebaseOptions; - import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; @@ -64,6 +63,7 @@ import org.thunderdog.challegram.telegram.TdlibAccount; import org.thunderdog.challegram.telegram.TdlibNotificationChannelGroup; import org.thunderdog.challegram.telegram.TdlibNotificationManager; +import org.thunderdog.challegram.telegram.TdlibNotificationUtils; import org.thunderdog.challegram.telegram.TdlibOptionListener; import org.thunderdog.challegram.telegram.TdlibSettingsManager; import org.thunderdog.challegram.telegram.TdlibUi; @@ -75,12 +75,15 @@ import org.thunderdog.challegram.util.RingtoneItem; import org.thunderdog.challegram.util.SimpleStringItem; import org.thunderdog.challegram.util.StringList; +import org.thunderdog.challegram.util.TokenRetriever; import org.thunderdog.challegram.util.text.Text; import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.widget.InfiniteRecyclerView; import org.thunderdog.challegram.widget.PopupLayout; import org.thunderdog.challegram.widget.RadioView; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -92,6 +95,7 @@ import me.vkryl.core.StringUtils; import me.vkryl.core.collection.IntList; import me.vkryl.td.ChatId; +import me.vkryl.td.Td; public class SettingsNotificationController extends RecyclerViewController implements View.OnClickListener, View.OnLongClickListener, @@ -102,13 +106,22 @@ public class SettingsNotificationController extends RecyclerViewController getCallRingtones () { @@ -755,6 +788,8 @@ private ListItem newAccountItem (TdlibAccount account) { .setLongValue(account.getKnownUserId()); } + private TdApi.ArchiveChatListSettings archiveChatListSettings; + @Override protected void onCreateView (Context context, CustomRecyclerView recyclerView) { adapter = new SettingsAdapter(this) { @@ -949,7 +984,7 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda } else if (itemId == R.id.btn_silenceNonContacts) { view.getToggler().setRadioEnabled(tdlib.settings().needMuteNonContacts(), isUpdate); } else if (itemId == R.id.btn_archiveMuteNonContacts) { - view.getToggler().setRadioEnabled(tdlib.autoArchiveEnabled(), isUpdate); + view.getToggler().setRadioEnabled(archiveChatListSettings != null && archiveChatListSettings.archiveAndMuteNewChatsFromUnknownUsers, isUpdate); } else if (itemId == R.id.btn_repeatNotifications) { int minutes = tdlib.notifications().getRepeatNotificationMinutes(); if (minutes == 0) { @@ -969,7 +1004,16 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda ArrayList items = new ArrayList<>(); boolean hasOptions = false; - if (customChatId != 0) { + if (specificSection != Section.DEFAULT) { + switch (specificSection) { + case Section.APP_BADGE: + addBadgeCounterItems(items); + break; + case Section.DEFAULT: + default: + throw new IllegalStateException(Integer.toString(specificSection)); + } + } else if (customChatId != 0) { if (hasVibrateAndSound = needVibrateAndSoundSettings()) { items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_customChat_vibrate, 0, R.string.Vibrate)); items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); @@ -1093,6 +1137,11 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda // items.add(new SettingItem(SettingItem.TYPE_HEADER, 0, 0, R.string.NotificationAdvanced)); break; } + case TdApi.NotificationSettingsScopeChannelChats.CONSTRUCTOR: + break; + default: + Td.assertNotificationSettingsScope_edff9c28(); + throw Td.unsupported(scope); } items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); @@ -1154,6 +1203,11 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda items.add(new ListItem(ListItem.TYPE_VALUED_SETTING, R.id.btn_notifications_snooze, R.drawable.baseline_bullhorn_24, R.string.Channels).setData(tdlib.notifications().scopeChannel())); items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_archiveSettings, 0, R.string.ArchiveSettings)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.ArchiveSettingsDesc)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_inApp_chatSounds, 0, R.string.InChatSound)); items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); @@ -1188,11 +1242,17 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.UnknownChats)); - if (tdlib.autoArchiveAvailable() || tdlib.autoArchiveEnabled()) { + if (tdlib.autoArchiveAvailable()) { items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_archiveMuteNonContacts, 0, R.string.ArchiveNonContacts)); items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.ArchiveNonContactsInfo)); + tdlib.send(new TdApi.GetArchiveChatListSettings(), (archiveSettings, error) -> runOnUiThreadOptional(() -> { + if (archiveSettings != null) { + this.archiveChatListSettings = archiveSettings; + adapter.updateValuedSettingById(R.id.btn_archiveMuteNonContacts); + } + })); } items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); @@ -1202,17 +1262,7 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.BadgeCounter)); items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); - items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_appBadgeCountMuted, 0, R.string.BadgeCounterMuted).setIntValue(Settings.BADGE_FLAG_MUTED)); - items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_appBadgeCountArchive, 0, R.string.BadgeCounterArchive).setIntValue(Settings.BADGE_FLAG_ARCHIVED)); - items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_appBadgeCountMessages, 0, R.string.BadgeCounterMessages).setIntValue(Settings.BADGE_FLAG_MESSAGES)); - items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); - items.add(new ListItem(ListItem.TYPE_DESCRIPTION, R.id.btn_appBadgeCountMessagesInfo, 0, (Settings.instance().getBadgeFlags() & Settings.BADGE_FLAG_MESSAGES) != 0 ? R.string.BadgeCounterMessagesOff : R.string.BadgeCounterMessagesOn)); - /*items.add(new SettingItem(SettingItem.TYPE_SHADOW_TOP)); - items.add(new SettingItem(SettingItem.TYPE_RADIO_SETTING, R.id.btn_appBadge, 0, R.string.BadgeCounterSetting)); - items.add(new SettingItem(SettingItem.TYPE_SHADOW_BOTTOM)); - items.add(new SettingItem(SettingItem.TYPE_DESCRIPTION, 0, 0, R.string.AppBadgeHint));*/ + addBadgeCounterItems(items); items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.NotificationAdvanced)); boolean split = Settings.instance().needSplitNotificationCategories(); @@ -1244,6 +1294,20 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda } } + private static void addBadgeCounterItems (List items) { + items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_appBadgeCountMuted, 0, R.string.BadgeCounterMuted).setIntValue(Settings.BADGE_FLAG_MUTED)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_appBadgeCountArchive, 0, R.string.BadgeCounterArchive).setIntValue(Settings.BADGE_FLAG_ARCHIVED)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_appBadgeCountMessages, 0, R.string.BadgeCounterMessages).setIntValue(Settings.BADGE_FLAG_MESSAGES)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, R.id.btn_appBadgeCountMessagesInfo, 0, (Settings.instance().getBadgeFlags() & Settings.BADGE_FLAG_MESSAGES) != 0 ? R.string.BadgeCounterMessagesOff : R.string.BadgeCounterMessagesOn)); + /*items.add(new SettingItem(SettingItem.TYPE_SHADOW_TOP)); + items.add(new SettingItem(SettingItem.TYPE_RADIO_SETTING, R.id.btn_appBadge, 0, R.string.BadgeCounterSetting)); + items.add(new SettingItem(SettingItem.TYPE_SHADOW_BOTTOM)); + items.add(new SettingItem(SettingItem.TYPE_DESCRIPTION, 0, 0, R.string.AppBadgeHint));*/ + } + @Override public void onContactRegisteredNotificationsDisabled (boolean areDisabled) { runOnUiThreadOptional(() -> adapter.updateValuedSettingById(R.id.btn_events_contactJoined), this::isCommonScreen); @@ -1259,6 +1323,14 @@ public void onNotificationGlobalSettingsChanged () { runOnUiThreadOptional(() -> adapter.updateValuedSettingById(R.id.btn_archiveMuteNonContacts), this::isCommonScreen); } + @Override + public void onArchiveChatListSettingsChanged (TdApi.ArchiveChatListSettings settings) { + runOnUiThreadOptional(() -> { + this.archiveChatListSettings = settings; + adapter.updateValuedSettingById(R.id.btn_archiveMuteNonContacts); + }); + } + private void checkContentPreview (boolean showPreview, int afterId, int id) { int i = adapter.indexOfViewById(id); if (showPreview) { @@ -1298,16 +1370,17 @@ private void shareTokenError () { Throwable fullError = tdlib.context().getTokenFullError(); String error = tdlib.context().getTokenError(); if (!StringUtils.isEmpty(error) || fullError != null) { - String report = "#firebase_error"; + TokenRetriever retriever = TdlibNotificationUtils.getTokenRetriever(); + String report = "#" + retriever.getName() + "_error"; if (!StringUtils.isEmpty(error)) { report += " " + error; } report += "\n\n"; - FirebaseOptions firebaseOptions = FirebaseOptions.fromResource(UI.getAppContext()); - if (firebaseOptions != null) { - report += "Firebase options:\n" + firebaseOptions; + String configuration = retriever.getConfiguration(); + if (!StringUtils.isEmpty(configuration)) { + report += "Configuration:\n" + configuration; } else { - report += "Firebase options unavailable!"; + report += "Configuration unavailable!"; } report += "\n" + U.getUsefulMetadata(tdlib); if (fullError != null) { @@ -1499,6 +1572,7 @@ public void onClick (View v) { int flag = item.getIntValue(); int flags = Settings.instance().getBadgeFlags(); int newFlags = BitwiseUtils.setFlag(flags, flag, enabled); + /* No longer needed as unmuted chats may remain archived switch (item.getIntValue()) { case Settings.BADGE_FLAG_MUTED: if (!enabled) { @@ -1510,9 +1584,9 @@ public void onClick (View v) { newFlags = BitwiseUtils.setFlag(newFlags, Settings.BADGE_FLAG_MUTED, true); } break; - } + }*/ if (Settings.instance().setBadgeFlags(newFlags)) { - tdlib.context().resetBadge(); + tdlib.context().resetBadge(true); switch (flag) { case Settings.BADGE_FLAG_MESSAGES: { int i = adapter.indexOfViewById(R.id.btn_appBadgeCountMessagesInfo); @@ -1810,7 +1884,17 @@ public void onClick (View v) { } else if (viewId == R.id.btn_silenceNonContacts) { tdlib.settings().setUserPreference(TdlibSettingsManager.PREFERENCE_MUTE_NON_CONTACTS, adapter.toggleView(v)); } else if (viewId == R.id.btn_archiveMuteNonContacts) { - tdlib.setAutoArchiveEnabled(adapter.toggleView(v)); + if (archiveChatListSettings != null) { + archiveChatListSettings.archiveAndMuteNewChatsFromUnknownUsers = adapter.toggleView(v); + tdlib.send(new TdApi.SetArchiveChatListSettings(archiveChatListSettings), (ok, error) -> { + if (ok != null) { + tdlib.listeners().notifyArchiveChatListSettingsChanged(archiveChatListSettings); + } + }); + } + } else if (viewId == R.id.btn_archiveSettings) { + SettingsArchiveChatListController c = new SettingsArchiveChatListController(context, tdlib); + navigateTo(c); } else if (viewId == R.id.btn_events_pinnedMessages) { boolean disabled = !adapter.toggleView(v); tdlib.notifications().setDefaultDisablePinnedMessages(tdlib.notifications().scopeGroup(), disabled); @@ -1970,7 +2054,8 @@ public void onActivityResult (int requestCode, int resultCode, Intent data) { forcedFileName = tdlib.id() + "_channel.ogg"; break; default: - throw new UnsupportedOperationException(scope.toString()); + Td.assertNotificationSettingsScope_edff9c28(); + throw Td.unsupported(scope); } } Uri uri = TdlibNotificationManager.fixSoundUri(originalUri, true, forcedFileName); @@ -2098,7 +2183,7 @@ private void stopSounds () { } private boolean isCommonScreen () { - return scope == null && customChatId == 0; + return scope == null && customChatId == 0 && specificSection == Section.DEFAULT; } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsPrivacyController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsPrivacyController.java index e519bf2d0b..e6559ded4d 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsPrivacyController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsPrivacyController.java @@ -146,6 +146,7 @@ public void setValuedSetting (ListItem item, SettingView v, boolean isUpdate) { TdApi.UserPrivacySetting[] privacySettings = new TdApi.UserPrivacySetting[] { new TdApi.UserPrivacySettingShowStatus(), new TdApi.UserPrivacySettingShowProfilePhoto(), + new TdApi.UserPrivacySettingShowBio(), new TdApi.UserPrivacySettingShowPhoneNumber(), new TdApi.UserPrivacySettingAllowFindingByPhoneNumber(), new TdApi.UserPrivacySettingShowLinkInForwardedMessages(), @@ -221,22 +222,31 @@ public void setValuedSetting (ListItem item, SettingView v, boolean isUpdate) { items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_accountTTL, 0, R.string.DeleteAccountIfAwayFor2)); items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.DeleteAccountHelp)); + + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_deleteAccount, 0, R.string.DeleteMyAccount).setTextColorId(ColorId.textNegative)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.DeleteMyAccountInfo)); } adapter.setItems(items, false); recyclerView.setAdapter(adapter); - tdlib.client().send(new TdApi.GetBlockedMessageSenders(0, 1), this); + Log.ensureReturnType(TdApi.GetBlockedMessageSenders.class, TdApi.MessageSenders.class); + tdlib.client().send(new TdApi.GetBlockedMessageSenders(new TdApi.BlockListMain(), 0, 1), this); fetchSessions(); + Log.ensureReturnType(TdApi.GetPasswordState.class, TdApi.PasswordState.class); tdlib.client().send(new TdApi.GetPasswordState(), this); + Log.ensureReturnType(TdApi.GetAccountTtl.class, TdApi.AccountTtl.class); tdlib.client().send(new TdApi.GetAccountTtl(), this); + Log.ensureReturnType(TdApi.GetConnectedWebsites.class, TdApi.ConnectedWebsites.class); tdlib.client().send(new TdApi.GetConnectedWebsites(), this); tdlib.cache().putGlobalUserDataListener(this); tdlib.contacts().addStatusListener(this); - tdlib.listeners().subscribeForAnyUpdates(this); + tdlib.listeners().subscribeForGlobalUpdates(this); } @Override @@ -267,7 +277,7 @@ public void destroy () { super.destroy(); tdlib.cache().deleteGlobalUserDataListener(this); tdlib.contacts().removeStatusListener(this); - tdlib.listeners().unsubscribeFromAnyUpdates(this); + tdlib.listeners().unsubscribeFromGlobalUpdates(this); } @Override @@ -290,18 +300,11 @@ public void onContactSyncEnabled (Tdlib tdlib, boolean isEnabled) { } private void getPrivacy (final TdApi.UserPrivacySetting setting) { - tdlib.client().send(new TdApi.GetUserPrivacySettingRules(setting), object -> tdlib.ui().post(() -> { - if (!isDestroyed()) { - switch (object.getConstructor()) { - case TdApi.UserPrivacySettingRules.CONSTRUCTOR: { - setPrivacyRules(setting.getConstructor(), (TdApi.UserPrivacySettingRules) object); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; - } - } + tdlib.send(new TdApi.GetUserPrivacySettingRules(setting), (rules, error) -> runOnUiThreadOptional(() -> { + if (error != null) { + UI.showError(error); + } else { + setPrivacyRules(setting.getConstructor(), rules); } })); } @@ -407,7 +410,7 @@ public void onClick (View v) { }); } else if (id == R.id.btn_blockedSenders) { SettingsBlockedController c = new SettingsBlockedController(context, tdlib); - c.setArguments(this); + c.setArguments(new TdApi.BlockListMain()); navigateTo(c); } else if (id == R.id.btn_privacyRule) { TdApi.UserPrivacySetting setting = (TdApi.UserPrivacySetting) ((ListItem) v.getTag()).getData(); @@ -456,6 +459,8 @@ public void onClick (View v) { new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_6month, 0, Lang.pluralBold(R.string.xMonths, 6), R.id.btn_accountTTL, months == 6), new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_1year, 0, Lang.pluralBold(R.string.xYears, 1), R.id.btn_accountTTL, years == 1) }, this); + } else if (id == R.id.btn_deleteAccount) { + tdlib.ui().permanentlyDeleteAccount(this, true); } else if (id == R.id.btn_clearAllDrafts) { showOptions(Lang.getString(R.string.AreYouSureClearDrafts), new int[] {R.id.btn_clearAllDrafts, R.id.btn_cancel}, new String[] {Lang.getString(R.string.PrivacyDeleteCloudDrafts), Lang.getString(R.string.Cancel)}, new int[] {OPTION_COLOR_RED, OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_forever_24, R.drawable.baseline_cancel_24}, (itemView, actionId) -> { if (actionId == R.id.btn_clearAllDrafts) { @@ -524,10 +529,10 @@ public void diffBlockList (int delta) { } @Override - public void onChatBlocked (long chatId, boolean isBlocked) { + public void onChatBlockListChanged (long chatId, @Nullable TdApi.BlockList blockList) { runOnUiThread(() -> { if (!isDestroyed()) { - tdlib.client().send(new TdApi.GetBlockedMessageSenders(0, 1), SettingsPrivacyController.this); + tdlib.client().send(new TdApi.GetBlockedMessageSenders(new TdApi.BlockListMain(), 0, 1), SettingsPrivacyController.this); } }, 350l); } @@ -730,8 +735,7 @@ public void onResult (final TdApi.Object object) { break; } default: { - Log.unexpectedTdlibResponse(object, TdApi.GetUser.class, TdApi.Users.class); - break; + throw new UnsupportedOperationException(object.toString()); } } }); diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsPrivacyKeyController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsPrivacyKeyController.java index 9e7d2c5adc..f1ea711f1e 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsPrivacyKeyController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsPrivacyKeyController.java @@ -15,6 +15,7 @@ package org.thunderdog.challegram.ui; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.text.Spannable; import android.text.SpannableStringBuilder; @@ -30,14 +31,17 @@ import org.thunderdog.challegram.component.dialogs.SearchManager; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TGUser; +import org.thunderdog.challegram.navigation.ActivityResultHandler; import org.thunderdog.challegram.telegram.PrivacySettings; import org.thunderdog.challegram.telegram.PrivacySettingsListener; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibCache; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.Fonts; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.util.ProfilePhotoDrawModifier; import org.thunderdog.challegram.util.CustomTypefaceSpan; import org.thunderdog.challegram.util.NoUnderlineClickableSpan; import org.thunderdog.challegram.util.UserPickerMultiDelegate; @@ -51,7 +55,8 @@ import me.vkryl.td.ChatId; import me.vkryl.td.Td; -public class SettingsPrivacyKeyController extends RecyclerViewController implements View.OnClickListener, UserPickerMultiDelegate, PrivacySettingsListener { +public class SettingsPrivacyKeyController extends RecyclerViewController implements View.OnClickListener, UserPickerMultiDelegate, PrivacySettingsListener, ActivityResultHandler, + TdlibCache.UserDataChangeListener { public SettingsPrivacyKeyController (Context context, Tdlib tdlib) { super(context, tdlib); @@ -71,6 +76,8 @@ public static int getIcon (TdApi.UserPrivacySetting privacySetting) { switch (privacySetting.getConstructor()) { case TdApi.UserPrivacySettingShowPhoneNumber.CONSTRUCTOR: return R.drawable.baseline_call_24; + case TdApi.UserPrivacySettingShowBio.CONSTRUCTOR: + return R.drawable.baseline_info_24; case TdApi.UserPrivacySettingAllowFindingByPhoneNumber.CONSTRUCTOR: return R.drawable.baseline_search_24; case TdApi.UserPrivacySettingShowStatus.CONSTRUCTOR: @@ -87,8 +94,10 @@ public static int getIcon (TdApi.UserPrivacySetting privacySetting) { return R.drawable.baseline_swap_horiz_24; case TdApi.UserPrivacySettingAllowPrivateVoiceAndVideoNoteMessages.CONSTRUCTOR: return R.drawable.baseline_mic_24; + default: + Td.assertUserPrivacySetting_21d3f4(); + throw Td.unsupported(privacySetting); } - return 0; } public static int getName (TdApi.UserPrivacySetting privacyKey, boolean isException, boolean isMultiChatException) { @@ -97,6 +106,8 @@ public static int getName (TdApi.UserPrivacySetting privacyKey, boolean isExcept return isException ? R.string.EditPrivacyPhoneNumber : R.string.PhoneNumber; case TdApi.UserPrivacySettingAllowFindingByPhoneNumber.CONSTRUCTOR: return R.string.FindingByPhoneNumber; + case TdApi.UserPrivacySettingShowBio.CONSTRUCTOR: + return isException ? R.string.EditPrivacyBio : R.string.UserBio; case TdApi.UserPrivacySettingAllowChatInvites.CONSTRUCTOR: return isException ? (isMultiChatException ? R.string.EditPrivacyChatInviteGroup : R.string.EditPrivacyChatInvite) : R.string.GroupsAndChannels; case TdApi.UserPrivacySettingShowStatus.CONSTRUCTOR: @@ -111,8 +122,10 @@ public static int getName (TdApi.UserPrivacySetting privacyKey, boolean isExcept return isException ? R.string.EditPrivacyPhoto : R.string.PrivacyPhotoTitle; case TdApi.UserPrivacySettingAllowPrivateVoiceAndVideoNoteMessages.CONSTRUCTOR: return isException ? R.string.EditPrivacyVoice : R.string.PrivacyVoiceVideoTitle; + default: + Td.assertUserPrivacySetting_21d3f4(); + throw Td.unsupported(privacyKey); } - throw new IllegalStateException("privacyKey == " + privacyKey); } @Override @@ -181,8 +194,9 @@ private void setPrivacyRules (TdApi.UserPrivacySettingRules rules) { } private boolean needNobodyOption () { + //noinspection SwitchIntDef switch (getArgumentsStrict().getConstructor()) { - case TdApi.UserPrivacySettingAllowChatInvites.CONSTRUCTOR: + // case TdApi.UserPrivacySettingAllowChatInvites.CONSTRUCTOR: case TdApi.UserPrivacySettingAllowFindingByPhoneNumber.CONSTRUCTOR: // case TdApi.UserPrivacySettingShowProfilePhoto.CONSTRUCTOR: return false; @@ -191,6 +205,7 @@ private boolean needNobodyOption () { } private boolean needExceptions () { + //noinspection SwitchIntDef switch (getArgumentsStrict().getConstructor()) { case TdApi.UserPrivacySettingAllowFindingByPhoneNumber.CONSTRUCTOR: return false; @@ -238,6 +253,11 @@ private void buildCells () { hintItem = new ListItem(ListItem.TYPE_DESCRIPTION, R.id.btn_description, 0, R.string.WhoCanSeePhoneInfo); break; } + case TdApi.UserPrivacySettingShowBio.CONSTRUCTOR: { + headerItem = new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.WhoCanSeeBio); + hintItem = new ListItem(ListItem.TYPE_DESCRIPTION, R.id.btn_description, 0, R.string.WhoCanSeeBioInfo); + break; + } case TdApi.UserPrivacySettingAllowFindingByPhoneNumber.CONSTRUCTOR: { headerItem = new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.WhoCanFindByPhone); hintItem = new ListItem(ListItem.TYPE_DESCRIPTION, R.id.btn_description, 0, rulesType == PrivacySettings.MODE_EVERYBODY ? R.string.WhoCanFindByPhoneInfoEveryone : R.string.WhoCanFindByPhoneInfoContacts); @@ -272,7 +292,8 @@ private void buildCells () { break; } default: { - throw new IllegalStateException("privacyKey == " + getArgumentsStrict()); + Td.assertUserPrivacySetting_21d3f4(); + throw Td.unsupported(getArgumentsStrict()); } } @@ -326,6 +347,13 @@ private void buildCells () { items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.CustomShareSettingsHelp)); } + if (getArgumentsStrict().getConstructor() == TdApi.UserPrivacySettingShowProfilePhoto.CONSTRUCTOR) { + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_setProfilePhoto, 0, R.string.PublicPhoto)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.PublicPhotoHint)); + } + /*if (privacyKey.getConstructor() == TdApi.UserPrivacySettingAllowCalls.CONSTRUCTOR) { items.add(new SettingItem(SettingItem.TYPE_HEADER, 0, 0, R.string.PrivacyCallsP2PTitle)); items.add(new SettingItem(SettingItem.TYPE_SHADOW_TOP)); @@ -349,6 +377,7 @@ private void buildCells () { private void updateHints () { int mode = currentRules().getMode(); + //noinspection SwitchIntDef switch (getArgumentsStrict().getConstructor()) { case TdApi.UserPrivacySettingAllowFindingByPhoneNumber.CONSTRUCTOR: { int i = adapter.indexOfViewById(R.id.btn_description); @@ -439,6 +468,8 @@ public void onPrivacySettingRulesChanged (TdApi.UserPrivacySetting setting, TdAp }); } + private long subscribedToUserId; + @Override protected void onCreateView (Context context, CustomRecyclerView recyclerView) { adapter = new SettingsAdapter(this) { @@ -452,6 +483,16 @@ protected void setValuedSetting (ListItem item, SettingView view, boolean isUpda int count = currentRules().getMinusTotalCount(tdlib); view.setData(count > 0 ? Lang.plural(R.string.xUsers, count) : Lang.getString(R.string.PrivacyAddUsers)); } + + if (itemId == R.id.btn_setProfilePhoto) { + final TdApi.UserFullInfo myUserFull = tdlib.myUserFull(); + final boolean hasAvatar = myUserFull != null && myUserFull.publicPhoto != null; + + view.setData(Lang.getString(hasAvatar ? R.string.PublicPhotoSet : R.string.PublicPhotoNoSet)); + view.setDrawModifier(new ProfilePhotoDrawModifier().requestFiles(view.getComplexReceiver(), tdlib)); + } else { + view.setDrawModifier(null); + } } @Override @@ -476,9 +517,21 @@ protected void modifyHeaderTextView (TextView textView, int viewHeight, int padd } } })); + + subscribedToUserId = tdlib.myUserId(); + tdlib.cache().addUserDataListener(subscribedToUserId, this); tdlib.listeners().subscribeToPrivacyUpdates(this); } + @Override + public void onUserFullUpdated (long userId, TdApi.UserFullInfo userFull) { + UI.post(() -> { + if (userId == tdlib.myUserId() && !isDestroyed()) { + adapter.updateValuedSettingById(R.id.btn_setProfilePhoto); + } + }); + } + @Override public void onBlur () { super.onBlur(); @@ -502,6 +555,7 @@ private void saveChanges () { public void destroy () { super.destroy(); tdlib.listeners().unsubscribeFromPrivacyUpdates(this); + tdlib.cache().removeUserDataListener(subscribedToUserId, this); } private int userPickMode; @@ -583,6 +637,22 @@ public void onClick (View v) { changedPrivacyRules = PrivacySettings.valueOf(currentRules().toggleGlobal(desiredMode)); updateRulesState(changedPrivacyRules); } + } else if (viewId == R.id.btn_setProfilePhoto) { + getAvatarPickerManager().showMenuForProfile(null, true); + } + } + + @Override + public void onActivityResult (int requestCode, int resultCode, Intent data) { + getAvatarPickerManager().handleActivityResult(requestCode, resultCode, data, TdlibUi.AvatarPickerManager.MODE_PROFILE_PUBLIC, null, null); + } + + private TdlibUi.AvatarPickerManager avatarPickerManager; + + private TdlibUi.AvatarPickerManager getAvatarPickerManager () { + if (avatarPickerManager == null) { + avatarPickerManager = new TdlibUi.AvatarPickerManager(this); } + return avatarPickerManager; } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsSessionsController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsSessionsController.java index 30c8f406be..9cfcc806cc 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsSessionsController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsSessionsController.java @@ -34,6 +34,7 @@ import org.thunderdog.challegram.telegram.SessionListener; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; @@ -240,8 +241,8 @@ private static CharSequence getSubtext (TdApi.Session session, boolean isFull) { if (isFull) { b.append('\n').append(Lang.getString(R.string.SessionLastActiveDate, versionCreator, Lang.getTimestamp(session.lastActiveDate, TimeUnit.SECONDS))); - if (!StringUtils.isEmpty(session.ip) || !StringUtils.isEmpty(session.country)) { - b.append('\n').append(Strings.concatIpLocation(Lang.codify(session.ip), session.country)); + if (!StringUtils.isEmpty(session.ipAddress) || !StringUtils.isEmpty(session.location)) { + b.append('\n').append(Strings.concatIpLocation(Lang.codify(session.ipAddress), session.location)); } } return b; @@ -255,7 +256,7 @@ public void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) if (item.getViewType() == ListItem.TYPE_VALUED_SETTING_COMPACT) { view.forcePadding(Screen.dp(63f), 0); } - int iconColorId = item.getTextColorId(ColorId.NONE); + @PorterDuffColorId int iconColorId = item.getTextColorId(ColorId.NONE); if (iconColorId == ColorId.textNegative) { iconColorId = ColorId.iconNegative; } @@ -278,8 +279,8 @@ protected void setSession (ListItem item, int position, RelativeLayout parent, b timeView.setText(""); titleView.setText(getTitle(sessions.currentSession)); subtextView.setText(getAppName(sessions.currentSession)); - if (!StringUtils.isEmpty(sessions.currentSession.ip) || !StringUtils.isEmpty(sessions.currentSession.country)) { - locationView.setText(Strings.concatIpLocation(sessions.currentSession.ip, sessions.currentSession.country)); + if (!StringUtils.isEmpty(sessions.currentSession.ipAddress) || !StringUtils.isEmpty(sessions.currentSession.location)) { + locationView.setText(Strings.concatIpLocation(sessions.currentSession.ipAddress, sessions.currentSession.location)); } else { locationView.setText(Lang.getString(R.string.SessionUnknown)); } @@ -298,8 +299,8 @@ protected void setSession (ListItem item, int position, RelativeLayout parent, b timeView.setText(date); titleView.setText(getTitle(session)); subtextView.setText(getAppName(session)); - if (!StringUtils.isEmpty(session.ip) || !StringUtils.isEmpty(session.country)) { - locationView.setText(Strings.concatIpLocation(session.ip, session.country)); + if (!StringUtils.isEmpty(session.ipAddress) || !StringUtils.isEmpty(session.location)) { + locationView.setText(Strings.concatIpLocation(session.ipAddress, session.location)); } else { locationView.setText(Lang.getString(R.string.SessionUnknown)); } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsStickersAndEmojiController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsStickersAndEmojiController.java new file mode 100644 index 0000000000..9aa9b97942 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsStickersAndEmojiController.java @@ -0,0 +1,412 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 18/08/2023 + */ +package org.thunderdog.challegram.ui; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.IdRes; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.BuildConfig; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.base.SettingView; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGReaction; +import org.thunderdog.challegram.data.TGStickerSetInfo; +import org.thunderdog.challegram.navigation.SettingsWrapBuilder; +import org.thunderdog.challegram.telegram.StickersListener; +import org.thunderdog.challegram.telegram.TGLegacyManager; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.unsorted.Settings; +import org.thunderdog.challegram.util.EmojiModifier; +import org.thunderdog.challegram.util.ReactionModifier; +import org.thunderdog.challegram.v.CustomRecyclerView; + +import java.util.ArrayList; +import java.util.List; + +public class SettingsStickersAndEmojiController extends RecyclerViewController implements View.OnClickListener, StickersListener, SettingsController.StickerSetLoadListener, TGLegacyManager.EmojiLoadListener { + private SettingsAdapter adapter; + private int stickerSetsCount = -1; + private int emojiPacksCount = -1; + + public SettingsStickersAndEmojiController (Context context, Tdlib tdlib) { + super(context, tdlib); + } + + @Override + protected void onCreateView (Context context, CustomRecyclerView recyclerView) { + adapter = new SettingsAdapter(this) { + @Override + protected void setValuedSetting (ListItem item, SettingView v, boolean isUpdate) { + v.setDrawModifier(item.getDrawModifier()); + + final int itemId = item.getId(); + if (itemId == R.id.btn_quick_reaction) { + final String[] reactions = Settings.instance().getQuickReactions(tdlib); + tdlib.ensureReactionsAvailable(reactions, reactionsUpdated -> { + if (reactionsUpdated) { + runOnUiThreadOptional(() -> updateQuickReaction()); + } + }); + StringBuilder stringBuilder = new StringBuilder(); + if (reactions.length > 0) { + final List tgReactions = new ArrayList<>(reactions.length); + for (String reactionKey : reactions) { + TdApi.ReactionType reactionType = TD.toReactionType(reactionKey); + final TGReaction tgReaction = tdlib.getReaction(reactionType, false); + if (tgReaction != null) { + tgReactions.add(tgReaction); + if (stringBuilder.length() > 0) { + stringBuilder.append(Lang.getConcatSeparator()); + } + stringBuilder.append(tgReaction.getTitle()); + } + } + v.setDrawModifier(new ReactionModifier(tgReactions.toArray(new TGReaction[0])).requestFiles(v.getComplexReceiver())); + v.setData(stringBuilder.toString()); + } else { + v.setDrawModifier(null); + v.setData(R.string.QuickReactionDisabled); + } + } else if (itemId == R.id.btn_big_reactions) { + StringBuilder b = new StringBuilder(); + if (Settings.instance().getBigReactionsInChats()) { + b.append(Lang.getString(R.string.BigReactionsChats)); + } + if (Settings.instance().getBigReactionsInChannels()) { + if (b.length() > 0) { + b.append(Lang.getConcatSeparator()); + } + b.append(Lang.getString(R.string.BigReactionsChannels)); + } + if (b.length() == 0) { + b.append(Lang.getString(R.string.BigReactionsNone)); + } + v.setData(b.toString()); + } else if (itemId == R.id.btn_emoji) { + Settings.EmojiPack emojiPack = Settings.instance().getEmojiPack(); + if (emojiPack.identifier.equals(BuildConfig.EMOJI_BUILTIN_ID)) { + v.setData(R.string.EmojiBuiltIn); + } else { + v.setData(emojiPack.displayName); + } + } else if (itemId == R.id.btn_useBigEmoji) { + v.getToggler().setRadioEnabled(Settings.instance().useBigEmoji(), isUpdate); + } else if (itemId == R.id.btn_toggleNewSetting) { + boolean value = Settings.instance().getNewSetting(item.getLongId()); + if (item.getBoolValue()) + value = !value; + v.getToggler().setRadioEnabled(value, isUpdate); + } else if (itemId == R.id.btn_animatedEmojiSettings) { + if (emojiPacksCount != -1) { + v.setData(Lang.plural(R.string.xEmojiSetsInstalled, emojiPacksCount)); + } else { + v.setData(Lang.getString(R.string.xEmojiSetsInstalledUnknown)); + } + } else if (itemId == R.id.btn_stickerSettings) { + if (stickerSetsCount != -1) { + v.setData(Lang.plural(R.string.xStickerSetsInstalled, stickerSetsCount)); + } else { + v.setData(Lang.getString(R.string.xStickerSetsInstalledUnknown)); + } + } else if (itemId == R.id.btn_stickerSuggestions) { + switch (Settings.instance().getStickerMode()) { + case Settings.STICKER_MODE_ALL: + v.setData(R.string.SuggestStickersAll); + break; + case Settings.STICKER_MODE_ONLY_INSTALLED: + v.setData(R.string.SuggestStickersInstalled); + break; + case Settings.STICKER_MODE_NONE: + v.setData(R.string.SuggestStickersNone); + break; + } + } else if (itemId == R.id.btn_avatarsInReactions) { + switch (Settings.instance().getReactionAvatarsMode()) { + case Settings.REACTION_AVATARS_MODE_ALWAYS: + v.setData(R.string.AvatarsInReactionsAlways); + break; + case Settings.REACTION_AVATARS_MODE_SMART_FILTER: + v.setData(R.string.AvatarsInReactionsSmartFilter); + break; + case Settings.REACTION_AVATARS_MODE_NEVER: + v.setData(R.string.AvatarsInReactionsNever); + break; + } + } else if (itemId == R.id.btn_emojiSuggestions) { + switch (Settings.instance().getEmojiMode()) { + case Settings.STICKER_MODE_ALL: + v.setData(R.string.SuggestStickersAll); + break; + case Settings.STICKER_MODE_ONLY_INSTALLED: + v.setData(R.string.SuggestStickersInstalled); + break; + case Settings.STICKER_MODE_NONE: + v.setData(R.string.SuggestStickersNone); + break; + } + } + } + }; + recyclerView.setAdapter(adapter); + + List items = new ArrayList<>(); + items.add(new ListItem(ListItem.TYPE_EMPTY_OFFSET_SMALL)); + items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.Reactions)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_quick_reaction, 0, R.string.QuickReaction)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_big_reactions, 0, R.string.BigReactions)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_avatarsInReactions, 0, R.string.AvatarsInReactions)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.EmojiHeader)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_animatedEmojiSettings, R.drawable.baseline_emoticon_outline_24, R.string.EmojiPacks)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_emoji, 0, R.string.Emoji).setDrawModifier(new EmojiModifier(Lang.getString(R.string.EmojiPreview), Paints.emojiPaint()))); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_emojiSuggestions, 0, R.string.SuggestAnimatedEmoji)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_toggleNewSetting, 0, R.string.AnimatedEmoji).setLongId(Settings.SETTING_FLAG_NO_ANIMATED_EMOJI).setBoolValue(true)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_useBigEmoji, 0, R.string.BigEmoji)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.Stickers)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_stickerSettings, R.drawable.deproko_baseline_insert_sticker_24, R.string.StickerPacks)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_stickerSuggestions, 0, R.string.SuggestStickers)); + items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); + items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_toggleNewSetting, 0, R.string.LoopAnimatedStickers).setLongId(Settings.SETTING_FLAG_NO_ANIMATED_STICKERS_LOOP).setBoolValue(true)); + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + TGLegacyManager.instance().addEmojiListener(this); + + adapter.setItems(items, true); + tdlib.listeners().subscribeToStickerUpdates(this); + } + + public void updateSelectedEmoji () { + if (adapter != null) + adapter.updateValuedSettingById(R.id.btn_emoji); + } + + public void updateQuickReaction () { + if (adapter != null) + adapter.updateValuedSettingById(R.id.btn_quick_reaction); + } + + private void invalidateById (int id) { + if (adapter != null) { + int index = adapter.indexOfViewById(id); + View view = getRecyclerView().getLayoutManager().findViewByPosition(index); + if (view != null) + view.invalidate(); + } + } + + @Override + public void onEmojiUpdated (boolean isPackSwitch) { + if (isPackSwitch) { + updateSelectedEmoji(); + } else { + invalidateById(R.id.btn_emoji); + } + } + + @Override + public void onClick (View v) { + final int viewId = v.getId(); + if (viewId == R.id.btn_quick_reaction) { + EditEnabledReactionsController c = new EditEnabledReactionsController(context, tdlib); + c.setArguments(new EditEnabledReactionsController.Args(null, EditEnabledReactionsController.TYPE_QUICK_REACTION)); + navigateTo(c); + } else if (viewId == R.id.btn_big_reactions) { + showSettings(R.id.btn_big_reactions, new ListItem[] { + new ListItem(ListItem.TYPE_INFO, 0, 0, R.string.BigReactionsInfo), + new ListItem(ListItem.TYPE_CHECKBOX_OPTION, R.id.btn_bigReactionsChats, 0, R.string.BigReactionsChats, R.id.btn_bigReactionsChats, Settings.instance().getBigReactionsInChats()), + new ListItem(ListItem.TYPE_CHECKBOX_OPTION, R.id.btn_bigReactionsChannels, 0, R.string.BigReactionsChannels, R.id.btn_bigReactionsChannels, Settings.instance().getBigReactionsInChannels()) + }, (id, result) -> { + Settings.instance().setBigReactionsInChannels(result.get(R.id.btn_bigReactionsChannels) == R.id.btn_bigReactionsChannels); + Settings.instance().setBigReactionsInChats(result.get(R.id.btn_bigReactionsChats) == R.id.btn_bigReactionsChats); + adapter.updateValuedSettingById(R.id.btn_big_reactions); + }); + } else if (viewId == R.id.btn_emoji) { + SettingsCloudEmojiController c = new SettingsCloudEmojiController(context, tdlib); + c.setArguments(new SettingsCloudController.Args<>(this)); + navigateTo(c); + } else if (viewId == R.id.btn_useBigEmoji) { + Settings.instance().setUseBigEmoji(adapter.toggleView(v)); + } else if (viewId == R.id.btn_toggleNewSetting) { + ListItem item = (ListItem) v.getTag(); + boolean value = adapter.toggleView(v); + if (item.getBoolValue()) + value = !value; + Settings.instance().setNewSetting(item.getLongId(), value); + if (value && item.getLongId() == Settings.SETTING_FLAG_DOWNLOAD_BETAS) { + context().appUpdater().checkForUpdates(); + } + } else if (viewId == R.id.btn_stickerSettings) { + SettingsStickersController c = new SettingsStickersController(context, tdlib, SettingsStickersController.TYPE_STICKER); + c.setArguments(getArguments()); + navigateTo(c); + } else if (viewId == R.id.btn_animatedEmojiSettings) { + SettingsStickersController c = new SettingsStickersController(context, tdlib, SettingsStickersController.TYPE_EMOJI); + c.setArguments(getArguments()); + navigateTo(c); + } else if (viewId == R.id.btn_stickerSuggestions) { + showStickerOptions(false); + } else if (viewId == R.id.btn_emojiSuggestions) { + showStickerOptions(true); + } else if (viewId == R.id.btn_avatarsInReactions) { + showReactionAvatarsOptions(); + } + } + + private void showReactionAvatarsOptions () { + final int reactionAvatarsMode = Settings.instance().getReactionAvatarsMode(); + showSettings(new SettingsWrapBuilder(R.id.btn_avatarsInReactions).setRawItems(new ListItem[]{ + new ListItem(ListItem.TYPE_INFO, 0, 0, R.string.ReactionAvatarsInfo), + new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_avatarsInReactionsAlways, 0, R.string.AvatarsInReactionsAlways, R.id.btn_avatarsInReactions, reactionAvatarsMode == Settings.REACTION_AVATARS_MODE_ALWAYS), + new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_avatarsInReactionsSmartFilter, 0, R.string.AvatarsInReactionsSmartFilter, R.id.btn_avatarsInReactions, reactionAvatarsMode == Settings.REACTION_AVATARS_MODE_SMART_FILTER), + new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_avatarsInReactionsNever, 0, R.string.AvatarsInReactionsNever, R.id.btn_avatarsInReactions, reactionAvatarsMode == Settings.REACTION_AVATARS_MODE_NEVER), + }).setIntDelegate((id, result) -> { + int newReactionAvatarsMode = Settings.instance().getReactionAvatarsMode(); + int stickerResultId = result.get(R.id.btn_avatarsInReactions); + if (stickerResultId == R.id.btn_avatarsInReactionsAlways) { + newReactionAvatarsMode = Settings.REACTION_AVATARS_MODE_ALWAYS; + } else if (stickerResultId == R.id.btn_avatarsInReactionsSmartFilter) { + newReactionAvatarsMode = Settings.REACTION_AVATARS_MODE_SMART_FILTER; + } else if (stickerResultId == R.id.btn_avatarsInReactionsNever) { + newReactionAvatarsMode = Settings.REACTION_AVATARS_MODE_NEVER; + } + + Settings.instance().setReactionAvatarsMode(newReactionAvatarsMode); + adapter.updateValuedSettingById(R.id.btn_avatarsInReactions); + }).setAllowResize(false)); + } + + private void showStickerOptions (boolean isEmoji) { + @IdRes int btnId = isEmoji ? R.id.btn_emojiSuggestions : R.id.btn_stickerSuggestions; + final int stickerOption = isEmoji ? + Settings.instance().getEmojiMode(): + Settings.instance().getStickerMode(); + + showSettings(new SettingsWrapBuilder(btnId).setRawItems(new ListItem[]{ + new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_stickerOrEmojiSuggestionsAll, 0, R.string.SuggestStickersAll, btnId, stickerOption == Settings.STICKER_MODE_ALL), + new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_stickerOrEmojiSuggestionsInstalled, 0, R.string.SuggestStickersInstalled, btnId, stickerOption == Settings.STICKER_MODE_ONLY_INSTALLED), + new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_stickerOrEmojiSuggestionsNone, 0, R.string.SuggestStickersNone, btnId, stickerOption == Settings.STICKER_MODE_NONE), + }).setIntDelegate((id, result) -> { + int newStickerMode = Settings.instance().getStickerMode(); + int stickerResultId = result.get(btnId); + if (stickerResultId == R.id.btn_stickerOrEmojiSuggestionsAll) { + newStickerMode = Settings.STICKER_MODE_ALL; + } else if (stickerResultId == R.id.btn_stickerOrEmojiSuggestionsInstalled) { + newStickerMode = Settings.STICKER_MODE_ONLY_INSTALLED; + } else if (stickerResultId == R.id.btn_stickerOrEmojiSuggestionsNone) { + newStickerMode = Settings.STICKER_MODE_NONE; + } + if (isEmoji) { + Settings.instance().setEmojiMode(newStickerMode); + } else { + Settings.instance().setStickerMode(newStickerMode); + } + adapter.updateValuedSettingById(btnId); + }).setAllowResize(false)); //.setHeaderItem(new SettingItem(SettingItem.TYPE_INFO, 0, 0, UI.getString(R.string.MarkdownHint), false)) + } + + @Override + public void onPrepareToShow () { + super.onPrepareToShow(); + updateQuickReaction(); + } + + @Override + public int getId () { + return R.id.controller_stickersAndEmoji; + } + + @Override + public CharSequence getName () { + return Lang.getString(R.string.StickersAndEmoji); + } + + @Override + public void setArguments (SettingsController args) { + super.setArguments(args); + + stickerSetsCount = args.getStickerSetsCount(false); + if (stickerSetsCount == -1) { + args.addStickerSetListener(false, this); + } + + emojiPacksCount = args.getStickerSetsCount(true); + if (emojiPacksCount == -1) { + args.addStickerSetListener(true, this); + } + } + + @Override + public void onStickerSetsLoaded (ArrayList stickerSets, TdApi.StickerType type) { + boolean isEmoji = type.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR; + boolean isStickers = type.getConstructor() == TdApi.StickerTypeRegular.CONSTRUCTOR; + if (isEmoji) { + updateStickerSetsCount(true, stickerSets.size()); + } else if (isStickers) { + updateStickerSetsCount(false, stickerSets.size()); + } + } + + @Override + public void onInstalledStickerSetsUpdated (long[] stickerSetIds, TdApi.StickerType stickerType) { + boolean isEmoji = stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR; + boolean isStickers = stickerType.getConstructor() == TdApi.StickerTypeRegular.CONSTRUCTOR; + if (isEmoji) { + updateStickerSetsCount(true, stickerSetIds.length); + } else if (isStickers) { + updateStickerSetsCount(false, stickerSetIds.length); + } + } + + private void updateStickerSetsCount (boolean isEmoji, int size) { + if (getArguments() != null) { + getArguments().removeStickerSetListener(isEmoji, this); + } + if (isEmoji) { + emojiPacksCount = size; + } else { + stickerSetsCount = size; + } + + if (adapter != null) { + adapter.updateValuedSettingById(isEmoji ? R.id.btn_animatedEmojiSettings : R.id.btn_stickerSettings); + } + } + + @Override + public void destroy () { + super.destroy(); + TGLegacyManager.instance().removeEmojiListener(this); + tdlib.listeners().unsubscribeFromStickerUpdates(this); + if (getArguments() != null) { + getArguments().removeStickerSetListener(true, this); + getArguments().removeStickerSetListener(false, this); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsStickersController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsStickersController.java index 635da5eb49..03b9403744 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsStickersController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsStickersController.java @@ -15,23 +15,41 @@ package org.thunderdog.challegram.ui; import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TGStickerSetInfo; import org.thunderdog.challegram.navigation.BackHeaderButton; +import org.thunderdog.challegram.navigation.HeaderView; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.navigation.ViewPagerController; +import org.thunderdog.challegram.navigation.ViewPagerHeaderViewCompact; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.v.HeaderEditText; import org.thunderdog.challegram.widget.ViewPager; import java.util.ArrayList; import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.StringUtils; public class SettingsStickersController extends ViewPagerController implements SettingsController.StickerSetLoadListener { - public SettingsStickersController (Context context, Tdlib tdlib) { + public static final int TYPE_STICKER = 0; + public static final int TYPE_EMOJI = 1; + + private final int type; + + public SettingsStickersController (Context context, Tdlib tdlib, int type) { super(context, tdlib); + this.type = type; } @Override @@ -46,7 +64,7 @@ protected int getBackButton () { @Override public CharSequence getName () { - return Lang.getString(R.string.Stickers); + return Lang.getString(type == TYPE_STICKER ? R.string.Stickers : R.string.EmojiPacks); } @Override @@ -54,24 +72,24 @@ protected int getTitleStyle () { return TITLE_STYLE_COMPACT_BIG; } - private ArrayList stickerSets; + private ArrayList cachedInstalledSets; @Override public void setArguments (SettingsController args) { super.setArguments(args); - ArrayList stickerSets = args.getStickerSets(); + ArrayList stickerSets = args.getStickerSets(isEmoji()); if (stickerSets == null) { - args.setStickerSetListener(this); + args.addStickerSetListener(isEmoji(), this); } else { setStickers(stickerSets); } } private void setStickers (ArrayList stickerSets) { - this.stickerSets = new ArrayList<>(stickerSets.size()); + this.cachedInstalledSets = new ArrayList<>(stickerSets.size()); for (TGStickerSetInfo info : stickerSets) { - info.setBoundList(this.stickerSets); - this.stickerSets.add(info); + info.setBoundList(this.cachedInstalledSets); + this.cachedInstalledSets.add(info); } } @@ -79,19 +97,24 @@ private void setStickers (ArrayList stickerSets) { public void destroy () { super.destroy(); if (getArguments() != null) { - getArguments().setStickerSetListener(null); + getArguments().removeStickerSetListener(isEmoji(), this); } } @Override - public void onStickerSetsLoaded (ArrayList stickerSets) { + public void onStickerSetsLoaded (ArrayList stickerSets, TdApi.StickerType type) { + if (!isEmoji() && type.getConstructor() != TdApi.StickerTypeRegular.CONSTRUCTOR + || isEmoji() && type.getConstructor() != TdApi.StickerTypeCustomEmoji.CONSTRUCTOR) { + return; + } + if (getArguments() != null) { - getArguments().setStickerSetListener(null); + getArguments().removeStickerSetListener(isEmoji(), this); } setStickers(stickerSets); ViewController c = getCachedControllerForId(R.id.controller_stickers); if (c != null) { - ((StickersController) c).setStickerSets(this.stickerSets, null); + ((StickersController) c).setStickerSets(this.cachedInstalledSets); } } @@ -122,16 +145,20 @@ public void onFocus () { @Override protected int getPagerItemCount () { - return 4; + return type == TYPE_STICKER ? 4 : 3; } @Override protected String[] getPagerSections () { - return new String[] { + return type == TYPE_STICKER ? new String[] { Lang.getString(R.string.Trending).toUpperCase(), Lang.getString(R.string.Installed).toUpperCase(), Lang.getString(R.string.Archived).toUpperCase(), Lang.getString(R.string.Masks).toUpperCase() + }: new String[] { + Lang.getString(R.string.Trending).toUpperCase(), + Lang.getString(R.string.Installed).toUpperCase(), + Lang.getString(R.string.Archived).toUpperCase() }; } @@ -151,23 +178,151 @@ protected ViewController onCreatePagerItemForPosition (Context context, int p switch (position) { case STICKERS_POSITION: { StickersController c = new StickersController(this.context, this.tdlib); - c.setArguments(new StickersController.Args(StickersController.MODE_STICKERS, true).setStickerSets(stickerSets)); + c.setArguments(new StickersController.Args(StickersController.MODE_STICKERS, isEmoji(), true).setStickerSets(cachedInstalledSets)); + c.search(searchRequest); return c; } case ARCHIVED_POSITION: { StickersController c = new StickersController(this.context, this.tdlib); - c.setArguments(new StickersController.Args(StickersController.MODE_STICKERS_ARCHIVED, false)); + c.setArguments(new StickersController.Args(StickersController.MODE_STICKERS_ARCHIVED, isEmoji(), false)); + c.search(searchRequest); return c; } case MASKS_POSITION: { StickersController c = new StickersController(this.context, this.tdlib); - c.setArguments(new StickersController.Args(StickersController.MODE_MASKS, false)); + c.setArguments(new StickersController.Args(StickersController.MODE_MASKS, isEmoji(), false)); + c.search(searchRequest); return c; } case TRENDING_POSITION: { - return new StickersTrendingController(this.context, this.tdlib); + StickersTrendingController c = new StickersTrendingController(this.context, this.tdlib, isEmoji()); + c.search(searchRequest); + return c; } } throw new IllegalArgumentException("position == " + position); } + + public boolean isEmoji () { + return type == TYPE_EMOJI; + } + + @Override + protected int getMenuId () { + return R.id.menu_search; + } + + @Override + protected int getSearchMenuId () { + return R.id.menu_clear; + } + + @Override + public void fillMenuItems (int id, HeaderView header, LinearLayout menu) { + if (id == R.id.menu_search) { + header.addSearchButton(menu, this, getHeaderIconColorId()).setTouchDownListener((v, e) -> {}); + } else if (id == R.id.menu_clear) { + header.addClearButton(menu, this); + } + } + + @Override + public void onMenuItemPressed (int id, View view) { + if (id == R.id.menu_btn_search) { + openSearchMode(); + } else if (id == R.id.menu_btn_clear) { + clearSearchInput(); + } + } + + /* Search */ + + private ViewPagerHeaderViewCompact searchHeaderCell; + private float lastSelectionFactor; + private @Nullable String searchRequest; + + @Override + protected void onSearchInputChanged (String input) { + super.onSearchInputChanged(input); + startSearch(input); + } + + @Override + protected void onLeaveSearchMode () { + super.onLeaveSearchMode(); + startSearch(null); + } + + private void startSearch (String request) { + if (StringUtils.equalsOrBothEmpty(request, searchRequest)) { + return; + } + searchRequest = request; + + for (int a = 0; a < getPagerItemCount(); a++) { + ViewController c = getCachedControllerForPosition(a); + if (c instanceof StickersController) { + ((StickersController) c).search(request); + } else if (c instanceof StickersTrendingController) { + ((StickersTrendingController) c).search(request); + } + } + } + + @Override + protected SearchEditTextDelegate genSearchHeader (HeaderView headerView) { + HeaderEditText editText = super.genSearchHeader(headerView).editView(); + searchHeaderCell = new ViewPagerHeaderViewCompact(context) { + boolean ignoreLayoutParams; + @Override + public void setLayoutParams (ViewGroup.LayoutParams params) { + if (!ignoreLayoutParams) { + ignoreLayoutParams = true; + super.setLayoutParams(params); + } + } + + @Override + protected boolean canTouchAt (float x, float y) { + y -= getRecyclerView().getTop() + (int) getRecyclerView().getTranslationY(); + return y < getTopView().getMeasuredHeight(); + } + }; + searchHeaderCell.getTopView().setItemPadding(Screen.dp(12f)); + searchHeaderCell.getTopView().setOnItemClickListener(this); + searchHeaderCell.getTopView().setItems(getPagerSections()); + searchHeaderCell.getTopView().setSelectionFactor(lastSelectionFactor); + searchHeaderCell.addView(editText); + + return new SearchEditTextDelegate() { + @NonNull + @Override + public View view () { + return searchHeaderCell; + } + + @NonNull + @Override + public HeaderEditText editView () { + return editText; + } + }; + } + + @Override + public void onPageScrolled (int position, float positionOffset, int positionOffsetPixels) { + super.onPageScrolled(position, positionOffset, positionOffsetPixels); + lastSelectionFactor = positionOffset + position; + if (searchHeaderCell != null) { + searchHeaderCell.getTopView().setSelectionFactor(lastSelectionFactor); + } + } + + @Override + protected void setCurrentPagerPosition (int position, boolean animated) { + super.setCurrentPagerPosition(position, animated); + if (searchHeaderCell != null && animated) { + searchHeaderCell.getTopView().setFromTo(getViewPager().getCurrentItem(), position); + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsThemeController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsThemeController.java index f3378f4283..49fea57821 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsThemeController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsThemeController.java @@ -31,7 +31,6 @@ import com.luckycatlabs.sunrisesunset.SunriseSunsetCalculator; -import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.BuildConfig; import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; @@ -41,13 +40,10 @@ import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.config.Device; import org.thunderdog.challegram.core.Lang; -import org.thunderdog.challegram.data.TD; -import org.thunderdog.challegram.data.TGReaction; import org.thunderdog.challegram.helper.LocationHelper; import org.thunderdog.challegram.navigation.SettingsWrapBuilder; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.ViewSupport; -import org.thunderdog.challegram.telegram.TGLegacyManager; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ThemeCustom; @@ -56,15 +52,13 @@ import org.thunderdog.challegram.theme.ThemeInfo; import org.thunderdog.challegram.theme.ThemeManager; import org.thunderdog.challegram.tool.Fonts; -import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.unsorted.Settings; +import org.thunderdog.challegram.util.AppInstallationUtil; import org.thunderdog.challegram.util.AppUpdater; import org.thunderdog.challegram.util.DrawableModifier; -import org.thunderdog.challegram.util.EmojiModifier; import org.thunderdog.challegram.util.Permissions; -import org.thunderdog.challegram.util.ReactionModifier; import org.thunderdog.challegram.util.StringList; import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.widget.RadioView; @@ -84,7 +78,7 @@ import me.vkryl.core.StringUtils; import me.vkryl.core.collection.IntList; -public class SettingsThemeController extends RecyclerViewController implements View.OnClickListener, ViewController.SettingsIntDelegate, SliderWrapView.RealTimeChangeListener, View.OnLongClickListener, TGLegacyManager.EmojiLoadListener, AppUpdater.Listener { +public class SettingsThemeController extends RecyclerViewController implements View.OnClickListener, ViewController.SettingsIntDelegate, SliderWrapView.RealTimeChangeListener, View.OnLongClickListener, AppUpdater.Listener { public SettingsThemeController (Context context, Tdlib tdlib) { super(context, tdlib); } @@ -137,7 +131,6 @@ public CharSequence getName () { public void destroy () { super.destroy(); cancelLocationRequest(); - TGLegacyManager.instance().removeEmojiListener(this); context().appUpdater().removeListener(this); } @@ -160,42 +153,6 @@ protected void setValuedSetting (ListItem item, SettingView v, boolean isUpdate) RadioView view = v.findRadioView(); if (view != null) view.setChecked(item.isSelected(), isUpdate); - } else if (itemId == R.id.btn_emoji) { - Settings.EmojiPack emojiPack = Settings.instance().getEmojiPack(); - if (emojiPack.identifier.equals(BuildConfig.EMOJI_BUILTIN_ID)) { - v.setData(R.string.EmojiBuiltIn); - } else { - v.setData(emojiPack.displayName); - } - } else if (itemId == R.id.btn_quick_reaction) { - final String[] reactions = Settings.instance().getQuickReactions(tdlib); - tdlib.ensureReactionsAvailable(reactions, reactionsUpdated -> { - if (reactionsUpdated) { - runOnUiThreadOptional(() -> { - updateQuickReaction(); - }); - } - }); - StringBuilder stringBuilder = new StringBuilder(); - if (reactions.length > 0) { - final List tgReactions = new ArrayList<>(reactions.length); - for (String reactionKey : reactions) { - TdApi.ReactionType reactionType = TD.toReactionType(reactionKey); - final TGReaction tgReaction = tdlib.getReaction(reactionType, false); - if (tgReaction != null) { - tgReactions.add(tgReaction); - if (stringBuilder.length() > 0) { - stringBuilder.append(Lang.getConcatSeparator()); - } - stringBuilder.append(tgReaction.getTitle()); - } - } - v.setDrawModifier(new ReactionModifier(v.getComplexReceiver(), tgReactions.toArray(new TGReaction[0]))); - v.setData(stringBuilder.toString()); - } else { - v.setDrawModifier(null); - v.setData(R.string.QuickReactionDisabled); - } } else if (itemId == R.id.btn_icon) { v.setData(R.string.IconsBuiltIn); } else if (itemId == R.id.btn_reduceMotion) { @@ -271,18 +228,6 @@ protected void setValuedSetting (ListItem item, SettingView v, boolean isUpdate) v.setData(R.string.ChatListStyle1); break; } - } else if (itemId == R.id.btn_stickerSuggestions) { - switch (Settings.instance().getStickerMode()) { - case Settings.STICKER_MODE_ALL: - v.setData(R.string.SuggestStickersAll); - break; - case Settings.STICKER_MODE_ONLY_INSTALLED: - v.setData(R.string.SuggestStickersInstalled); - break; - case Settings.STICKER_MODE_NONE: - v.setData(R.string.SuggestStickersNone); - break; - } } else if (itemId == R.id.btn_autoNightModeScheduled_location) { if (isUpdate) { v.setEnabledAnimated(locationHelper == null); @@ -309,8 +254,6 @@ protected void setValuedSetting (ListItem item, SettingView v, boolean isUpdate) v.getToggler().setRadioEnabled(tdlib.ignoreSensitiveContentRestrictions(), isUpdate); } else if (itemId == R.id.btn_ignoreContentRestrictions) { v.getToggler().setRadioEnabled(!Settings.instance().needRestrictContent(), isUpdate); - } else if (itemId == R.id.btn_useBigEmoji) { - v.getToggler().setRadioEnabled(Settings.instance().useBigEmoji(), isUpdate); } else if (itemId == R.id.btn_markdown) { v.getToggler().setRadioEnabled(Settings.instance().getNewSetting(Settings.SETTING_FLAG_EDIT_MARKDOWN), isUpdate); } else if (itemId == R.id.btn_forceExoPlayerExtensions) { @@ -338,21 +281,6 @@ protected void setValuedSetting (ListItem item, SettingView v, boolean isUpdate) } else if (itemId == R.id.btn_autoNightModeScheduled_timeOff || itemId == R.id.btn_autoNightModeScheduled_timeOn) { int time = v.getId() == R.id.btn_autoNightModeScheduled_timeOn ? Settings.instance().getNightModeScheduleOn() : Settings.instance().getNightModeScheduleOff(); v.setData(U.timeToString(time)); - } else if (itemId == R.id.btn_big_reactions) { - StringBuilder b = new StringBuilder(); - if (Settings.instance().getBigReactionsInChats()) { - b.append(Lang.getString(R.string.BigReactionsChats)); - } - if (Settings.instance().getBigReactionsInChannels()) { - if (b.length() > 0) { - b.append(Lang.getConcatSeparator()); - } - b.append(Lang.getString(R.string.BigReactionsChannels)); - } - if (b.length() == 0) { - b.append(Lang.getString(R.string.BigReactionsNone)); - } - v.setData(b.toString()); } else if (itemId == R.id.btn_chatSwipes) { StringBuilder b = new StringBuilder(); if (Settings.instance().needChatQuickShare()) { @@ -460,19 +388,11 @@ protected void setValuedSetting (ListItem item, SettingView v, boolean isUpdate) items.add(new ListItem(ListItem.TYPE_CHECKBOX_OPTION, R.id.btn_forcePlainChannels, 0, R.string.ChatStyleBubblesChannel, R.id.btn_forcePlainChannels, !tdlib.settings().forcePlainModeInChannels())); if (!tdlib.account().isDebug()) { - items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_emoji, 0, R.string.Emoji).setDrawModifier(new EmojiModifier(Lang.getString(R.string.EmojiPreview), Paints.emojiPaint()))); - TGLegacyManager.instance().addEmojiListener(this); - if (BuildConfig.DEBUG) { items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_icon, 0, R.string.Icons).setDrawModifier(new DrawableModifier(R.drawable.baseline_star_20, R.drawable.baseline_account_balance_wallet_20, R.drawable.baseline_location_on_20, R.drawable.baseline_favorite_20))); } } - if (!tdlib.account().isDebug() || BuildConfig.DEBUG) { - items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_quick_reaction, 0, R.string.QuickReaction)); - } items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_chatListStyle, 0, R.string.ChatListStyle)); @@ -609,7 +529,7 @@ public void onRemove (RecyclerView.ViewHolder viewHolder) { }*/ items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); - if (U.isAppSideLoaded()) { + if (AppInstallationUtil.isAppSideLoaded()) { items.addAll(Arrays.asList( new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.InAppUpdates), new ListItem(ListItem.TYPE_SHADOW_TOP), @@ -632,16 +552,8 @@ public void onRemove (RecyclerView.ViewHolder viewHolder) { items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.Chats)); items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); - items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_toggleNewSetting, 0, R.string.AnimatedEmoji).setLongId(Settings.SETTING_FLAG_NO_ANIMATED_EMOJI).setBoolValue(true)); - items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_useBigEmoji, 0, R.string.BigEmoji)); - items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_big_reactions, 0, R.string.BigReactions)); - items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_toggleNewSetting, 0, R.string.LoopAnimatedStickers).setLongId(Settings.SETTING_FLAG_NO_ANIMATED_STICKERS_LOOP).setBoolValue(true)); - items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); - items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_stickerSuggestions, 0, R.string.SuggestStickers)); - boolean sideLoaded = U.isAppSideLoaded(); + + boolean sideLoaded = AppInstallationUtil.isAppSideLoaded(); if (tdlib.canIgnoreSensitiveContentRestriction() && (sideLoaded || tdlib.ignoreSensitiveContentRestrictions())) { items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL)); items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_restrictSensitiveContent, 0, R.string.DisplaySensitiveContent)); @@ -803,45 +715,11 @@ private static ListItem newCameraFlipInfoItem () { return new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.CameraFlipInfo); } - public void updateSelectedEmoji () { - if (adapter != null) - adapter.updateValuedSettingById(R.id.btn_emoji); - } - - public void updateQuickReaction () { - if (adapter != null) - adapter.updateValuedSettingById(R.id.btn_quick_reaction); - } - public void updateSelectedIconPack () { if (adapter != null) adapter.updateValuedSettingById(R.id.btn_icon); } - @Override - public void onPrepareToShow () { - super.onPrepareToShow(); - updateQuickReaction(); - } - - @Override - public void onEmojiUpdated (boolean isPackSwitch) { - if (isPackSwitch) { - updateSelectedEmoji(); - } else { - invalidateById(R.id.btn_emoji); - } - } - - private void invalidateById (int id) { - if (adapter != null) { - int index = adapter.indexOfViewById(id); - View view = getRecyclerView().getLayoutManager().findViewByPosition(index); - if (view != null) - view.invalidate(); - } - } - private static ListItem newItem (ThemeInfo theme) { int themeId = theme.getId(); ListItem item; @@ -1075,14 +953,6 @@ public void onClick (View v) { if (viewId == R.id.btn_reduceMotion) { Settings.instance().toggleReduceMotion(); adapter.updateValuedSettingById(R.id.btn_reduceMotion); - } else if (viewId == R.id.btn_quick_reaction) { - EditEnabledReactionsController c = new EditEnabledReactionsController(context, tdlib); - c.setArguments(new EditEnabledReactionsController.Args(null, EditEnabledReactionsController.TYPE_QUICK_REACTION)); - navigateTo(c); - } else if (viewId == R.id.btn_emoji) { - SettingsCloudEmojiController c = new SettingsCloudEmojiController(context, tdlib); - c.setArguments(new SettingsCloudController.Args<>(this)); - navigateTo(c); } else if (viewId == R.id.btn_icon) { SettingsCloudIconController c = new SettingsCloudIconController(context, tdlib); c.setArguments(new SettingsCloudController.Args<>(this)); @@ -1237,10 +1107,15 @@ public void onClick (View v) { } else { Calendar sunrise = SunriseSunsetCalculator.getSunrise(location.getLatitude(), location.getLongitude(), TimeZone.getDefault(), DateUtils.getNowCalendar(), 0); Calendar sunset = SunriseSunsetCalculator.getSunset(location.getLatitude(), location.getLongitude(), TimeZone.getDefault(), DateUtils.getNowCalendar(), 0); - /*if (result == null || result[0] == -1 || result[1] == -1) { - UI.showToast(R.string.AutoNightModeScheduledByLocationError, Toast.LENGTH_SHORT); - return; - }*/ + + if (sunrise == null || sunset == null) { + context.tooltipManager() + .builder(v) + .icon(R.drawable.baseline_info_24) + .show(tdlib, R.string.AutoNightModeScheduledByLocationPolarNight); + return; + } + int startHour = sunset.get(Calendar.HOUR_OF_DAY); int startMinute = sunset.get(Calendar.MINUTE); @@ -1310,12 +1185,8 @@ public void onClick (View v) { tdlib.setIgnoreSensitiveContentRestrictions(adapter.toggleView(v)); } else if (viewId == R.id.btn_ignoreContentRestrictions) { Settings.instance().setRestrictContent(!adapter.toggleView(v)); - } else if (viewId == R.id.btn_useBigEmoji) { - Settings.instance().setUseBigEmoji(adapter.toggleView(v)); } else if (viewId == R.id.btn_secret_batmanTransitions) { Settings.instance().setNewSetting(Settings.SETTING_FLAG_BATMAN_POLL_TRANSITIONS, adapter.toggleView(v)); - } else if (viewId == R.id.btn_stickerSuggestions) { - showStickerOptions(); } else if (viewId == R.id.btn_chatListStyle) { showChatListOptions(); } else if (viewId == R.id.btn_instantViewMode) { @@ -1328,16 +1199,6 @@ public void onClick (View v) { Settings.instance().setDisableChatQuickActions(result.get(R.id.btn_messageShare) != R.id.btn_messageShare, result.get(R.id.btn_messageReply) != R.id.btn_messageReply); adapter.updateValuedSettingById(R.id.btn_chatSwipes); }); - } else if (viewId == R.id.btn_big_reactions) { - showSettings(R.id.btn_big_reactions, new ListItem[] { - new ListItem(ListItem.TYPE_INFO, 0, 0, R.string.BigReactionsInfo), - new ListItem(ListItem.TYPE_CHECKBOX_OPTION, R.id.btn_bigReactionsChats, 0, R.string.BigReactionsChats, R.id.btn_bigReactionsChats, Settings.instance().getBigReactionsInChats()), - new ListItem(ListItem.TYPE_CHECKBOX_OPTION, R.id.btn_bigReactionsChannels, 0, R.string.BigReactionsChannels, R.id.btn_bigReactionsChannels, Settings.instance().getBigReactionsInChannels()) - }, (id, result) -> { - Settings.instance().setBigReactionsInChannels(result.get(R.id.btn_bigReactionsChannels) == R.id.btn_bigReactionsChannels); - Settings.instance().setBigReactionsInChats(result.get(R.id.btn_bigReactionsChats) == R.id.btn_bigReactionsChats); - adapter.updateValuedSettingById(R.id.btn_big_reactions); - }); } else if (viewId == R.id.btn_systemEmoji) { Settings.instance().setUseSystemEmoji(adapter.toggleView(v)); } else if (viewId == R.id.btn_customVibrations) { @@ -1528,7 +1389,7 @@ private void showThemeOptions (ListItem item) { int customThemeId = ThemeManager.resolveCustomThemeId(themeId); boolean canEdit = isCustom && Settings.instance().hasThemeOwnership(customThemeId); boolean isCurrent = ThemeManager.instance().isCurrentTheme(themeId); - int size = isCustom ? (isCurrent ? 3 : 4): 1; + int size = isCustom ? (isCurrent ? 3 : 4) : 1; IntList ids = new IntList(size); IntList icons = new IntList(size); StringList strings = new StringList(size); @@ -1843,27 +1704,6 @@ private void showChatListOptions () { }).setAllowResize(false)); //.setHeaderItem(new SettingItem(SettingItem.TYPE_INFO, 0, 0, UI.getString(R.string.MarkdownHint), false)) } - private void showStickerOptions () { - int stickerOption = Settings.instance().getStickerMode(); - showSettings(new SettingsWrapBuilder(R.id.btn_stickerSuggestions).setRawItems(new ListItem[]{ - new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_stickerSuggestionsAll, 0, R.string.SuggestStickersAll, R.id.btn_stickerSuggestions, stickerOption == Settings.STICKER_MODE_ALL), - new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_stickerSuggestionsInstalled, 0, R.string.SuggestStickersInstalled, R.id.btn_stickerSuggestions, stickerOption == Settings.STICKER_MODE_ONLY_INSTALLED), - new ListItem(ListItem.TYPE_RADIO_OPTION, R.id.btn_stickerSuggestionsNone, 0, R.string.SuggestStickersNone, R.id.btn_stickerSuggestions, stickerOption == Settings.STICKER_MODE_NONE), - }).setIntDelegate((id, result) -> { - int newStickerMode = Settings.instance().getStickerMode(); - int stickerResultId = result.get(R.id.btn_stickerSuggestions); - if (stickerResultId == R.id.btn_stickerSuggestionsAll) { - newStickerMode = Settings.STICKER_MODE_ALL; - } else if (stickerResultId == R.id.btn_stickerSuggestionsInstalled) { - newStickerMode = Settings.STICKER_MODE_ONLY_INSTALLED; - } else if (stickerResultId == R.id.btn_stickerSuggestionsNone) { - newStickerMode = Settings.STICKER_MODE_NONE; - } - Settings.instance().setStickerMode(newStickerMode); - adapter.updateValuedSettingById(R.id.btn_stickerSuggestions); - }).setAllowResize(false)); //.setHeaderItem(new SettingItem(SettingItem.TYPE_INFO, 0, 0, UI.getString(R.string.MarkdownHint), false)) - } - private void showEarpieceOptions (boolean isVideo) { int earpieceMode = Settings.instance().getEarpieceMode(isVideo); int settingId = isVideo ? R.id.btn_earpieceModeVideo : R.id.btn_earpieceMode; diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsWebsitesController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsWebsitesController.java index 856e8cec42..2955f168a8 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsWebsitesController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsWebsitesController.java @@ -21,7 +21,6 @@ import android.widget.TextView; import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.base.SettingView; import org.thunderdog.challegram.core.Lang; @@ -121,7 +120,7 @@ protected void setSession (ListItem item, int position, RelativeLayout parent, b TdApi.ConnectedWebsite website = (TdApi.ConnectedWebsite) item.getData(); titleView.setText(Emoji.instance().replaceEmoji(website.domainName)); subtextView.setText(Strings.concat(", ", Emoji.instance().replaceEmoji(tdlib.cache().userName(website.botUserId)), website.browser, website.platform)); - locationView.setText(Strings.concatIpLocation(website.ip, website.location)); + locationView.setText(Strings.concatIpLocation(website.ipAddress, website.location)); timeView.setText(Lang.timeOrDateShort(website.lastActiveDate, TimeUnit.SECONDS)); avatarView.setUser(tdlib, website.botUserId, false); @@ -140,24 +139,13 @@ protected void setSession (ListItem item, int position, RelativeLayout parent, b } if (getArguments() == null) { - tdlib.client().send(new TdApi.GetConnectedWebsites(), object -> tdlib.ui().post(() -> { - if (!isDestroyed()) { - switch (object.getConstructor()) { - case TdApi.ConnectedWebsites.CONSTRUCTOR: { - TdApi.ConnectedWebsite[] websites = ((TdApi.ConnectedWebsites) object).websites; - setWebsites(websites); - buildCells(); - break; - } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(object); - break; - } - default: { - Log.unexpectedTdlibResponse(object, TdApi.GetConnectedWebsites.class, TdApi.ConnectedWebsites.class); - break; - } - } + tdlib.send(new TdApi.GetConnectedWebsites(), (connectedWebsites, error) -> runOnUiThreadOptional(() -> { + if (error != null) { + UI.showError(error); + } else { + TdApi.ConnectedWebsite[] websites = connectedWebsites.websites; + setWebsites(websites); + buildCells(); } })); } @@ -280,7 +268,7 @@ private void terminateSession (final TdApi.ConnectedWebsite website, boolean ban })); if (banUser) { - tdlib.blockSender(new TdApi.MessageSenderUser(website.botUserId), true, tdlib.okHandler()); + tdlib.blockSender(new TdApi.MessageSenderUser(website.botUserId), new TdApi.BlockListMain(), tdlib.okHandler()); } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ShareController.java b/app/src/main/java/org/thunderdog/challegram/ui/ShareController.java index 8dd59baa4c..2d2368d28a 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/ShareController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/ShareController.java @@ -58,6 +58,8 @@ import org.thunderdog.challegram.component.chat.EmojiToneHelper; import org.thunderdog.challegram.component.chat.InputView; import org.thunderdog.challegram.component.dialogs.SearchManager; +import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Background; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.InlineResult; @@ -69,6 +71,7 @@ import org.thunderdog.challegram.navigation.HeaderButton; import org.thunderdog.challegram.navigation.HeaderView; import org.thunderdog.challegram.navigation.Menu; +import org.thunderdog.challegram.navigation.MenuMoreWrap; import org.thunderdog.challegram.navigation.TelegramViewController; import org.thunderdog.challegram.navigation.TooltipOverlayView; import org.thunderdog.challegram.navigation.ViewController; @@ -111,7 +114,10 @@ import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import me.vkryl.android.AnimatorUtils; import me.vkryl.android.ViewUtils; @@ -179,6 +185,8 @@ public static class Args { private boolean allowCopyLink; + private boolean disallowReply; + private ShareProviderDelegate customDelegate; private Runnable after; @@ -232,8 +240,13 @@ public Args (TdApi.User contactUser) { } public Args (TdApi.Sticker sticker) { - this.mode = MODE_STICKER; - this.sticker = sticker; + if (Td.customEmojiId(sticker) != 0) { + this.mode = MODE_TEXT; + this.text = TD.toSingleEmojiText(sticker); + } else { + this.mode = MODE_STICKER; + this.sticker = sticker; + } } public Args (ShareProviderDelegate delegate) { @@ -260,6 +273,11 @@ public Args setExport (String exportText) { return this; } + public Args setDisallowReply (boolean disallowReply) { + this.disallowReply = disallowReply; + return this; + } + public Args setShare (@NonNull String shareText, @Nullable String shareButtonText) { this.shareText = shareText; this.shareButtonText = shareButtonText; @@ -306,8 +324,7 @@ public ShareController (Context context, Tdlib tdlib) { } private int mode; - private TdApi.ChatList chatList; - private TdlibChatListSlice list; + private TdApi.ChatList displayingChatList; @Override public CharSequence getName () { @@ -338,7 +355,7 @@ protected int getBackButton () { public void setArguments (Args args) { super.setArguments(args); this.mode = args.mode; - this.chatList = args.chatList != null ? args.chatList : ChatPosition.CHAT_LIST_MAIN; + this.displayingChatList = args.chatList != null ? args.chatList : ChatPosition.CHAT_LIST_MAIN; } @Override @@ -394,7 +411,7 @@ public boolean onBackPressed (boolean fromTop) { @Override public void onMenuItemPressed (int id, View view) { if (id == R.id.menu_btn_search) { - if (displayingChats != null) { + if (getDisplayingChats() != null) { openSearchMode(); } } else if (id == R.id.menu_btn_copy) { @@ -520,7 +537,7 @@ private void onFileLoaded (FileEntry file) { if (headerView != null) { headerView.updateCustomButton(getMenuId(), R.id.menu_btn_forward, view -> { if (view instanceof HeaderButton) { - ((HeaderButton) view).setShowProgress(false, 0f); + ((HeaderButton) view).setShowProgress(false, 0f); } }); } @@ -556,7 +573,7 @@ private void cancelDownloadingFiles () { if (!isDestroyed() && headerView != null) { headerView.updateCustomButton(getMenuId(), R.id.menu_btn_forward, view -> { if (view instanceof HeaderButton) { - ((HeaderButton) view).setShowProgress(false, 0f); + ((HeaderButton) view).setShowProgress(false, 0f); } }); } @@ -672,9 +689,9 @@ private void exportContent () { String mimeType = getExportMimeType(args.messages[0]); int count = args.messages.length; - int type = args.messages[0].content.getConstructor(); + @TdApi.MessageContent.Constructors int type = args.messages[0].content.getConstructor(); for (int i = 1; i < count; i++) { - int contentType = args.messages[i].content.getConstructor(); + @TdApi.MessageContent.Constructors int contentType = args.messages[i].content.getConstructor(); if (contentType != type) { type = 0; break; @@ -915,47 +932,13 @@ public void onThemeColorsChanged (boolean areTemp, ColorState state) { public boolean accept (TdApi.Chat chat) { if (tdlib.chatAvailable(chat)) { Tdlib.RestrictionStatus restrictionStatus = tdlib.getRestrictionStatus(chat, RightId.SEND_BASIC_MESSAGES); - return restrictionStatus == null || !restrictionStatus.isGlobal(); + return restrictionStatus == null || !restrictionStatus.isGlobal() || tdlib.canSendSendSomeMedia(chat, true); } return false; } @Override protected View onCreateView (Context context) { - list = new TdlibChatListSlice(tdlib, chatList, this, true) { - @Override - protected boolean modifySlice (List slice, int currentSize) { - int index = 0; - for (Entry entry : slice) { - if (tdlib.isSelfChat(entry.chat)) { - if (currentSize > 0) { - slice.remove(index); - return true; - } else if (index == 0 || ChatPosition.isPinned(entry.chat, chatList)) { - return false; - } else { - slice.remove(index); - entry.bringToTop(); - slice.add(0, entry); - return true; - } - } - index++; - } - if (currentSize == 0) { - TdApi.Chat selfChat = tdlib.selfChat(); - if (selfChat != null && !ChatPosition.isPinned(selfChat, chatList)) { - Entry entry = new Entry(selfChat, chatList, ChatPosition.findPosition(selfChat, chatList), true); - entry.bringToTop(); - slice.add(0, entry); - return true; - } - list.bringToTop(tdlib.selfChatId(), () -> new TdApi.CreatePrivateChat(tdlib.myUserId(), false), null); - } - return false; - } - }; - canShareLink = canShareLink(); headerCell = new DoubleHeaderView(context); @@ -963,6 +946,16 @@ protected boolean modifySlice (List slice, int currentSize) { headerCell.initWithMargin(Screen.dp(56f) * getMenuItemCount(), false); headerCell.setThemedTextColor(ColorId.text, ColorId.textLight, this); updateHeader(); + if (Settings.instance().chatFoldersEnabled() && TD.isChatListMain(displayingChatList) && tdlib.chatFoldersCount() > 0) { + headerCell.setTitle(R.string.CategoryMain); + headerCell.setTitleIcon(R.drawable.baseline_keyboard_arrow_down_20); + headerCell.setOnClickListener(v -> { + if (!inSearchMode()) { + showFolderSelector(); + } + }); + Views.setClickable(headerCell); + } contentView = new RelativeLayout(context); contentView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); @@ -1031,11 +1024,12 @@ public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newSta @Override public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { checkHeaderPosition(); - if (list.canLoad() && !inSearchMode()) { + TdlibChatListSlice displayingList = getDisplayingList(); + if (displayingList != null && displayingList.canLoad() && !inSearchMode()) { GridLayoutManager gridManager = (GridLayoutManager) recyclerView.getLayoutManager(); int i = gridManager.findLastVisibleItemPosition(); if (i >= adapter.getItemCount() - gridManager.getSpanCount()) { - list.loadMore(30, null); + displayingList.loadMore(30, null); } } } @@ -1057,7 +1051,7 @@ public int getSpanSize (int position) { recyclerView.addItemDecoration(decoration); recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { @Override - public void getItemOffsets (Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + public void getItemOffsets (@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { outRect.top = 0; outRect.bottom = 0; int i = parent.getChildAdapterPosition(view); @@ -1377,22 +1371,69 @@ protected void setChatData (ListItem item, VerticalChatView chatView) { // Load chats - tdlib.client().send(new TdApi.CreatePrivateChat(tdlib.myUserId(), true), tdlib.silentHandler()); + initializeChatList(displayingChatList); + + return wrapView; + } + + private void initializeChatList (TdApi.ChatList chatList) { + TdlibChatListSlice list = new TdlibChatListSlice(tdlib, chatList, this, true) { + @Override + protected boolean modifySlice (List slice, int currentSize) { + int index = 0; + for (Entry entry : slice) { + if (tdlib.isSelfChat(entry.chat)) { + if (currentSize > 0) { + slice.remove(index); + return true; + } else if (index == 0 || ChatPosition.isPinned(entry.chat, chatList())) { + return false; + } else { + slice.remove(index); + entry.bringToTop(); + slice.add(0, entry); + return true; + } + } + index++; + } + if (currentSize == 0 && TD.isChatListMain(chatList())) { + TdApi.Chat selfChat = tdlib.selfChat(); + if (selfChat != null && !ChatPosition.isPinned(selfChat, chatList())) { + Entry entry = new Entry(selfChat, chatList(), ChatPosition.findPosition(selfChat, chatList()), true); + entry.bringToTop(); + slice.add(0, entry); + return true; + } + bringToTop(tdlib.selfChatId(), () -> new TdApi.CreatePrivateChat(tdlib.myUserId(), false), null); + } + return false; + } + }; // FIXME replace Math.max with proper fix. int startLoadCount = Math.max(20, Screen.calculateLoadingItems(Screen.dp(95f), 1) * calculateSpanCount()); - list.initializeList(this, this::processChats, startLoadCount, this::executeScheduledAnimation); + list.initializeList(this, entries -> processChats(list.chatList(), entries), startLoadCount, this::executeScheduledAnimation); + bindListToChatList(chatList, list); + } - return wrapView; + private boolean isDisplayingChatList (@Nullable TdApi.ChatList chatList) { + return Td.equalsTo(chatList, displayingChatList); } @Override public void onChatAdded (TdlibChatList chatList, TdApi.Chat chat, int atIndex, Tdlib.ChatChange changeInfo) { runOnUiThreadOptional(() -> { + List displayingChats = getChatsByChatList(chatList.chatList()); if (displayingChats != null) { - TGFoundChat parsedChat = newChat(chat); + TGFoundChat parsedChat = newChat(chatList.chatList(), chat); displayingChats.add(atIndex, parsedChat); - adapter.addItem(atIndex, valueOfChat(parsedChat)); - recyclerView.invalidateItemDecorations(); + if (isDisplayingChatList(chatList.chatList())) { + if (isEmptyItem(adapter.getItem(0))) { + adapter.removeItem(0); + } + adapter.addItem(atIndex, valueOfChat(parsedChat)); + recyclerView.invalidateItemDecorations(); + } } }); } @@ -1400,10 +1441,13 @@ public void onChatAdded (TdlibChatList chatList, TdApi.Chat chat, int atIndex, T @Override public void onChatRemoved (TdlibChatList chatList, TdApi.Chat chat, int fromIndex, Tdlib.ChatChange changeInfo) { runOnUiThreadOptional(() -> { + List displayingChats = getChatsByChatList(chatList.chatList()); if (displayingChats != null) { displayingChats.remove(fromIndex); - adapter.removeItem(fromIndex); - recyclerView.invalidateItemDecorations(); + if (isDisplayingChatList(chatList.chatList())) { + adapter.removeItem(fromIndex); + recyclerView.invalidateItemDecorations(); + } } }); } @@ -1411,43 +1455,46 @@ public void onChatRemoved (TdlibChatList chatList, TdApi.Chat chat, int fromInde @Override public void onChatMoved (TdlibChatList chatList, TdApi.Chat chat, int fromIndex, int toIndex, Tdlib.ChatChange changeInfo) { runOnUiThreadOptional(() -> { + List displayingChats = getChatsByChatList(chatList.chatList()); if (displayingChats != null) { TGFoundChat entry = displayingChats.remove(fromIndex); displayingChats.add(toIndex, entry); - LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager(); - int savedPosition, savedOffset; - if (manager != null) { - savedPosition = manager.findFirstVisibleItemPosition(); - View view = manager.findViewByPosition(savedPosition); - savedOffset = view != null ? manager.getDecoratedTop(view) : 0; - } else { - savedPosition = RecyclerView.NO_POSITION; - savedOffset = 0; - } + if (isDisplayingChatList(chatList.chatList())) { + LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager(); + int savedPosition, savedOffset; + if (manager != null) { + savedPosition = manager.findFirstVisibleItemPosition(); + View view = manager.findViewByPosition(savedPosition); + savedOffset = view != null ? manager.getDecoratedTop(view) : 0; + } else { + savedPosition = RecyclerView.NO_POSITION; + savedOffset = 0; + } - adapter.moveItem(fromIndex, toIndex); - recyclerView.invalidateItemDecorations(); // TODO detect only first-non-first row changes - if (savedPosition != RecyclerView.NO_POSITION) { - manager.scrollToPositionWithOffset(savedPosition, savedOffset); + adapter.moveItem(fromIndex, toIndex); + recyclerView.invalidateItemDecorations(); // TODO detect only first-non-first row changes + if (savedPosition != RecyclerView.NO_POSITION) { + manager.scrollToPositionWithOffset(savedPosition, savedOffset); + } } } }); } - private TGFoundChat newChat (TdApi.Chat rawChat) { + private TGFoundChat newChat (TdApi.ChatList chatList, TdApi.Chat rawChat) { TGFoundChat chat = new TGFoundChat(tdlib, chatList, rawChat, false, null); chat.setNoUnread(); chat.setNoSubscription(); return chat; } - private void processChats (List entries) { + private void processChats (TdApi.ChatList chatList, List entries) { final List result = new ArrayList<>(entries.size()); for (TdlibChatList.Entry entry : entries) { - result.add(newChat(entry.chat)); + result.add(newChat(chatList, entry.chat)); } runOnUiThreadOptional(() -> - displayChats(result) + displayChats(chatList, result) ); } @@ -1511,7 +1558,7 @@ protected boolean canSelectFoundChat (TGFoundChat chat) { } private int detectTopRecyclerEdge () { - GridLayoutManager manager = (GridLayoutManager) recyclerView.getLayoutManager(); + GridLayoutManager manager = (GridLayoutManager) recyclerView.getLayoutManager(); int first = manager.findFirstVisibleItemPosition(); int spanCount = manager.getSpanCount(); int top = 0; @@ -1563,13 +1610,25 @@ protected boolean onFoundChatClick (View view, TGFoundChat chat) { } final long chatId = chat.getAnyId(); + final TGFoundChat finalChat = chat; + final RunnableBool after = (b) -> { + if (b) { + onFoundChatClickAfter(finalChat); + } + }; + if (!isChecked(chatId)) { if (processSingleTap(chat)) return true; - if (!toggleChecked(view, chat, null)) + if (!toggleChecked(view, chat, after)) return true; + } else { + onFoundChatClickAfter(chat); } + return true; + } + private void onFoundChatClickAfter (TGFoundChat chat) { int i = adapter.indexOfViewByLongId(chat.getAnyId()); if (i != -1) { View itemView = recyclerView.getLayoutManager().findViewByPosition(i); @@ -1579,14 +1638,29 @@ protected boolean onFoundChatClick (View view, TGFoundChat chat) { adapter.notifyItemChanged(i); } } - - list.bringToTop(chat.getAnyId(), null, () -> runOnUiThreadOptional(() -> { - needPostponeAutoScroll = true; - closeSearchMode(null); - needPostponeAutoScroll = false; - })); - - return true; + TdApi.Chat selectedChat = chat.getChat(); + if (selectedChat != null) { + Runnable after = () -> runOnUiThreadOptional(() -> { + needPostponeAutoScroll = true; + closeSearchMode(null); + needPostponeAutoScroll = false; + }); + for (TdlibChatListSlice list : listByChatList.values()) { + TdApi.ChatPosition position = ChatPosition.findPosition(selectedChat, list.chatList()); + if (position != null && position.order != 0) { + Runnable doAfter; + if (isDisplayingChatList(list.chatList())) { + doAfter = after; after = null; + } else { + doAfter = null; + } + list.bringToTop(chat.getAnyId(), null, doAfter); + } + } + if (after != null) { + after.run(); + } + } } @Override @@ -1631,6 +1705,13 @@ private void checkHeaderPosition () { } else if (autoScrollFinished && top > 0 && inSearchMode() && getSearchTransformFactor() == 1f) { recyclerView.scrollBy(0, top); } + if (folderSelectorLayout != null) { + View boundView = folderSelectorLayout.getBoundView(); + if (boundView != null && boundView.getTranslationY() != headerView.getTranslationY()) { + boundView.setTranslationY(headerView.getTranslationY()); + boundView.requestLayout(); + } + } } } @@ -1670,8 +1751,7 @@ private boolean hasVoiceOrVideoMessageContent () { } case MODE_MESSAGES: { for (TdApi.Message message : args.messages) { - if (message.content.getConstructor() == TdApi.MessageVoiceNote.CONSTRUCTOR || - message.content.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR) { + if (Td.isVoiceNote(message.content) || Td.isVideoNote(message.content)) { return true; } } @@ -1679,8 +1759,7 @@ private boolean hasVoiceOrVideoMessageContent () { } case MODE_CUSTOM_CONTENT: { return - args.customContent.getConstructor() == TdApi.InputMessageVoiceNote.CONSTRUCTOR || - args.customContent.getConstructor() == TdApi.InputMessageVideoNote.CONSTRUCTOR; + Td.isVoiceNote(args.customContent) || Td.isVideoNote(args.customContent); } } return false; @@ -1689,6 +1768,12 @@ private boolean hasVoiceOrVideoMessageContent () { private CharSequence getErrorMessage (long chatId) { Args args = getArgumentsStrict(); TdApi.Chat chat = tdlib.chatStrict(chatId); + + CharSequence slowModeRestrictionText = tdlib().getSlowModeRestrictionText(chatId); + if (slowModeRestrictionText != null) { + return slowModeRestrictionText; + } + switch (mode) { case MODE_TEXT: { return tdlib.getBasicMessageRestrictionText(chat); @@ -1715,15 +1800,21 @@ private CharSequence getErrorMessage (long chatId) { } case MODE_MESSAGES: { for (TdApi.Message message : args.messages) { - if (ChatId.isSecret(chatId) && !TD.canSendToSecretChat(message.content)) - return Lang.getString(R.string.SecretChatForwardError); + if (ChatId.isSecret(chatId)) { + if (!TD.canSendToSecretChat(message.content)) + return Lang.getString(R.string.SecretChatForwardError); + TdApi.ForwardMessages function = new TdApi.ForwardMessages(chatId, 0, message.chatId, new long[] {message.id}, new TdApi.MessageSendOptions(false, false, false, false, null, 0, true), needHideAuthor, needRemoveCaptions); + TdApi.Object check = tdlib.clientExecute(function, 1000L); + if (check instanceof TdApi.Error) { + return TD.toErrorString(check); + } + } if (message.content.getConstructor() == TdApi.MessagePoll.CONSTRUCTOR && !((TdApi.MessagePoll) message.content).poll.isAnonymous && tdlib.isChannel(chatId)) return Lang.getString(R.string.PollPublicForwardHint); - if (message.content.getConstructor() == TdApi.MessageVoiceNote.CONSTRUCTOR || - message.content.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR) { - CharSequence restrictionText = tdlib.getVoiceVideoRestricitonText(chat, message.content.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR); + if (Td.isVoiceNote(message.content) || Td.isVideoNote(message.content)) { + CharSequence restrictionText = tdlib.getVoiceVideoRestrictionText(chat, Td.isVideoNote(message.content)); if (restrictionText != null) return restrictionText; } @@ -1774,13 +1865,13 @@ private boolean showErrorMessage (View anchorView, long chatId, boolean includeT return false; } - private boolean toggleChecked (View view, TGFoundChat chat, RunnableBool after) { + private boolean toggleChecked (View view, TGFoundChat chat, RunnableBool after) { return toggleCheckedImpl(view, chat, after, true); } private final LongSet lockedChatIds = new LongSet(); - private boolean toggleCheckedImpl (View view, TGFoundChat chat, @Nullable RunnableBool after, boolean performAsyncChecks) { + private boolean toggleCheckedImpl (View view, TGFoundChat chat, @Nullable RunnableBool after, boolean performAsyncChecks) { long chatId = chat.getAnyId(); if (lockedChatIds.has(chatId)) { return false; @@ -1789,18 +1880,27 @@ private boolean toggleCheckedImpl (View view, TGFoundChat chat, @Nullable Runnab boolean result = !isChecked(chatId); if (result) { - if (performAsyncChecks && ChatId.isUserChat(chatId) && hasVoiceOrVideoMessageContent()) { - lockedChatIds.add(chatId); - tdlib.cache().userFull(tdlib.chatUserId(chatId), userFullInfo -> { - lockedChatIds.remove(chatId); - // FIXME: view recycling safety - // By the time `after` is called, initial view could have been already recycled. - // Current implementation relies on the quick response from GetUserFull, - // however, there's a chance `view` could have been already taken by some other view. - // Should be fixed inside `after` contents. - toggleCheckedImpl(view, chat, after, false); - }); - return false; + if (performAsyncChecks) { + // FIXME: view recycling safety + // By the time `after` is called, initial view could have been already recycled. + // Current implementation relies on the quick response from GetUserFull, + // however, there's a chance `view` could have been already taken by some other view. + // Should be fixed inside `after` contents. + if (ChatId.isUserChat(chatId) && hasVoiceOrVideoMessageContent()) { + lockedChatIds.add(chatId); + tdlib.cache().userFull(tdlib.chatUserId(chatId), userFullInfo -> { + lockedChatIds.remove(chatId); + toggleCheckedImpl(view, chat, after, false); + }); + return false; + } else if (ChatId.isSupergroup(chatId)) { + lockedChatIds.add(chatId); + tdlib.cache().supergroupFull(ChatId.toSupergroupId(chatId), supergroupFullInfo -> { + lockedChatIds.remove(chatId); + toggleCheckedImpl(view, chat, after, false); + }); + return false; + } } if (showErrorMessage(view, chatId, false)) { result = false; @@ -1870,6 +1970,10 @@ private void performSend (boolean needHideKeyboard, TdApi.MessageSendOptions fin } } + private boolean selectedSelfChatOnly () { + return selectedChats.size() == 1 && selectedChats.valueAt(0).isSelfChat(); + } + @Override public void onClick (View v) { final int viewId = v.getId(); @@ -1879,11 +1983,19 @@ public void onClick (View v) { shareLink(); // copyLink(); } + } else if (inputView != null && !selectedSelfChatOnly() && !tdlib.hasPremium() && inputView.hasOnlyPremiumFeatures()) { + context().tooltipManager().builder(sendButton) + .show(tdlib, Strings.buildMarkdown(this, Lang.getString(R.string.MessageContainsPremiumFeatures), null)) + .hideDelayed(); } else { performSend(false, Td.newSendOptions(), false); } } else if (viewId == R.id.btn_done) { - if (selectedChats.size() == 0) { + if (inputView != null && !selectedSelfChatOnly() && !tdlib.hasPremium() && inputView.hasOnlyPremiumFeatures()) { + context().tooltipManager().builder(okButton) + .show(tdlib, Strings.buildMarkdown(this, Lang.getString(R.string.MessageContainsPremiumFeatures), null)) + .hideDelayed(); + } else if (selectedChats.size() == 0) { hideSoftwareKeyboard(); } else { performSend(true, Td.newSendOptions(), false); @@ -2010,6 +2122,7 @@ protected void onEnterSearchMode () { } else { setAutoScrollFinished(true); } + hideFolderSelector(); } private void setAutoScrollFinished (boolean isFinished) { @@ -2214,32 +2327,84 @@ public int getCurrentPopupHeight () { // Data loading - private List displayingChats; + private final Map listByChatList = new HashMap<>(); + private final Map> chatsByChatList = new HashMap<>(); + + private @Nullable TdlibChatListSlice getDisplayingList () { + return getListByChatList(displayingChatList); + } + + private @Nullable List getDisplayingChats () { + return getChatsByChatList(displayingChatList); + } + + private @Nullable TdlibChatListSlice getListByChatList (TdApi.ChatList chatList) { + return listByChatList.get(TD.makeChatListKey(chatList)); + } + + private void bindListToChatList (TdApi.ChatList chatList, TdlibChatListSlice list) { + listByChatList.put(TD.makeChatListKey(chatList), list); + } + + private @Nullable List getChatsByChatList (TdApi.ChatList chatList) { + return chatsByChatList.get(TD.makeChatListKey(chatList)); + } + + private void bindChatsToChatList (TdApi.ChatList chatList, List chats) { + chatsByChatList.put(TD.makeChatListKey(chatList), chats); + } - private void displayChats (List chats) { - boolean areFirst = displayingChats == null; - if (areFirst) { - displayingChats = chats; + private void displayChatList (String title, TdApi.ChatList chatList) { + if (isDisplayingChatList(chatList)) + return; + headerCell.setTitle(StringUtils.isEmptyOrBlank(title) ? getName() : title); + displayingChatList = chatList; + if (getListByChatList(chatList) != null) { + List chats = getChatsByChatList(chatList); + if (chats != null && !chats.isEmpty()) { + List items = new ArrayList<>(chats.size()); + addCells(chats, items); + adapter.setItems(items, false); + } else { + adapter.setItems(Collections.singletonList(emptyItem()), false); + } + } else { + adapter.setItems(Collections.singletonList(emptyItem()), false); + initializeChatList(chatList); + } + recyclerView.invalidateItemDecorations(); + } + + private void displayChats (TdApi.ChatList chatList, List chats) { + List displayingChats = getChatsByChatList(chatList); + if (displayingChats == null) { + bindChatsToChatList(chatList, chats); } else { ArrayUtils.ensureCapacity(displayingChats, displayingChats.size() + chats.size()); displayingChats.addAll(chats); } - // if (!inSearchMode()) { + if (isDisplayingChatList(chatList)) { + if (isEmptyItem(adapter.getItem(0))) { + adapter.getItems().remove(0); + adapter.notifyItemRemoved(0); + } final int startIndex = adapter.getItems().size(); addCells(chats, adapter.getItems()); - adapter.notifyItemRangeInserted(startIndex, adapter.getItems().size() - startIndex); - // } - - if (areFirst) { - recyclerView.setAdapter(adapter); - launchOpenAnimation(); - } else { - recyclerView.invalidateItemDecorations(); + if (recyclerView.getAdapter() == null) { + recyclerView.setAdapter(adapter); + launchOpenAnimation(); + } else { + int newItemCount = adapter.getItems().size() - startIndex; + if (newItemCount > 0) { + adapter.notifyItemRangeInserted(startIndex, newItemCount); + recyclerView.invalidateItemDecorations(); + } + } } } - private void addCells (List entries, List out) { + private static void addCells (List entries, List out) { if (entries.isEmpty()) { return; } @@ -2253,6 +2418,14 @@ private static ListItem valueOfChat (TGFoundChat chat) { return new ListItem(ListItem.TYPE_CHAT_VERTICAL_FULLWIDTH, R.id.chat).setData(chat).setLongId(chat.getAnyId()); } + private static ListItem emptyItem () { + return new ListItem(ListItem.TYPE_PADDING, android.R.id.empty).setHeight(Screen.dp(86f) + Screen.dp(VERTICAL_PADDING_SIZE)); + } + + private static boolean isEmptyItem (@Nullable ListItem item) { + return item != null && item.getId() == android.R.id.empty; + } + // Button private static class SendButton extends FrameLayoutFix implements FactorAnimator.Target { @@ -2751,6 +2924,11 @@ public void onEnterEmoji (String emoji) { inputView.onEmojiSelected(emoji); } + @Override + public void onEnterCustomEmoji (TGStickerObj sticker) { + inputView.onCustomEmojiSelected(sticker); + } + @Override public void onDeleteEmoji () { if (inputView.length() > 0) { @@ -2777,6 +2955,7 @@ private String getExportMimeType (TdApi.Message message) { TdApi.File file = getFile(message); if (file == null) return null; + //noinspection SwitchIntDef switch (message.content.getConstructor()) { case TdApi.MessageText.CONSTRUCTOR: { return TD.getMimeType(((TdApi.MessageText) message.content).webPage); @@ -2840,6 +3019,7 @@ private boolean canExportContent () { } private static boolean canExportStaticContent (TdApi.Message msg) { + //noinspection SwitchIntDef switch (msg.content.getConstructor()) { case TdApi.MessageContact.CONSTRUCTOR: return true; @@ -2848,6 +3028,7 @@ private static boolean canExportStaticContent (TdApi.Message msg) { } private void exportStaticContent (TdApi.Message msg) { + //noinspection SwitchIntDef switch (msg.content.getConstructor()) { case TdApi.MessageContact.CONSTRUCTOR: { TdApi.Contact contact = ((TdApi.MessageContact) msg.content).contact; @@ -2944,7 +3125,7 @@ private void showShareSettings () { if (mode == MODE_MESSAGES) { for (TdApi.Message message : getArgumentsStrict().messages) { - if (message.content.getConstructor() != TdApi.MessageText.CONSTRUCTOR && TD.canCopyText(message)) { + if (!Td.isText(message.content) && TD.canCopyText(message)) { canRemoveCaptions = true; break; } @@ -3027,33 +3208,65 @@ private void sendMessages (boolean forceGoToChat, boolean isSingleTap, @Nullable tdlib.client().send(new TdApi.CreatePrivateChat(myUserId, true), tdlib.silentHandler()); } TdApi.MessageSendOptions sendOptions = ChatId.isSecret(chatId) ? secretSendOptions : cloudSendOptions; + boolean messageReplyIncluded = false; if (hasComment) { - functions.addAll(TD.sendMessageText(chatId, 0, 0, sendOptions, new TdApi.InputMessageText(comment, false, false), tdlib.maxMessageTextLength())); + TdApi.InputMessageReplyToMessage replyTo = null; + if (Config.FORCE_REPLY_WHEN_FORWARDING_WITH_COMMENT && !args.disallowReply && !ChatId.isSecret(chatId) && mode == MODE_MESSAGES && !(needHideAuthor || needRemoveCaptions) && args.messages[0].chatId != chatId) { + messageReplyIncluded = true; + + long singleSourceChatId = 0, singleSourceMediaGroupId = 0, contentfulMediaMessageId = 0; + for (int index = 0; index < args.messages.length; index++) { + TdApi.Message message = args.messages[index]; + if (!message.canBeRepliedInAnotherChat) { + messageReplyIncluded = false; + break; + } + if (index == 0) { + singleSourceChatId = message.chatId; + singleSourceMediaGroupId = message.mediaAlbumId; + } else if (singleSourceChatId != message.chatId || singleSourceMediaGroupId != message.mediaAlbumId || message.mediaAlbumId == 0) { + messageReplyIncluded = false; + break; + } + if (!Td.isEmpty(Td.textOrCaption(message.content))) { + if (contentfulMediaMessageId != 0) { + messageReplyIncluded = false; + break; + } + contentfulMediaMessageId = message.id; + } + } + + if (messageReplyIncluded) { + replyTo = new TdApi.InputMessageReplyToMessage(args.messages[0].chatId, contentfulMediaMessageId != 0 ? contentfulMediaMessageId : args.messages[0].id, null); + } + } + functions.addAll(TD.sendMessageText(chatId, 0, replyTo, sendOptions, new TdApi.InputMessageText(comment, null, false), tdlib.maxMessageTextLength())); } switch (mode) { case MODE_TEXT: { - functions.addAll(TD.sendMessageText(chatId, 0, 0, sendOptions, new TdApi.InputMessageText(args.text, false, false), tdlib.maxMessageTextLength())); + functions.addAll(TD.sendMessageText(chatId, 0, null, sendOptions, new TdApi.InputMessageText(args.text, null, false), tdlib.maxMessageTextLength())); break; } case MODE_MESSAGES: { - if (!TD.forwardMessages(chatId, 0, args.messages, needHideAuthor, needRemoveCaptions, sendOptions, functions)) + if (!messageReplyIncluded && !TD.forwardMessages(chatId, 0, args.messages, needHideAuthor, needRemoveCaptions, sendOptions, functions)) return; break; } case MODE_GAME: { - functions.add(new TdApi.SendMessage(chatId, 0, 0, sendOptions, null, new TdApi.InputMessageForwarded(args.botMessage.chatId, args.botMessage.id, args.withUserScore, null))); + functions.add(new TdApi.SendMessage(chatId, 0, null, sendOptions, null, new TdApi.InputMessageForwarded(args.botMessage.chatId, args.botMessage.id, args.withUserScore, null))); break; } case MODE_FILE: { - functions.add(new TdApi.SendMessage(chatId, 0, 0, sendOptions, null, new TdApi.InputMessageDocument(TD.createInputFile(args.filePath), null, false, null))); + functions.add(new TdApi.SendMessage(chatId, 0, null, sendOptions, null, new TdApi.InputMessageDocument(TD.createInputFile(args.filePath), null, false, null))); break; } case MODE_CONTACT: { - functions.add(new TdApi.SendMessage(chatId, 0, 0, sendOptions, null, new TdApi.InputMessageContact(new TdApi.Contact(args.contactUser.phoneNumber, args.contactUser.firstName, args.contactUser.lastName, null, args.botUserId)))); + functions.add(new TdApi.SendMessage(chatId, 0, null, sendOptions, null, new TdApi.InputMessageContact(new TdApi.Contact(args.contactUser.phoneNumber, args.contactUser.firstName, args.contactUser.lastName, null, args.botUserId)))); break; } case MODE_STICKER: { - functions.add(new TdApi.SendMessage(chatId, 0, 0, sendOptions, null, new TdApi.InputMessageSticker(new TdApi.InputFileId(args.sticker.sticker.id), null, 0, 0, null))); + functions.add(new TdApi.SendMessage(chatId, 0, null, sendOptions, null, new TdApi.InputMessageSticker(new TdApi.InputFileId(args.sticker.sticker.id), null, 0, 0, null))); break; } case MODE_CUSTOM: { @@ -3061,21 +3274,21 @@ private void sendMessages (boolean forceGoToChat, boolean isSingleTap, @Nullable break; } case MODE_CUSTOM_CONTENT: { - functions.addAll(TD.sendMessageText(chatId, 0,0, sendOptions, args.customContent, tdlib.maxMessageTextLength())); + functions.addAll(TD.sendMessageText(chatId, 0, null, sendOptions, args.customContent, tdlib.maxMessageTextLength())); break; } case MODE_TELEGRAM_FILES: { TdApi.FormattedText formattedCaption = StringUtils.isEmpty(args.telegramCaption) ? null : TD.newText(args.telegramCaption); TdApi.FormattedText messageCaption = formattedCaption != null && formattedCaption.text.codePointCount(0, formattedCaption.text.length()) <= tdlib.maxCaptionLength() ? formattedCaption : null; if (formattedCaption != null && messageCaption == null) { - functions.addAll(TD.sendMessageText(chatId, 0, 0, sendOptions, new TdApi.InputMessageText(formattedCaption, false, false), tdlib.maxMessageTextLength())); + functions.addAll(TD.sendMessageText(chatId, 0, null, sendOptions, new TdApi.InputMessageText(formattedCaption, null, false), tdlib.maxMessageTextLength())); } for (MediaItem item : args.telegramFiles) { boolean last = item == args.telegramFiles[args.telegramFiles.length - 1]; TdApi.InputMessageContent content = item.createShareContent(last ? messageCaption : null); if (content == null) return; - functions.add(new TdApi.SendMessage(chatId, 0, 0, sendOptions, null, content)); + functions.add(new TdApi.SendMessage(chatId, 0, null, sendOptions, null, content)); } break; } @@ -3170,7 +3383,8 @@ private void shareLink () { isUser = false; break; default: - throw new UnsupportedOperationException(args.messages[0].senderId.toString()); + Td.assertMessageSender_439d4c9c(); + throw Td.unsupported(args.messages[0].senderId); } tdlib.getMessageLink(args.messages[0], args.messages.length > 1, args.messageThreadId != 0, link -> Intents.shareText(Lang.getString(args.messageThreadId != 0 && isUser ? R.string.ShareTextComment : isUser || !tdlib.isChannel(args.messages[0].chatId) ? R.string.ShareTextMessage : R.string.ShareTextPost, link.url, name)) @@ -3237,10 +3451,64 @@ private void copyLink () { } } + private @Nullable PopupLayout folderSelectorLayout; + + private void showFolderSelector () { + if (headerView == null) + return; + MenuMoreWrap menu = new MenuMoreWrap(context, /* scrollable */ true) { + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + int overrideHeightMeasureSpec; + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightSpecMode == MeasureSpec.UNSPECIFIED) { + overrideHeightMeasureSpec = heightMeasureSpec; + } else { + int heightSpecSize = Math.max(MeasureSpec.getSize(heightMeasureSpec) - Math.round(getTranslationY()), 0); + overrideHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSpecSize, heightSpecMode); + } + super.onMeasure(widthMeasureSpec, overrideHeightMeasureSpec); + } + }; + menu.init(getThemeListeners(), null); + menu.addItem(0, Lang.getString(R.string.CategoryMain), R.drawable.baseline_forum_24, null, v -> { + PopupLayout popupLayout = PopupLayout.parentOf(v); + popupLayout.hideWindow(true); + displayChatList(Lang.getString(R.string.CategoryMain), ChatPosition.CHAT_LIST_MAIN); + }); + View.OnClickListener onItemClickListener = v -> { + PopupLayout popupLayout = PopupLayout.parentOf(v); + popupLayout.hideWindow(true); + TdApi.ChatFolderInfo chatFolderInfo = (TdApi.ChatFolderInfo) v.getTag(); + displayChatList(chatFolderInfo.title, new TdApi.ChatListFolder(chatFolderInfo.id)); + }; + for (TdApi.ChatFolderInfo chatFolderInfo : tdlib.chatFolders()) { + View itemView = menu.addItem(View.NO_ID, chatFolderInfo.title, TD.findFolderIcon(chatFolderInfo.icon, R.drawable.baseline_folder_24), /* icon */ null, onItemClickListener); + itemView.setTag(chatFolderInfo); + } + menu.setAnchorMode(MenuMoreWrap.ANCHOR_MODE_HEADER); + menu.setTranslationY(headerView.getTranslationY()); + folderSelectorLayout = new PopupLayout(context); + folderSelectorLayout.init(true); + folderSelectorLayout.setNeedRootInsets(); + folderSelectorLayout.setOverlayStatusBar(true); + folderSelectorLayout.setDismissListener((popup) -> folderSelectorLayout = null); + folderSelectorLayout.showMoreView(menu); + } + + private void hideFolderSelector () { + if (folderSelectorLayout != null) { + folderSelectorLayout.hideWindow(isFocused()); + folderSelectorLayout = null; + } + } + @Override public void destroy () { super.destroy(); - list.unsubscribeFromUpdates(this); + for (TdlibChatListSlice list : listByChatList.values()) { + list.unsubscribeFromUpdates(this); + } Views.destroyRecyclerView(recyclerView); TGLegacyManager.instance().removeEmojiListener(adapter); cancelDownloadingFiles(); @@ -3286,8 +3554,8 @@ public void onInputSpansChanged (InputView view) { private void setTextFormattingLayoutVisible (boolean visible) { textFormattingVisible = visible; if (emojiLayout != null && textFormattingLayout != null) { - textFormattingLayout.setVisibility(visible ? View.VISIBLE: View.GONE); - emojiLayout.optimizeForDisplayTextFormattingLayout(!visible); + textFormattingLayout.setVisibility(visible ? View.VISIBLE : View.GONE); + emojiLayout.optimizeForDisplayTextFormattingLayout(visible); if (visible) { textFormattingLayout.checkButtonsActive(false); } @@ -3301,6 +3569,6 @@ private void closeTextFormattingKeyboard () { } public @DrawableRes int getTargetIcon () { - return (textInputHasSelection || (textFormattingVisible && emojiShown)) ? R.drawable.baseline_format_text_24: R.drawable.deproko_baseline_insert_emoticon_26; + return (textInputHasSelection || (textFormattingVisible && emojiShown)) ? R.drawable.baseline_format_text_24 : R.drawable.deproko_baseline_insert_emoticon_26; } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SharedBaseController.java b/app/src/main/java/org/thunderdog/challegram/ui/SharedBaseController.java index 71a95832ae..6a11d918e7 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SharedBaseController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SharedBaseController.java @@ -28,7 +28,6 @@ import androidx.recyclerview.widget.RecyclerView; import org.drinkless.tdlib.TdApi; -import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.MediaCollectorDelegate; import org.thunderdog.challegram.core.Lang; @@ -44,6 +43,7 @@ import org.thunderdog.challegram.telegram.MessageListener; import org.thunderdog.challegram.telegram.TGLegacyManager; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; @@ -155,9 +155,12 @@ public void setParent (@NonNull MessagesController parent) { tdlib.listeners().subscribeToMessageUpdates(chatId, this); } + private TdlibMessageViewer.Viewport messageViewport; + @SuppressLint("InflateParams") @Override protected final View onCreateView (Context context) { + messageViewport = tdlib.messageViewer().createViewport(new TdApi.MessageSourceSearch(), this); recyclerView = (MediaRecyclerView) Views.inflate(context(), R.layout.recycler_sharedmedia, null); recyclerView.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); addThemeInvalidateListener(recyclerView); @@ -168,6 +171,7 @@ protected final View onCreateView (Context context) { recyclerView.setHasFixedSize(true); recyclerView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); recyclerView.setItemAnimator(null); // new CustomItemAnimator(Anim.DECELERATE_INTERPOLATOR, 180l)); + tdlib.ui().attachViewportToRecyclerView(messageViewport, recyclerView); adapter = new SettingsAdapter(this, needsDefaultOnClick() ? this : null, this) { @Override protected void setInfo (ListItem item, int position, ListInfoView infoView) { @@ -696,8 +700,7 @@ protected final void processData (final String query, final long offset, final T break; } default: { - Log.unexpectedTdlibResponse(object, TdApi.GetChats.class, TdApi.Chats.class); - return; + throw new UnsupportedOperationException(object.toString()); } } final long nextOffsetFinal = nextOffset; @@ -1367,6 +1370,9 @@ public void destroy () { if (alternateParent != null) { tdlib.listeners().unsubscribeFromMessageUpdates(chatId, this); } + if (messageViewport != null) { + messageViewport.performDestroy(); + } TGLegacyManager.instance().removeEmojiListener(adapter); Views.destroyRecyclerView(recyclerView); } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SharedChatsController.java b/app/src/main/java/org/thunderdog/challegram/ui/SharedChatsController.java index 2f338ff012..183c4781c9 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SharedChatsController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SharedChatsController.java @@ -129,13 +129,13 @@ protected boolean probablyHasEmoji () { @Override protected void onCreateView (Context context, MediaRecyclerView recyclerView, SettingsAdapter adapter) { super.onCreateView(context, recyclerView, adapter); - tdlib.cache().subscribeToAnyUpdates(this); + tdlib.cache().subscribeForGlobalUpdates(this); } @Override public void destroy () { super.destroy(); - tdlib.cache().unsubscribeFromAnyUpdates(this); + tdlib.cache().unsubscribeFromGlobalUpdates(this); } // Updates for texts diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SharedCommonController.java b/app/src/main/java/org/thunderdog/challegram/ui/SharedCommonController.java index 5a7ad272b5..d76d5f03f4 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SharedCommonController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SharedCommonController.java @@ -167,7 +167,7 @@ protected InlineResult parseObject (TdApi.Object object) { if (result != null) { result.setQueryId(message.id); result.setDate(message.date); - if (result instanceof InlineResultCommon && message.content.getConstructor() == TdApi.MessageAudio.CONSTRUCTOR) { + if (result instanceof InlineResultCommon && Td.isAudio(message.content)) { ((InlineResultCommon) result).setIsTrack(false); } } @@ -247,6 +247,7 @@ public TGPlayerController.PlayList buildPlayList (TdApi.Message fromMessage) { int foundIndex = -1; int desiredType; + //noinspection SwitchIntDef switch (fromMessage.content.getConstructor()) { case TdApi.MessageAudio.CONSTRUCTOR: desiredType = InlineResult.TYPE_AUDIO; diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SharedMediaController.java b/app/src/main/java/org/thunderdog/challegram/ui/SharedMediaController.java index 74973e9331..b9f4e610c5 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/SharedMediaController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/SharedMediaController.java @@ -50,6 +50,7 @@ import me.vkryl.android.util.ClickHelper; import me.vkryl.core.collection.IntList; +import me.vkryl.td.Td; public class SharedMediaController extends SharedBaseController implements ClickHelper.Delegate, ForceTouchView.ActionListener { public SharedMediaController (Context context, Tdlib tdlib) { @@ -294,7 +295,7 @@ protected TdApi.SearchMessagesFilter provideSearchFilter () { @Override protected MediaItem parseObject (TdApi.Object object) { TdApi.Message message = (TdApi.Message) object; - if (message.selfDestructTime > 0 && message.selfDestructTime <= 60) { + if (Td.isSecret(message.content)) { return null; } MediaItem item = MediaItem.valueOf(context(), tdlib, message); @@ -451,7 +452,7 @@ public boolean onLongPressRequestedAt (View v, float x, float y) { if (item != null && item.getViewType() == ListItem.TYPE_SMALL_MEDIA) { MediaItem mediaItem = (MediaItem) item.getData(); TdApi.Message message = mediaItem.getMessage(); - if (message == null || message.content.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR) { + if (message == null || Td.isVideoNote(message.content)) { return super.onLongClick(v); } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/StickersController.java b/app/src/main/java/org/thunderdog/challegram/ui/StickersController.java index 20193260a7..318ddc7a55 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/StickersController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/StickersController.java @@ -18,6 +18,7 @@ import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.collection.LongSparseArray; @@ -32,11 +33,13 @@ import org.thunderdog.challegram.component.sticker.StickerSetWrap; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TGStickerSetInfo; +import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.telegram.StickersListener; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Strings; import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.util.text.Highlight; import org.thunderdog.challegram.v.CustomRecyclerView; import org.thunderdog.challegram.widget.DoubleTextView; import org.thunderdog.challegram.widget.NonMaterialButton; @@ -46,23 +49,26 @@ import me.vkryl.android.AnimatorUtils; import me.vkryl.core.ArrayUtils; +import me.vkryl.core.StringUtils; import me.vkryl.core.collection.LongList; import me.vkryl.core.collection.LongSparseIntArray; import me.vkryl.td.Td; -public class StickersController extends RecyclerViewController implements Client.ResultHandler, View.OnClickListener, StickersListener { +public class StickersController extends RecyclerViewController implements View.OnClickListener, StickersListener { public static final int MODE_STICKERS = 0; public static final int MODE_STICKERS_ARCHIVED = 1; public static final int MODE_MASKS = 2; public static class Args { public final int mode; + public final boolean isEmoji; public final boolean doNotLoad; public @Nullable ArrayList stickerSets; - public Args (int mode, boolean doNotLoad) { + public Args (int mode, boolean isEmoji, boolean doNotLoad) { this.mode = mode; + this.isEmoji = isEmoji; this.doNotLoad = doNotLoad; } @@ -87,60 +93,26 @@ public int getId () { } private int mode; + private boolean isEmoji; private boolean doNotLoad; + private @Nullable ArrayList stickerSetsToSets; @Override public void setArguments (Args args) { super.setArguments(args); this.mode = args.mode; + this.isEmoji = args.isEmoji; this.doNotLoad = args.doNotLoad; - this.stickerSets = args.stickerSets; + this.stickerSetsToSets = args.stickerSets; } - private SettingsAdapter adapter; - - private static final int STATE_NONE = 0; - private static final int STATE_IN_PROGRESS = 1; - private static final int STATE_DONE = 2; - - private LongSparseIntArray currentStates; - - private int getState (TGStickerSetInfo stickerSet) { - return currentStates != null ? currentStates.get(stickerSet.getId(), STATE_NONE) : STATE_NONE; - } - - private void setState (long setId, int state) { - if (currentStates == null) { - currentStates = new LongSparseIntArray(); - } - currentStates.put(setId, state); - adapter.updateStickerSetById(setId); - } + private AdapterForAdapter adapterForAdapter; @Override protected void onCreateView (Context context, CustomRecyclerView recyclerView) { - adapter = new SettingsAdapter(this) { - @Override - protected void setStickerSet (ListItem item, int position, DoubleTextView group, boolean isArchived, boolean isUpdate) { - TGStickerSetInfo stickerSet; - if (isArchived && archivedSets != null) { - stickerSet = archivedSets.get(position - getArchivedStartIndex()); - } else if (stickerSets != null) { - stickerSet = stickerSets.get(position - getStartIndex()); - } else { - return; - } - group.setStickerSet(stickerSet); - if (isArchived) { - NonMaterialButton button = group.getButton(); - if (button != null) { - int state = getState(stickerSet); - button.setInProgress(state == STATE_IN_PROGRESS, isUpdate); - button.setIsDone(state == STATE_DONE, isUpdate); - } - } - } - }; + adapterForAdapter = new AdapterForAdapter(this, mode, isEmoji); + adapterForAdapter.setStickerSets(stickerSetsToSets); + adapterForAdapter.search(searchRequest); if (mode == MODE_STICKERS || mode == MODE_MASKS) { if (mode == MODE_STICKERS) { @@ -148,12 +120,9 @@ protected void setStickerSet (ListItem item, int position, DoubleTextView group, } ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.Callback() { @Override - public int getMovementFlags (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - if (stickerSets == null) { - return 0; - } - int position = viewHolder.getAdapterPosition(); - if (position != -1 && position >= getStartIndex() && stickerSets != null && position < getStartIndex() + stickerSets.size()) { + public int getMovementFlags (@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + int viewType = viewHolder.getItemViewType(); + if (viewType == ListItem.TYPE_STICKER_SET && !adapterForAdapter.inSearchMode()) { int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; return makeMovementFlags(dragFlags, 0); } @@ -169,19 +138,22 @@ public boolean isLongPressDragEnabled () { private int dragTo = -1; @Override - public void onMoved (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int fromPos, RecyclerView.ViewHolder target, int toPos, int x, int y) { + public void onMoved (@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, int fromPos, @NonNull RecyclerView.ViewHolder target, int toPos, int x, int y) { super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y); viewHolder.itemView.invalidate(); target.itemView.invalidate(); } @Override - public boolean onMove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { + public boolean onMove (@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { int fromPosition = viewHolder.getAdapterPosition(); int toPosition = target.getAdapterPosition(); - if (stickerSets != null && fromPosition >= getStartIndex() && fromPosition < getStartIndex() + stickerSets.size() && toPosition >= getStartIndex() && toPosition < getStartIndex() + stickerSets.size()) { - moveStickerSet(fromPosition - getStartIndex(), toPosition - getStartIndex()); + int fromIndex = adapterForAdapter.getIndexFromPosition(fromPosition); + int toIndex = adapterForAdapter.getIndexFromPosition(toPosition); + + if (stickerSetsToSets != null && fromIndex != -1 && toIndex != -1) { + adapterForAdapter.moveStickerSet(fromIndex, toIndex); if (dragFrom == -1) { dragFrom = fromPosition; @@ -194,23 +166,19 @@ public boolean onMove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHo return false; } - private void reallyMoved (int from, int to) { - saveStickersOrder(); - } - @Override - public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) { - reallyMoved(dragFrom, dragTo); + adapterForAdapter.saveStickersOrder(); } dragFrom = dragTo = -1; } @Override - public void onSwiped (RecyclerView.ViewHolder viewHolder, int direction) { + public void onSwiped (@NonNull RecyclerView.ViewHolder viewHolder, int direction) { } }); @@ -220,12 +188,12 @@ public void onSwiped (RecyclerView.ViewHolder viewHolder, int direction) { if (mode == MODE_STICKERS_ARCHIVED) { recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onScrolled (RecyclerView recyclerView, int dx, int dy) { - if (!isLoading && ((stickerSets != null && !stickerSets.isEmpty()) || (archivedSets != null && !archivedSets.isEmpty()))) { + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + if (!isLoading && ((adapterForAdapter.stickerSets != null && !adapterForAdapter.stickerSets.isEmpty()))) { int position = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition(); if (position != RecyclerView.NO_POSITION) { position += 10; - if (position >= adapter.getItemCount() - 1) { + if (position >= adapterForAdapter.adapter.getItemCount() - 1) { loadData(true); } } @@ -234,8 +202,8 @@ public void onScrolled (RecyclerView recyclerView, int dx, int dy) { }); } - if (stickerSets != null) { - buildCells(); + if (stickerSetsToSets != null) { + adapterForAdapter.buildItems(); } else if (!doNotLoad) { loadData(false); } @@ -244,23 +212,17 @@ public void onScrolled (RecyclerView recyclerView, int dx, int dy) { tdlib.listeners().subscribeToStickerUpdates(this); } - recyclerView.setAdapter(adapter); + recyclerView.setAdapter(adapterForAdapter.adapter); } - private void saveStickersOrder () { - if (stickerSets == null || stickerSets.isEmpty()) { - return; - } - - long[] stickerSetIds = new long[stickerSets.size()]; - int i = 0; - for (TGStickerSetInfo info : stickerSets) { - stickerSetIds[i++] = info.getId(); - } - tdlib.client().send(new TdApi.ReorderInstalledStickerSets(getStickerType(), stickerSetIds), tdlib.okHandler()); + private TdApi.StickerType getStickerType () { + return getStickerType(mode, isEmoji); } - private TdApi.StickerType getStickerType () { + private static TdApi.StickerType getStickerType (int mode, boolean isEmoji) { + if (isEmoji) { + return new TdApi.StickerTypeCustomEmoji(); + } switch (mode) { case MODE_STICKERS: case MODE_STICKERS_ARCHIVED: @@ -276,7 +238,7 @@ private TdApi.StickerType getStickerType () { public void onParentFocus () { if (!parentFocusApplied) { parentFocusApplied = true; - getRecyclerView().setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180l)); + getRecyclerView().setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180L)); } } @@ -290,8 +252,8 @@ public void destroy () { public void onInstalledStickerSetsUpdated (final long[] stickerSetIds, TdApi.StickerType stickerType) { if (Td.equalsTo(getStickerType(), stickerType)) { runOnUiThreadOptional(() -> { - if (!isLoading && stickerSets != null) { - changeStickerSets(stickerSetIds); + if (!isLoading) { + adapterForAdapter.changeStickerSets(stickerSetIds); } }); } @@ -305,11 +267,17 @@ private static boolean isMasks (TdApi.StickerType type) { public void onStickerSetArchived (final TdApi.StickerSetInfo stickerSet) { if ((mode == MODE_MASKS && isMasks(stickerSet.stickerType)) || (mode == MODE_STICKERS_ARCHIVED && !isMasks(stickerSet.stickerType))) { tdlib.ui().post(() -> { - if (!isDestroyed() && !isLoading && stickerSets != null) { + if (!isDestroyed() && !isLoading) { + TdApi.StickerSetInfo newRawInfo = Td.copyOf(stickerSet); if (mode == MODE_MASKS) { - addArchivedMasks(stickerSet); + newRawInfo.isInstalled = true; + newRawInfo.isArchived = true; + TGStickerSetInfo info = new TGStickerSetInfo(tdlib, newRawInfo); + adapterForAdapter.addStickerSet(info, adapterForAdapter.getInstalledSetsInDatasetCount()); } else { - addArchivedSet(stickerSet); + newRawInfo.isArchived = true; + TGStickerSetInfo info = new TGStickerSetInfo(tdlib, newRawInfo); + adapterForAdapter.addStickerSet(info, 0); } } }); @@ -320,11 +288,11 @@ public void onStickerSetArchived (final TdApi.StickerSetInfo stickerSet) { public void onStickerSetRemoved (TdApi.StickerSetInfo stickerSet) { if ((mode == MODE_MASKS && isMasks(stickerSet.stickerType)) || (mode == MODE_STICKERS_ARCHIVED && !isMasks(stickerSet.stickerType))) { tdlib.ui().post(() -> { - if (!isDestroyed() && !isLoading && stickerSets != null) { + if (!isDestroyed() && !isLoading) { if (mode == MODE_MASKS) { // TODO? } else { - removeStickerSet(stickerSet.id); + adapterForAdapter.removeStickerSet(stickerSet.id); } } }); @@ -335,11 +303,11 @@ public void onStickerSetRemoved (TdApi.StickerSetInfo stickerSet) { public void onStickerSetInstalled (TdApi.StickerSetInfo stickerSet) { if ((mode == MODE_MASKS && isMasks(stickerSet.stickerType)) || (mode == MODE_STICKERS_ARCHIVED && !isMasks(stickerSet.stickerType))) { tdlib.ui().post(() -> { - if (!isDestroyed() && !isLoading && stickerSets != null) { + if (!isDestroyed()) { if (mode == MODE_MASKS) { // TODO? } else { - removeStickerSet(stickerSet.id); + adapterForAdapter.removeStickerSet(stickerSet.id); } } }); @@ -349,37 +317,30 @@ public void onStickerSetInstalled (TdApi.StickerSetInfo stickerSet) { private boolean isLoading, isLoadingMore, endReached; private void loadData (boolean isMore) { - if (!isLoading) { - if (isMore && endReached) { - return; - } - isLoading = true; - isLoadingMore = isMore; - switch (mode) { - case MODE_STICKERS: - case MODE_MASKS: { - if (!isMore) { - tdlib.client().send(new TdApi.GetInstalledStickerSets(getStickerType()), this); - } - break; - } - case MODE_STICKERS_ARCHIVED: { - long offsetStickerSetId; - int limit; - if (isMore) { - if (stickerSets == null || stickerSets.isEmpty()) { - return; - } - offsetStickerSetId = stickerSets.get(stickerSets.size() - 1).getId(); - limit = 100; - } else { - offsetStickerSetId = 0; - limit = Screen.calculateLoadingItems(Screen.dp(72f), 20); - } - tdlib.client().send(new TdApi.GetArchivedStickerSets(getStickerType(), offsetStickerSetId, limit), this); - break; + if (isLoading || isMore && endReached) return; + + isLoading = true; + isLoadingMore = isMore; + TdApi.Function function = null; + if ((mode == MODE_STICKERS || mode == MODE_MASKS) && !isMore) { + function = new TdApi.GetInstalledStickerSets(getStickerType()); + } else if (mode == MODE_STICKERS_ARCHIVED) { + long offsetStickerSetId; + int limit; + if (isMore) { + if (adapterForAdapter.stickerSets == null || adapterForAdapter.stickerSets.isEmpty()) { + return; } + offsetStickerSetId = adapterForAdapter.stickerSets.get(adapterForAdapter.stickerSets.size() - 1).getId(); + limit = 100; + } else { + offsetStickerSetId = 0; + limit = Screen.calculateLoadingItems(Screen.dp(72f), 20); } + function = new TdApi.GetArchivedStickerSets(getStickerType(), offsetStickerSetId, limit); + } + if (function != null) { + tdlib.client().send(function, this::onStickerSetsResult); } } @@ -388,32 +349,32 @@ public void onClick (View v) { final int viewId = v.getId(); if (viewId == R.id.btn_stickerSetInfo) { ListItem item = (ListItem) v.getTag(); - TGStickerSetInfo info = findStickerSetById(item.getLongId()); + TGStickerSetInfo info = adapterForAdapter.findStickerSetById(item.getLongId()); if (info != null) { - if (mode == MODE_STICKERS_ARCHIVED && currentStates != null && currentStates.get(info.getId(), STATE_NONE) == STATE_DONE) { + if (mode == MODE_STICKERS_ARCHIVED && adapterForAdapter.getState(info) == AdapterForAdapter.STATE_DONE) { return; } StickerSetWrap.showStickerSet(this, info.getInfo()).setIsOneShot(); } } else if (viewId == R.id.btn_double) { ListItem item = (ListItem) ((ViewGroup) v.getParent()).getTag(); - final TGStickerSetInfo info = findStickerSetById(item.getLongId()); + final TGStickerSetInfo info = adapterForAdapter.findStickerSetById(item.getLongId()); if (info != null) { - int state = getState(info); - if (state == STATE_NONE) { - setState(info.getId(), STATE_IN_PROGRESS); + int state = adapterForAdapter.getState(info); + if (state == AdapterForAdapter.STATE_NONE) { + adapterForAdapter.setState(info.getId(), AdapterForAdapter.STATE_IN_PROGRESS); tdlib.client().send(new TdApi.ChangeStickerSet(info.getId(), true, false), object -> tdlib.ui().post(() -> { if (!isDestroyed()) { boolean isOk = object.getConstructor() == TdApi.Ok.CONSTRUCTOR; if (isOk) { info.setIsInstalled(); } - setState(info.getId(), isOk ? STATE_DONE : STATE_NONE); + adapterForAdapter.setState(info.getId(), isOk ? AdapterForAdapter.STATE_DONE : AdapterForAdapter.STATE_NONE); if (isOk) { if (mode == MODE_STICKERS_ARCHIVED) { - UI.post(() -> removeArchivedStickerSet(info), 1500); - } else if (currentStates != null) { - currentStates.delete(info.getId()); + UI.post(() -> adapterForAdapter.removeStickerSet(info.getId()), 1500); + } else if (adapterForAdapter.currentStates != null) { + adapterForAdapter.currentStates.delete(info.getId()); } } } @@ -423,181 +384,12 @@ public void onClick (View v) { } } - private @Nullable TGStickerSetInfo findStickerSetById (long stickerSetId) { - if (stickerSets != null && !stickerSets.isEmpty()) { - for (TGStickerSetInfo info : stickerSets) { - if (info.getId() == stickerSetId) { - return info; - } - } - } - if (archivedSets != null && !archivedSets.isEmpty()) { - for (TGStickerSetInfo info : archivedSets) { - if (info.getId() == stickerSetId) { - return info; - } - } - } - return null; - } - - private @Nullable ArrayList stickerSets; - private @Nullable ArrayList archivedSets; - - @UiThread - public void addMoreStickerSets (ArrayList stickerSets, @Nullable ArrayList archivedSets) { - switch (mode) { - case MODE_STICKERS_ARCHIVED: { - if (this.stickerSets == null || this.stickerSets.isEmpty() || stickerSets.isEmpty()) { - return; - } - this.stickerSets.addAll(stickerSets); - List items = adapter.getItems(); - int startIndex = items.size() - 1; - ListItem shadow = items.remove(startIndex); - for (TGStickerSetInfo info : stickerSets) { - info.setBoundList(this.stickerSets); - items.add(new ListItem(ListItem.TYPE_ARCHIVED_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setLongId(info.getId())); - } - items.add(shadow); - adapter.notifyItemRangeInserted(startIndex, stickerSets.size()); - break; - } - } - } - @UiThread - public void setStickerSets (ArrayList stickerSets, @Nullable ArrayList archivedSets) { - this.stickerSets = stickerSets; - this.archivedSets = archivedSets; - buildCells(); + public void setStickerSets (ArrayList stickerSets) { + this.adapterForAdapter.setStickerSets(stickerSets); } - private void buildCells () { - ArrayList items = new ArrayList<>(Math.max(0, stickerSets != null ? stickerSets.size() * 2 - 1 : 0)); - - if (!stickerSets.isEmpty() || (archivedSets != null && !archivedSets.isEmpty())) { - if (mode == MODE_STICKERS_ARCHIVED) { - items.add(new ListItem(ListItem.TYPE_EMPTY_OFFSET_SMALL)); - items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, Lang.getString(R.string.ArchivedStickersInfo, Strings.buildCounter(tdlib.getInstalledStickerSetLimit())), false)); - items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); - } else if (mode == MODE_MASKS) { - items.add(new ListItem(ListItem.TYPE_EMPTY_OFFSET_SMALL)); - items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.MasksHint)); - - if (!stickerSets.isEmpty()) { - items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); - } - } - - if (mode == MODE_STICKERS_ARCHIVED) { - for (TGStickerSetInfo info : stickerSets) { - items.add(new ListItem(ListItem.TYPE_ARCHIVED_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setLongId(info.getId())); - } - } else { - for (TGStickerSetInfo info : stickerSets) { - items.add(new ListItem(ListItem.TYPE_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setLongId(info.getId())); - } - } - if (!stickerSets.isEmpty()) { - items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); - } - - if (mode == MODE_MASKS && archivedSets != null && !archivedSets.isEmpty()) { - items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.Archived)); - items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); - for (TGStickerSetInfo info : archivedSets) { - items.add(new ListItem(ListItem.TYPE_ARCHIVED_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setLongId(info.getId())); - } - items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); - } - - if (mode == MODE_STICKERS) { - items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, Lang.getString(R.string.ArchivedStickersInfo, Strings.buildCounter(tdlib.getInstalledStickerSetLimit())), false)); - } - } else if (mode == MODE_STICKERS_ARCHIVED) { - items.add(new ListItem(ListItem.TYPE_EMPTY, 0, 0, Lang.getString(R.string.ArchivedStickersInfo, Strings.buildCounter(tdlib.getInstalledStickerSetLimit())), false)); - } else { - items.add(new ListItem(ListItem.TYPE_EMPTY, 0, 0, mode == MODE_STICKERS ? R.string.NoStickerSets : R.string.NoMasks)); - } - - adapter.setItems(items, false); - } - - private void removeArchivedStickerSet (TGStickerSetInfo info) { - if (stickerSets == null) { - return; - } - if (currentStates != null) { - currentStates.delete(info.getId()); - } - int i = indexOfStickerSet(info.getId()); - if (i != -1) { - stickerSets.remove(i); - if (stickerSets.size() == 0) { - buildCells(); - } else { - adapter.getItems().remove(3 + i); - adapter.notifyItemRemoved(3 + i); - } - } - } - - private void addArchivedSet (TdApi.StickerSetInfo rawInfo) { - if (stickerSets == null) { - return; - } - TdApi.StickerSetInfo newRawInfo = Td.copyOf(rawInfo); - newRawInfo.isArchived = true; - TGStickerSetInfo info = new TGStickerSetInfo(tdlib, newRawInfo); - info.setBoundList(stickerSets); - stickerSets.add(0, info); - - if (stickerSets.size() == 1) { - buildCells(); - } else { - adapter.getItems().add(3, new ListItem(ListItem.TYPE_ARCHIVED_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setLongId(info.getId())); - adapter.notifyItemInserted(3); - } - } - - private void addArchivedMasks (TdApi.StickerSetInfo rawInfo) { - if (archivedSets == null) { - archivedSets = new ArrayList<>(); - } else { - for (TGStickerSetInfo prevInfo : archivedSets) { - if (prevInfo.getId() == rawInfo.id) { - return; - } - } - } - - TdApi.StickerSetInfo newRawInfo = Td.copyOf(rawInfo); - newRawInfo.isInstalled = true; - newRawInfo.isArchived = true; - TGStickerSetInfo info = new TGStickerSetInfo(tdlib, newRawInfo); - info.setBoundList(archivedSets); - - int startIndex = getArchivedStartIndex(); - - ListItem item = new ListItem(ListItem.TYPE_ARCHIVED_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setLongId(info.getId()); - archivedSets.add(0, info); - - if (archivedSets.size() == 1) { - int index = adapter.getItems().size(); - adapter.getItems().add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.Archived)); - adapter.getItems().add(new ListItem(ListItem.TYPE_SHADOW_TOP)); - adapter.getItems().add(item); - adapter.getItems().add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); - adapter.notifyItemRangeInserted(index, 4); - } else { - adapter.getItems().add(startIndex, item); - adapter.notifyItemInserted(startIndex); - } - } - - @Override - public void onResult (final TdApi.Object object) { + private void onStickerSetsResult (final TdApi.Object object) { switch (object.getConstructor()) { case TdApi.StickerSets.CONSTRUCTOR: { TdApi.StickerSetInfo[] sets = ((TdApi.StickerSets) object).sets; @@ -639,27 +431,22 @@ public void onResult (final TdApi.Object object) { if (mode == MODE_MASKS) { tdlib.client().send(new TdApi.GetArchivedStickerSets(getStickerType(), 0, 100), object1 -> { - final ArrayList archivedSets; - if (object1.getConstructor() == TdApi.StickerSets.CONSTRUCTOR && ((TdApi.StickerSets) object1).sets.length > 0) { TdApi.StickerSetInfo[] stickerSets1 = ((TdApi.StickerSets) object1).sets; - archivedSets = new ArrayList<>(stickerSets1.length); for (TdApi.StickerSetInfo stickerSet : stickerSets1) { TGStickerSetInfo info = new TGStickerSetInfo(tdlib, stickerSet); - info.setBoundList(archivedSets); - archivedSets.add(info); + info.setBoundList(stickerSets); + stickerSets.add(info); } - } else { - archivedSets = null; } tdlib.ui().post(() -> { if (!isDestroyed()) { isLoading = false; if (isLoadingMore) { - addMoreStickerSets(stickerSets, archivedSets); + adapterForAdapter.addMoreStickerSets(stickerSets); } else { - setStickerSets(stickerSets, archivedSets); + setStickerSets(stickerSets); } } }); @@ -669,9 +456,9 @@ public void onResult (final TdApi.Object object) { if (!isDestroyed()) { isLoading = false; if (isLoadingMore) { - addMoreStickerSets(stickerSets, null); + adapterForAdapter.addMoreStickerSets(stickerSets); } else { - setStickerSets(stickerSets, null); + setStickerSets(stickerSets); } } }); @@ -691,264 +478,606 @@ public void onResult (final TdApi.Object object) { } } - private int indexOfStickerSet (long setId) { - if (stickerSets == null) { - return -1; - } - int i = 0; - for (TGStickerSetInfo info : stickerSets) { - if (info.getId() == setId) { - return i; - } - i++; - } - return -1; - } - private int getStartIndex () { - return stickerSets == null || mode == MODE_STICKERS ? 0 : stickerSets.isEmpty() ? 1 : 3; - } - private int getArchivedStartIndex () { - return getStartIndex() + (stickerSets != null ? stickerSets.size() + 3 : 1); - } - private void moveStickerSet (int oldPosition, int newPosition) { - if (oldPosition == newPosition || stickerSets == null) { - return; + + /* Search */ + + private String searchRequest; + + public void search (String request) { + this.searchRequest = request; + if (getWrapUnchecked() != null) { + this.adapterForAdapter.search(request); + this.adapterForAdapter.buildItems(); } + } - ArrayUtils.move(stickerSets, oldPosition, newPosition); + public class AdapterForAdapter { + private final Tdlib tdlib; + private final SettingsAdapter adapter; + private final boolean isEmoji; + private final int mode; + private final ViewController context; + + private @Nullable ArrayList stickerSets = null; + private @NonNull ArrayList foundStickerSets = new ArrayList<>(); + private @Nullable String searchRequest; + private int itemsStartIndex = -1; + private int itemsBreakIndex = -1; + private int itemsBreakSize = -1; + + public AdapterForAdapter (ViewController context, int mode, boolean isEmoji) { + this.context = context; + this.adapter = new SettingsAdapter(context) { + @Override + protected void setStickerSet (ListItem item, int position, DoubleTextView group, boolean isArchived, boolean isUpdate) { + bind(item, position, group, isArchived, isUpdate); + } + }; + this.isEmoji = isEmoji; + this.tdlib = context.tdlib(); + this.mode = mode; + } - oldPosition += getStartIndex(); - newPosition += getStartIndex(); + public void buildItems () { + ArrayList stickerSets = getActualStickerSetsList(); + ArrayList items = new ArrayList<>(Math.max(1, stickerSets != null ? stickerSets.size() * 2 - 1 : 0)); + itemsStartIndex = -1; + itemsBreakIndex = -1; + itemsBreakSize = -1; - int firstVisiblePosition = ((LinearLayoutManager) getRecyclerView().getLayoutManager()).findFirstVisibleItemPosition(); - View topView = getRecyclerView().getLayoutManager().findViewByPosition(firstVisiblePosition); - int offset = topView != null ? topView.getTop() : 0; + if (this.stickerSets == null) { + items.add(new ListItem(ListItem.TYPE_PROGRESS)); + adapter.setItems(items, false); + return; + } - adapter.moveItem(oldPosition, newPosition, true); + if (stickerSets == null || stickerSets.isEmpty()) { + if (mode == MODE_STICKERS_ARCHIVED) { + items.add(new ListItem(ListItem.TYPE_EMPTY, 0, 0, Lang.getString(!isEmoji ? R.string.ArchivedStickersInfo : R.string.ArchivedEmojiInfo, Strings.buildCounter(tdlib.getInstalledStickerSetLimit())), false)); + } else { + items.add(new ListItem(ListItem.TYPE_EMPTY, 0, 0, mode == MODE_STICKERS ? R.string.NoStickerSets : R.string.NoMasks)); + } + adapter.setItems(items, false); + return; + } - ((LinearLayoutManager) getRecyclerView().getLayoutManager()).scrollToPositionWithOffset(firstVisiblePosition, offset); - } + if (mode == MODE_STICKERS_ARCHIVED) { + items.add(new ListItem(ListItem.TYPE_EMPTY_OFFSET_SMALL)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, Lang.getString(!isEmoji ? R.string.ArchivedStickersInfo : R.string.ArchivedEmojiInfo, Strings.buildCounter(tdlib.getInstalledStickerSetLimit())), false)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + } else if (mode == MODE_MASKS) { + items.add(new ListItem(ListItem.TYPE_EMPTY_OFFSET_SMALL)); + items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.MasksHint)); + } + itemsStartIndex = items.size(); - private void addStickerSet (TGStickerSetInfo stickerSet, int index) { - if (stickerSets == null) { - return; + if (mode == MODE_MASKS) { + for (int a = 0; a < stickerSets.size(); a++) { + TGStickerSetInfo info = stickerSets.get(a); + if (!info.isArchived() && a == 0) { + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + itemsStartIndex += 1; + } + if (info.isArchived() && itemsBreakIndex == -1) { + itemsBreakIndex = items.size(); + if (itemsBreakIndex != itemsStartIndex) { + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + } + items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.Archived)); + items.add(new ListItem(ListItem.TYPE_SHADOW_TOP)); + itemsBreakSize = items.size() - itemsBreakIndex; + } + items.add(new ListItem(itemsBreakIndex == -1 ? ListItem.TYPE_STICKER_SET : ListItem.TYPE_ARCHIVED_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setLongId(info.getId()).setHighlightValue(searchRequest)); + } + } else if (mode == MODE_STICKERS_ARCHIVED) { + for (TGStickerSetInfo info : stickerSets) { + items.add(new ListItem(ListItem.TYPE_ARCHIVED_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setLongId(info.getId()).setHighlightValue(searchRequest)); + } + } else { + for (TGStickerSetInfo info : stickerSets) { + items.add(new ListItem(ListItem.TYPE_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setLongId(info.getId()).setHighlightValue(searchRequest)); + } + } + if (!stickerSets.isEmpty()) { + items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); + } + if (mode != MODE_MASKS) { + items.add(new ListItem(ListItem.TYPE_DESCRIPTION_CENTERED, R.id.view_emojiPacksCount, 0, Lang.pluralBold(isEmoji ? R.string.xEmojiPacks : R.string.xStickerPacks, stickerSets.size()), false)); + } + adapter.setItems(items, false); } - stickerSet.setBoundList(stickerSets); - stickerSets.add(index, stickerSet); - int firstVisiblePosition = ((LinearLayoutManager) getRecyclerView().getLayoutManager()).findFirstVisibleItemPosition(); - View topView = getRecyclerView().getLayoutManager().findViewByPosition(firstVisiblePosition); - int offset = topView != null ? topView.getTop() : 0; + public void search (@Nullable String searchRequest) { + this.searchRequest = searchRequest; + this.foundStickerSets = filter(stickerSets, searchRequest); + } - ListItem item = new ListItem(ListItem.TYPE_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setLongId(stickerSet.getId()); + public void setStickerSets (@Nullable ArrayList items) { + stickerSets = items; + foundStickerSets = filter(items, searchRequest); + buildItems(); + } - if (stickerSets.size() == 1 && mode != MODE_STICKERS) { - index += 2; - adapter.getItems().add(index, new ListItem(ListItem.TYPE_SHADOW_BOTTOM)); - adapter.getItems().add(index, item); - adapter.getItems().add(index, new ListItem(ListItem.TYPE_SHADOW_TOP)); - adapter.notifyItemRangeInserted(index, 3); - } else { - index += getStartIndex(); - adapter.getItems().add(index, item); - adapter.notifyItemInserted(index); + public void bind (ListItem item, int position, DoubleTextView group, boolean isArchived, boolean isUpdate) { + TGStickerSetInfo stickerSet = /*findStickerSetById(item.getLongId()); //*/ getStickerSetByPosition(position); + if (stickerSet == null || stickerSet.getId() != item.getLongId()) { + return; + } + group.setStickerSet(stickerSet, item.getHighlightValue()); + if (isArchived) { + NonMaterialButton button = group.getButton(); + if (button != null) { + int state = getState(stickerSet); + button.setInProgress(state == STATE_IN_PROGRESS, isUpdate); + button.setIsDone(state == STATE_DONE, isUpdate); + } + } } - ((LinearLayoutManager) getRecyclerView().getLayoutManager()).scrollToPositionWithOffset(firstVisiblePosition, offset); - } - private void removeStickerSet (long setId) { - int i = indexOfStickerSet(setId); - if (i != -1) { - removeStickerSetByPosition(i); - } - } + public void addMoreStickerSets (ArrayList stickerSetsToAdd) { + if (mode != MODE_STICKERS_ARCHIVED) return; // todo: for masks + if (stickerSets == null || stickerSets.isEmpty() || stickerSetsToAdd.isEmpty()) { + return; + } + + final ArrayList currentStickerSets = getActualStickerSetsList(); + final boolean inSearchMode = inSearchMode(); + final int oldSize = currentStickerSets != null ? currentStickerSets.size() : 0; + + stickerSets.addAll(stickerSetsToAdd); - private void removeStickerSetByPosition (int i) { - if (stickerSets == null) { - return; + ArrayList realStickersToAdd = stickerSetsToAdd; + if (inSearchMode) { + foundStickerSets.addAll(realStickersToAdd = filter(stickerSetsToAdd, searchRequest)); + } + if (oldSize == 0) { + buildItems(); + return; + } + List items = adapter.getItems(); + int startIndex = itemsStartIndex + oldSize; + int i = startIndex; + for (TGStickerSetInfo info : realStickersToAdd) { + info.setBoundList(this.stickerSets); + items.add(i, new ListItem(ListItem.TYPE_ARCHIVED_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setHighlightValue(searchRequest).setLongId(info.getId())); + i++; + } + adapter.notifyItemRangeInserted(startIndex, realStickersToAdd.size()); + updateEmojiPacksCount(); } - stickerSets.remove(i); + public void removeStickerSet (long id) { + final ArrayList currentStickerSets = getActualStickerSetsList(); + final boolean inSearchMode = inSearchMode(); + final int totalIndex = indexOfStickerSetById(stickerSets, id); + final int foundIndex = indexOfStickerSetById(foundStickerSets, id); + final int realIndex = inSearchMode ? foundIndex : totalIndex; - if (stickerSets.isEmpty()) { - if (mode == MODE_MASKS) { - i += 2; - adapter.removeRange(i, 3); + if (currentStates != null) { + currentStates.delete(id); + } + + if (stickerSets != null && totalIndex != -1) { + stickerSets.remove(totalIndex); + } + if (inSearchMode && foundIndex != -1) { + foundStickerSets.remove(foundIndex); + } + if (itemsStartIndex == -1 || currentStickerSets == null || currentStickerSets.isEmpty()) { + buildItems(); + return; + } + if (realIndex == -1) { + return; + } + + boolean needUpdateBreakPosition = false; + int position = itemsStartIndex + realIndex; + if (itemsBreakIndex != -1 && position > itemsBreakIndex) { + position += itemsBreakSize; } else { - buildCells(); + needUpdateBreakPosition = itemsBreakIndex != -1; + } + if (needUpdateBreakPosition) { + itemsBreakIndex -= 1; } - } else { - i += getStartIndex(); - adapter.getItems().remove(i); - adapter.notifyItemRemoved(i); + adapter.removeItem(position); + updateEmojiPacksCount(); } - } - private boolean applyingChanges; - private ArrayList pendingChanges; + public void addStickerSet (TGStickerSetInfo info, int index) { + final ArrayList currentStickerSets = getActualStickerSetsList(); + final boolean inSearchMode = inSearchMode(); + final int oldSize = currentStickerSets != null ? currentStickerSets.size() : 0; + final int totalIndex = indexOfStickerSetById(stickerSets, info.getId()); + if (totalIndex != -1 || stickerSets == null) { + if (totalIndex != -1 && mode == MODE_MASKS) { + removeStickerSet(info.getId()); // todo: call move + } else { + return; + } + } + info.setBoundList(stickerSets); + + int firstVisiblePosition = ((LinearLayoutManager) getRecyclerView().getLayoutManager()).findFirstVisibleItemPosition(); + View topView = getRecyclerView().getLayoutManager().findViewByPosition(firstVisiblePosition); + int offset = topView != null ? topView.getTop() : 0; + + final int realIndex; + stickerSets.add(index, info); + if (inSearchMode && filterImpl(info.getTitle(), searchRequest)) { + int indexToInsert = indexOfFoundStickerSet(index); + foundStickerSets.add(indexToInsert, info); + realIndex = indexToInsert; + } else { + realIndex = inSearchMode ? -1 : index; + } - private void setApplyingChanges (boolean applyingChanges) { - if (this.applyingChanges != applyingChanges) { - this.applyingChanges = applyingChanges; - if (!applyingChanges && pendingChanges != null && !pendingChanges.isEmpty()) { - do { - long[] stickerSetIds = pendingChanges.remove(0); - changeStickerSets(stickerSetIds); - } while (!pendingChanges.isEmpty() && !this.applyingChanges); + if (realIndex == -1) { + return; + } + if (oldSize == 0 || mode == MODE_MASKS) { + buildItems(); + return; } - } - } - private void removeArchivedSet (int index) { - if (archivedSets == null || archivedSets.isEmpty()) { - return; - } + boolean needUpdateBreakPosition = false; + int position = itemsStartIndex + realIndex; + if (itemsBreakIndex != -1 && position > itemsBreakIndex) { + position += itemsBreakSize; + } else { + needUpdateBreakPosition = itemsBreakIndex != -1; + } + if (needUpdateBreakPosition) { + itemsBreakIndex += 1; + } + adapter.addItem(position, new ListItem(info.isArchived() ? ListItem.TYPE_ARCHIVED_STICKER_SET : ListItem.TYPE_STICKER_SET, R.id.btn_stickerSetInfo, 0, 0).setHighlightValue(searchRequest).setLongId(info.getId())); + updateEmojiPacksCount(); - TGStickerSetInfo info = archivedSets.remove(index); - if (currentStates != null) { - currentStates.delete(info.getId()); + ((LinearLayoutManager) getRecyclerView().getLayoutManager()).scrollToPositionWithOffset(firstVisiblePosition, offset); } - if (archivedSets.isEmpty()) { - adapter.removeRange(getArchivedStartIndex() - 2, 4); - } else { - adapter.removeRange(getArchivedStartIndex() + index, 1); + + public void moveStickerSet (int oldPosition, int newPosition) { + final boolean inSearchMode = inSearchMode(); + if (oldPosition == newPosition || stickerSets == null) { + return; + } + ArrayUtils.move(stickerSets, oldPosition, newPosition); + + int realOldIndex = oldPosition; + int realNewIndex = newPosition; + boolean needUpdateAdapter = true; + + if (inSearchMode) { + if (filterImpl(stickerSets.get(newPosition).getTitle(), searchRequest) || filterImpl(stickerSets.get(oldPosition).getTitle(), searchRequest)) { + int oldFoundPosition = Math.min(indexOfFoundStickerSet(oldPosition), foundStickerSets.size() - 1); + int newFoundPosition = Math.min(indexOfFoundStickerSet(newPosition), foundStickerSets.size() - 1); + if (oldFoundPosition != newFoundPosition) { + realOldIndex = oldFoundPosition; + realNewIndex = newFoundPosition; + ArrayUtils.move(foundStickerSets, oldFoundPosition, newFoundPosition); + } else { + needUpdateAdapter = false; + } + } else { + needUpdateAdapter = false; + } + } + + if (!needUpdateAdapter) { + return; + } + + int positionOld = itemsStartIndex + realOldIndex; + int positionNew = itemsStartIndex + realNewIndex; + if (itemsBreakIndex != -1 && positionOld > itemsBreakIndex) { + positionOld += itemsBreakSize; + } + if (itemsBreakIndex != -1 && positionNew > itemsBreakIndex) { + positionNew += itemsBreakSize; + } + + int firstVisiblePosition = ((LinearLayoutManager) getRecyclerView().getLayoutManager()).findFirstVisibleItemPosition(); + View topView = getRecyclerView().getLayoutManager().findViewByPosition(firstVisiblePosition); + int offset = topView != null ? topView.getTop() : 0; + adapter.moveItem(positionOld, positionNew, true); + ((LinearLayoutManager) getRecyclerView().getLayoutManager()).scrollToPositionWithOffset(firstVisiblePosition, offset); } - } - private void changeStickerSets (long[] stickerSetIds) { - if (mode == MODE_STICKERS_ARCHIVED) { - for (long stickerSetId : stickerSetIds) { - removeStickerSet(stickerSetId); + private void updateEmojiPacksCount () { + if (mode == MODE_MASKS) return; + ArrayList stickerSets = getActualStickerSetsList(); + + int i = adapter.indexOfViewById(R.id.view_emojiPacksCount); + if (i != -1) { + ListItem item = adapter.getItems().get(i); + boolean changed = item.setStringIfChanged(Lang.pluralBold(R.string.xEmojiPacks, stickerSets != null ? stickerSets.size() : 0)); + if (changed) { + adapter.notifyItemChanged(i); + } } - return; } - if (applyingChanges) { - if (pendingChanges == null) { - pendingChanges = new ArrayList<>(); + private int indexOfFoundStickerSet (int index) { + if (stickerSets == null) return -1; + int indexToInsert = 0; + for (int a = 0; a < index; a++) { + TGStickerSetInfo info2 = stickerSets.get(a); + if (filterImpl(info2.getTitle(), searchRequest)) { + indexToInsert += 1; + } } - pendingChanges.add(stickerSetIds); - return; + return indexToInsert; } - if ((stickerSets == null || stickerSets.isEmpty()) && mode != MODE_MASKS) { - loadData(false); - return; + public int getIndexFromPosition (int position) { + ArrayList stickerSets = getActualStickerSetsList(); + + if (stickerSets == null || itemsStartIndex == -1) return -1; + position -= itemsStartIndex; + + if (itemsBreakIndex != -1 && position >= itemsBreakIndex) { + position -= itemsBreakSize; + } + + if (position >= 0 && position < stickerSets.size()) { + return position; + } + return -1; } - final LongSparseArray removedStickerSets = new LongSparseArray<>(); + @Nullable + private TGStickerSetInfo getStickerSetByPosition (int position) { + ArrayList stickerSets = getActualStickerSetsList(); + int index = getIndexFromPosition(position); - for (TGStickerSetInfo stickerSet : stickerSets) { - removedStickerSets.put(stickerSet.getId(), stickerSet); + return index != -1 && stickerSets != null ? stickerSets.get(index) : null; } - LongSparseArray positions = null; - LongSparseArray allItems = new LongSparseArray<>(stickerSetIds.length); - LongList futureItems = null; + private boolean inSearchMode () { + return !StringUtils.isEmpty(searchRequest); + } - int setIndex = 0; - int totalIndex = 0; - int lastAddedIndex = -1; - boolean reloadAfterLocalChanges = false; - for (long setId : stickerSetIds) { - TGStickerSetInfo currentSet = removedStickerSets.get(setId); + @Nullable + private ArrayList getActualStickerSetsList () { + return inSearchMode() ? foundStickerSets : stickerSets; + } - if (currentSet == null) { - if (!reloadAfterLocalChanges) { - if (totalIndex != ++lastAddedIndex) { - reloadAfterLocalChanges = true; - } else { - if (futureItems == null) { - futureItems = new LongList(5); - } - futureItems.append(setId); + @Nullable + private TGStickerSetInfo findStickerSetById (long stickerSetId) { + ArrayList stickerSets = getActualStickerSetsList(); + if (stickerSets != null && !stickerSets.isEmpty()) { + for (TGStickerSetInfo info : stickerSets) { + if (info.getId() == stickerSetId) { + return info; } } - } else { - removedStickerSets.remove(setId); + } + return null; + } - if (positions == null) { - positions = new LongSparseArray<>(5); + private int getInstalledSetsInDatasetCount () { + if (stickerSets == null) { + return 0; + } + for (int a = 0; a < stickerSets.size(); a++) { + if (!stickerSets.get(a).isInstalled()) { + return a; } - - positions.put(setId, setIndex); - setIndex++; } + return stickerSets.size(); + } - allItems.put(setId, totalIndex); - totalIndex++; + private int indexOfStickerSetById (@Nullable ArrayList stickerSets, long id) { + if (stickerSets == null) { + return -1; + } + int i = 0; + for (TGStickerSetInfo info : stickerSets) { + if (info.getId() == id) { + return i; + } + i++; + } + return -1; } - if (archivedSets != null && !archivedSets.isEmpty()) { - final int size = archivedSets.size(); - for (int i = size - 1; i >= 0; i--) { - if (allItems.get(archivedSets.get(i).getId(), -1) != -1) { - removeArchivedSet(i); + @NonNull + private ArrayList filter (@Nullable ArrayList stickerSets, @Nullable String request) { + if (StringUtils.isEmpty(request) || stickerSets == null) { + return new ArrayList<>(); + } + ArrayList results = new ArrayList<>(stickerSets.size()); + for (TGStickerSetInfo info : stickerSets) { + if (filterImpl(info.getTitle(), request)) { + results.add(info); } } + + return results; + } + + private boolean filterImpl (String title, String request) { + return Highlight.isExactMatch(Highlight.valueOf(title, request)); + } + + /* States */ + + private static final int STATE_NONE = 0; + private static final int STATE_IN_PROGRESS = 1; + private static final int STATE_DONE = 2; + + private LongSparseIntArray currentStates; + + private int getState (TGStickerSetInfo stickerSet) { + return currentStates != null ? currentStates.get(stickerSet.getId(), STATE_NONE) : STATE_NONE; } - // First, remove items - final int removedCount = removedStickerSets.size(); - for (int i = 0; i < removedCount; i++) { - TGStickerSetInfo stickerSet = removedStickerSets.valueAt(i); - removeStickerSet(stickerSet.getId()); + private void setState (long setId, int state) { + if (currentStates == null) { + currentStates = new LongSparseIntArray(); + } + currentStates.put(setId, state); + adapter.updateStickerSetById(setId); } - // Then, move items - if (positions != null && !stickerSets.isEmpty() ) { - for (int j = 0; j < positions.size(); j++) { - long setId = positions.keyAt(j); - int newPosition = positions.valueAt(j); - int currentPosition = indexOfStickerSet(setId); - if (currentPosition == -1) { - throw new RuntimeException(); + /* ? */ + + private void changeStickerSets (long[] stickerSetIds) { + if (mode == MODE_STICKERS_ARCHIVED) { + for (long stickerSetId : stickerSetIds) { + removeStickerSet(stickerSetId); } - if (currentPosition != newPosition) { - moveStickerSet(currentPosition, newPosition); + return; + } + + if (applyingChanges) { + if (pendingChanges == null) { + pendingChanges = new ArrayList<>(); } + pendingChanges.add(stickerSetIds); + return; } - } - if (reloadAfterLocalChanges) { - loadData(false); - return; - } + if ((stickerSets == null || stickerSets.isEmpty()) && mode != MODE_MASKS) { + loadData(false); + return; + } - if (futureItems != null) { - setApplyingChanges(true); - final long[] setIds = futureItems.get(); - final int addedCount = futureItems.size(); - final int[] index = new int[2]; - tdlib.client().send(new TdApi.GetStickerSet(setIds[index[0]]), new Client.ResultHandler() { - @Override - public void onResult (TdApi.Object object) { - if (object.getConstructor() == TdApi.StickerSet.CONSTRUCTOR) { - final TGStickerSetInfo stickerSet = new TGStickerSetInfo(tdlib, (TdApi.StickerSet) object); - final int insertIndex = index[1]++; + final LongSparseArray removedStickerSets = new LongSparseArray<>(); + for (TGStickerSetInfo stickerSet : stickerSets) { + if (mode != MODE_MASKS || !stickerSet.isArchived()) { + removedStickerSets.put(stickerSet.getId(), stickerSet); + } + } - tdlib.ui().post(() -> { - if (!isDestroyed()) { - addStickerSet(stickerSet, insertIndex); + LongSparseArray positions = null; + LongSparseArray allItems = new LongSparseArray<>(stickerSetIds.length); + LongList futureItems = null; + + int setIndex = 0; + int totalIndex = 0; + int lastAddedIndex = -1; + boolean reloadAfterLocalChanges = false; + for (long setId : stickerSetIds) { + TGStickerSetInfo currentSet = removedStickerSets.get(setId); + + if (currentSet == null) { + if (!reloadAfterLocalChanges) { + if (totalIndex != ++lastAddedIndex) { + reloadAfterLocalChanges = true; + } else { + if (futureItems == null) { + futureItems = new LongList(5); } - }); + futureItems.append(setId); + } } + } else { + removedStickerSets.remove(setId); - if (++index[0] < addedCount) { - tdlib.client().send(new TdApi.GetStickerSet(setIds[index[0]]), this); - } else { - tdlib.ui().post(() -> { - if (!isDestroyed()) { - setApplyingChanges(false); - } - }); + if (positions == null) { + positions = new LongSparseArray<>(5); } + + positions.put(setId, setIndex); + setIndex++; } - }); + + allItems.put(setId, totalIndex); + totalIndex++; + } + + // First, remove items + final int removedCount = removedStickerSets.size(); + for (int i = 0; i < removedCount; i++) { + TGStickerSetInfo stickerSet = removedStickerSets.valueAt(i); + removeStickerSet(stickerSet.getId()); + } + + // Then, move items + if (positions != null && !stickerSets.isEmpty()) { + for (int j = 0; j < positions.size(); j++) { + long setId = positions.keyAt(j); + int newPosition = positions.valueAt(j); + int currentPosition = indexOfStickerSetById(stickerSets, setId); + if (currentPosition == -1) { + throw new RuntimeException(); + } + if (currentPosition != newPosition) { + moveStickerSet(currentPosition, newPosition); + } + } + } + + if (reloadAfterLocalChanges) { + loadData(false); + return; + } + + if (futureItems != null) { + setApplyingChanges(true); + final long[] setIds = futureItems.get(); + final int addedCount = futureItems.size(); + final int[] index = new int[2]; + tdlib.client().send(new TdApi.GetStickerSet(setIds[index[0]]), new Client.ResultHandler() { + @Override + public void onResult (TdApi.Object object) { + if (object.getConstructor() == TdApi.StickerSet.CONSTRUCTOR) { + final TGStickerSetInfo stickerSet = new TGStickerSetInfo(tdlib, (TdApi.StickerSet) object); + final int insertIndex = index[1]++; + + tdlib.ui().post(() -> { + if (!context.isDestroyed()) { + addStickerSet(stickerSet, insertIndex); + } + }); + } + + if (++index[0] < addedCount) { + tdlib.client().send(new TdApi.GetStickerSet(setIds[index[0]]), this); + } else { + tdlib.ui().post(() -> { + if (!context.isDestroyed()) { + setApplyingChanges(false); + } + }); + } + } + }); + } + } + + private boolean applyingChanges; + private ArrayList pendingChanges; + + private void setApplyingChanges (boolean applyingChanges) { + if (this.applyingChanges != applyingChanges) { + this.applyingChanges = applyingChanges; + if (!applyingChanges && pendingChanges != null && !pendingChanges.isEmpty()) { + do { + long[] stickerSetIds = pendingChanges.remove(0); + changeStickerSets(stickerSetIds); + } while (!pendingChanges.isEmpty() && !this.applyingChanges); + } + } + } + + private void saveStickersOrder () { + if (stickerSets == null || stickerSets.isEmpty()) { + return; + } + + long[] stickerSetIds = new long[stickerSets.size()]; + int i = 0; + for (TGStickerSetInfo info : stickerSets) { + stickerSetIds[i++] = info.getId(); + } + tdlib.client().send(new TdApi.ReorderInstalledStickerSets(getStickerType(mode, isEmoji), stickerSetIds), tdlib.okHandler()); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/StickersListController.java b/app/src/main/java/org/thunderdog/challegram/ui/StickersListController.java index d9e4eba8a5..d7b27eb8e8 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/StickersListController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/StickersListController.java @@ -15,14 +15,17 @@ package org.thunderdog.challegram.ui; import android.content.Context; -import android.graphics.Canvas; -import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; +import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.collection.LongSparseArray; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -30,48 +33,57 @@ import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; import org.thunderdog.challegram.component.attach.CustomItemAnimator; +import org.thunderdog.challegram.component.emoji.MediaStickersAdapter; +import org.thunderdog.challegram.component.sticker.StickerPreviewView; import org.thunderdog.challegram.component.sticker.StickerSmallView; import org.thunderdog.challegram.component.sticker.TGStickerObj; import org.thunderdog.challegram.config.Config; +import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGStickerSetInfo; import org.thunderdog.challegram.navigation.BackHeaderButton; import org.thunderdog.challegram.navigation.HeaderView; import org.thunderdog.challegram.navigation.Menu; import org.thunderdog.challegram.navigation.MoreDelegate; +import org.thunderdog.challegram.navigation.NavigationController; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.telegram.StickersListener; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.ColorState; -import org.thunderdog.challegram.theme.Theme; -import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.unsorted.Size; import org.thunderdog.challegram.util.StringList; import org.thunderdog.challegram.v.RtlGridLayoutManager; -import org.thunderdog.challegram.widget.ProgressComponentView; import java.util.ArrayList; import me.vkryl.android.AnimatorUtils; import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.ArrayUtils; import me.vkryl.core.collection.IntList; +import me.vkryl.core.collection.LongList; import me.vkryl.core.lambda.CancellableRunnable; +import me.vkryl.core.lambda.RunnableData; import me.vkryl.td.Td; -public class StickersListController extends ViewController implements Menu, StickerSmallView.StickerMovementCallback, Client.ResultHandler, MoreDelegate, StickersListener { +public class StickersListController extends ViewController implements + Menu, StickerSmallView.StickerMovementCallback, Client.ResultHandler, + MoreDelegate, StickersListener, StickerPreviewView.MenuStickerPreviewCallback { public StickersListController (Context context, Tdlib tdlib) { super(context, tdlib); } public interface StickerSetProvider { - boolean canArchiveStickerSet (); - boolean canRemoveStickerSet (); + boolean canArchiveStickerSet (long setId); + boolean canRemoveStickerSet (long setId); + boolean canInstallStickerSet (long setId); boolean canViewPack (); - void archiveStickerSet (); - void removeStickerSet (); + void archiveStickerSets (long[] setIds); + void installStickerSets (long[] setIds); + void removeStickerSets (long[] setIds); boolean onStickerClick (View view, TGStickerObj obj, boolean isMenuClick, TdApi.MessageSendOptions sendOptions); long getStickerOutputChatId (); } @@ -120,33 +132,58 @@ protected int getHeaderColorId () { @Override public void onMenuItemPressed (int id, View view) { + if (getArguments() == null) return; + if (id == R.id.menu_btn_more) { - if (info != null) { - IntList ids = new IntList(4); - StringList strings = new StringList(4); - IntList icons = new IntList(4); + IntList ids = new IntList(4); + StringList strings = new StringList(4); + IntList icons = new IntList(4); + if (stickerSetInfoToLoad != null) { ids.append(R.id.btn_share); strings.append(R.string.Share); icons.append(R.drawable.baseline_forward_24); + } - ids.append(R.id.btn_copyLink); - strings.append(R.string.CopyLink); - icons.append(R.drawable.baseline_link_24); + ids.append(R.id.btn_copyLink); + strings.append(R.string.CopyLink); + icons.append(R.drawable.baseline_link_24); - if (getArguments() != null) { - if (getArguments().canArchiveStickerSet()) { - ids.append(R.id.btn_archive); - strings.append(R.string.StickersHide); - icons.append(R.drawable.baseline_archive_24); + if (stickerSetInfoToLoad != null) { + if (getArguments().canArchiveStickerSet(stickerSetInfoToLoad != null ? stickerSetInfoToLoad.id : -1)) { + ids.append(R.id.btn_archive); + strings.append(R.string.StickersHide); + icons.append(R.drawable.baseline_archive_24); + } + if (getArguments().canRemoveStickerSet(stickerSetInfoToLoad != null ? stickerSetInfoToLoad.id : -1)) { + ids.append(R.id.btn_delete); + strings.append(R.string.DeleteArchivedPack); + icons.append(R.drawable.baseline_delete_24); + } + showMore(ids.get(), strings.get(), icons.get(), 0); + } else if (!stickerSections.isEmpty() ) { + int setsToInstall = 0; + int setsToArchive = 0; + + for (StickerSection section : stickerSections) { + if (section.info == null) continue; + if (getArguments().canArchiveStickerSet(section.info.getId())) { + setsToArchive += 1; } - if (getArguments().canRemoveStickerSet()) { - ids.append(R.id.btn_delete); - strings.append(R.string.DeleteArchivedPack); - icons.append(R.drawable.baseline_delete_24); + if (getArguments().canInstallStickerSet(section.info.getId())) { + setsToInstall += 1; } } - + if (setsToInstall > 0) { + ids.append(R.id.btn_installStickerSet); + strings.append(Lang.plural(R.string.xStickersInstall, setsToInstall)); + icons.append(R.drawable.deproko_baseline_stickers_24); + } + if (setsToArchive > 0) { + ids.append(R.id.btn_archive); + strings.append(Lang.plural(R.string.xStickersHide, setsToArchive)); + icons.append(R.drawable.baseline_archive_24); + } showMore(ids.get(), strings.get(), icons.get(), 0); } } @@ -155,56 +192,109 @@ public void onMenuItemPressed (int id, View view) { @Override public void onMoreItemPressed (int id) { if (id == R.id.btn_share) { - tdlib.ui().shareStickerSetUrl(this, info); + tdlib.ui().shareStickerSetUrl(this, stickerSetInfoToLoad); } else if (id == R.id.btn_copyLink) { - UI.copyText(TD.getStickerPackLink(info.name), R.string.CopiedLink); + if (stickerSetInfoToLoad != null) { + UI.copyText(tdlib.tMeStickerSetUrl(stickerSetInfoToLoad), R.string.CopiedLink); + } else { + StringBuilder b = new StringBuilder(); + for (StickerSection section : stickerSections) { + TdApi.StickerSetInfo stickerSetInfo = section.info != null ? section.info.getInfo() : null; + if (stickerSetInfo == null) { + continue; + } + if (b.length() != 0) { + b.append('\n'); + } + b.append(tdlib.tMeStickerSetUrl(stickerSetInfo)); + } + UI.copyText(b.toString(), R.string.CopiedLink); + } } else if (id == R.id.btn_archive) { if (getArguments() != null) { - getArguments().archiveStickerSet(); + if (stickerSetInfoToLoad != null) { + getArguments().archiveStickerSets(new long[] {stickerSetInfoToLoad.id}); + } else { + LongList stickerSetsToArchive = new LongList(stickerSections.size()); + for (StickerSection section : stickerSections) { + if (section.info == null) continue; + long setId = section.info.getId(); + if (getArguments().canArchiveStickerSet(setId)) { + stickerSetsToArchive.append(setId); + } + } + getArguments().archiveStickerSets(stickerSetsToArchive.get()); + } } } else if (id == R.id.btn_delete) { if (getArguments() != null) { - getArguments().removeStickerSet(); + long stickerSetId = stickerSetInfoToLoad != null ? stickerSetInfoToLoad.id : -1; + getArguments().removeStickerSets(new long[] {stickerSetId}); + } + } else if (id == R.id.btn_installStickerSet) { + if (getArguments() != null) { + if (stickerSetInfoToLoad != null) { + getArguments().installStickerSets(new long[] {stickerSetInfoToLoad.id}); + } else { + LongList stickerSetsToInstall = new LongList(stickerSections.size()); + for (StickerSection section : stickerSections) { + if (section.info == null) continue; + final long setId = section.info.getId(); + if (getArguments().canInstallStickerSet(setId)) { + stickerSetsToInstall.append(setId); + } + } + getArguments().installStickerSets(stickerSetsToInstall.get()); + } } } } @Override public CharSequence getName () { - if (info != null) { + if (stickerSetInfoToLoad != null) { + TdApi.StickerSetInfo info = stickerSetInfoToLoad; TdApi.TextEntity[] entities = Td.findEntities(info.title); return TD.formatString(this, info.title, entities, null, null); } + if (stickerSections.size() > 1) { + return Lang.plural(R.string.xEmojiPacks, stickerSections.size()); + } + if (stickerSetIdsToLoad != null && stickerSetIdsToLoad.length > 1) { + return Lang.plural(R.string.xEmojiPacks, stickerSetIdsToLoad.length); + } return null; } - private TdApi.StickerSetInfo info; - private OffsetProvider offsetProvider; + private MediaStickersAdapter.OffsetProvider offsetProvider; + + - public void setOffsetProvider (OffsetProvider provider) { + public void setOffsetProvider (MediaStickersAdapter.OffsetProvider provider) { this.offsetProvider = provider; } public void setStickerSetInfo (TdApi.StickerSetInfo info) { - this.info = info; + this.stickerSetInfoToLoad = info; + this.isEmojiPack = info.stickerType.getConstructor() == TdApi.StickerTypeCustomEmoji.CONSTRUCTOR; + } + + public void setIsEmojiPack (boolean isEmojiPack) { + this.isEmojiPack = isEmojiPack; + } + + public void setStickerSets (long[] stickerSetIds) { + this.stickerSetIdsToLoad = stickerSetIds; } public void setStickers (TdApi.Sticker[] stickers, TdApi.StickerType stickerType, TdApi.Emojis[] emojis) { - this.stickers = new ArrayList<>(stickers.length); - int i = 0; boolean canViewPack = getArguments() == null || getArguments().canViewPack(); - for (TdApi.Sticker sticker : stickers) { - TGStickerObj obj = new TGStickerObj(tdlib, sticker, stickerType, emojis[i].emojis); - if (!canViewPack) { - obj.setNoViewPack(); - } - this.stickers.add(obj); - i++; - } + this.stickerSections.clear(); + this.stickerSections.add(new StickerSection(tdlib, stickers, stickerType, emojis, canViewPack)); } private RecyclerView recyclerView; - private StickersAdapter adapter; + private MediaStickersAdapter adapter; private int spanCount; private int lastSpanCountWidth, lastSpanCountHeight; private GridLayoutManager manager; @@ -216,7 +306,7 @@ private void setSpanCount (int width, int height) { if (lastSpanCountWidth != width || lastSpanCountHeight != height) { lastSpanCountWidth = width; lastSpanCountHeight = height; - int newSpanCount = calculateSpanCount(width, height); + int newSpanCount = calculateSpanCount(width, height, isEmojiPack); if (newSpanCount != spanCount) { this.spanCount = newSpanCount; manager.setSpanCount(newSpanCount); @@ -248,7 +338,7 @@ public void onThemeColorsChanged (boolean areTemp, ColorState state) { @Override protected View onCreateView (Context context) { - spanCount = calculateSpanCount(lastSpanCountWidth = Screen.currentWidth(), lastSpanCountHeight = Screen.currentHeight()); + spanCount = calculateSpanCount(lastSpanCountWidth = Screen.currentWidth(), lastSpanCountHeight = Screen.currentHeight(), isEmojiPack); FrameLayoutFix contentView = new FrameLayoutFix(context) { @Override @@ -265,12 +355,6 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { params.bottomMargin = Screen.dp(56f); recyclerView = new RecyclerView(context) { - @Override - public void draw (Canvas c) { - c.drawRect(0, offsetProvider.provideOffset() - offsetScroll, getMeasuredWidth(), getMeasuredHeight(), Paints.fillingPaint(Theme.fillingColor())); - super.draw(c); - } - @Override public boolean onTouchEvent (MotionEvent e) { if (e.getAction() == MotionEvent.ACTION_DOWN) { @@ -286,18 +370,56 @@ public boolean onTouchEvent (MotionEvent e) { recyclerView.setItemAnimator(null); recyclerView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? View.OVER_SCROLL_IF_CONTENT_SCROLLS : View.OVER_SCROLL_NEVER); recyclerView.setLayoutManager(manager = new RtlGridLayoutManager(context, spanCount).setAlignOnly(true)); - recyclerView.setAdapter(adapter = new StickersAdapter(this, recyclerView, this, offsetProvider)); + recyclerView.setAdapter(adapter = new MediaStickersAdapter(this, this, false, this, offsetProvider, true, null) { + @Override + protected void onToggleCollapseRecentStickers (TextView collapseView, TGStickerSetInfo recentSet) { + int existingIndex = getStickerSetSectionIndexById(recentSet.getId()); + if (existingIndex == -1) return; + + StickerSection section = stickerSections.get(existingIndex); + boolean needExpand = recentSet.isCollapsed(); + int endIndex = recentSet.getEndIndex(); + int visibleItemCount = recentSet.getItemCount(); + + if (needExpand) { + recentSet.setSize(recentSet.getFullSize()); + + ArrayList stickers = section.toItems(true); + for (int a = 0; a < visibleItemCount - 1; a++) { + stickers.remove(0); + } + if (existingIndex != stickerSections.size() - 1) { + stickers.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_SEPARATOR)); + } + + shiftStickerSets(existingIndex, stickers.size() - 1); + adapter.removeRange(endIndex - 1, 1); + adapter.addRange(endIndex - 1, stickers); + } else { + // recentSet.setSize(spanCount * 2); + // adapter.removeRange(recentSet.getEndIndex(), endIndex - recentSet.getEndIndex()); + // shiftStickerSets(existingIndex, recentSet.getEndIndex() - endIndex); + } + } + + @Override + public void updateCollapseView (TextView collapseView, TGStickerSetInfo stickerSet, @StringRes int showMoreRes) { + if (stickerSet != null && stickerSet.isCollapsed()) { // ignore updates for expanded sets + super.updateCollapseView(collapseView, stickerSet, showMoreRes); + } + } + }); recyclerView.setLayoutParams(params); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onScrollStateChanged (RecyclerView recyclerView, int newState) { + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { offsetProvider.onScrollFinished(); } } @Override - public void onScrolled (RecyclerView recyclerView, int dx, int dy) { + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { int i = manager.findFirstVisibleItemPosition(); View view = manager.findViewByPosition(i); if (view != null) { @@ -314,21 +436,34 @@ public void onScrolled (RecyclerView recyclerView, int dx, int dy) { manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize (int position) { - return position == 0 || (position == 1 && adapter.getItemViewType(1) == StickerHolder.TYPE_PROGRESS) ? spanCount : 1; + int type = adapter.getItemViewType(position); + return position == 0 + || type == MediaStickersAdapter.StickerHolder.TYPE_HEADER + || type == MediaStickersAdapter.StickerHolder.TYPE_HEADER_COLLAPSABLE + || type == MediaStickersAdapter.StickerHolder.TYPE_PROGRESS_OFFSETABLE + || type == MediaStickersAdapter.StickerHolder.TYPE_HEADER_TRENDING + || type == MediaStickersAdapter.StickerHolder.TYPE_SEPARATOR_COLLAPSABLE + || type == MediaStickersAdapter.StickerHolder.TYPE_SEPARATOR ? spanCount : 1; } }); + adapter.setMenuStickerPreviewCallback(this); + adapter.setRepaintingColorId(ColorId.text); + adapter.setManager(manager); + adapter.setIsBig(); contentView.addView(recyclerView); - if (stickers != null) { - adapter.setItems(stickers); - } else if (info != null) { - tdlib.client().send(new TdApi.GetStickerSet(info.id), this); - } - if (info != null) { - tdlib.listeners().subscribeToStickerUpdates(this); + buildCells(false); + if (stickerSections.isEmpty()) { + if (stickerSetInfoToLoad != null) { + tdlib.client().send(new TdApi.GetStickerSet(stickerSetInfoToLoad.id), this); + } else if (stickerSetIdsToLoad != null) { + startLoadStickerSets(); + } } + tdlib.listeners().subscribeToStickerUpdates(this); + return contentView; } @@ -346,17 +481,16 @@ public int getOffsetScroll () { private CancellableRunnable itemAnimatorRunnable; public void setItemAnimator () { - recyclerView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180l)); + recyclerView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180L)); } - private ArrayList stickers; + private boolean isEmojiPack; - private void buildCells () { - if (itemAnimatorRunnable != null) { - itemAnimatorRunnable.cancel(); - itemAnimatorRunnable = null; + private void shiftStickerSets (int startPosition, int offset) { + for (int i = startPosition + 1; i < stickerSections.size(); i++) { + TGStickerSetInfo stickerSet = stickerSections.get(i).info; + stickerSet.setStartIndex(stickerSet.getStartIndex() + offset); } - adapter.setItems(stickers); } @Override @@ -366,16 +500,6 @@ public void destroy () { tdlib.listeners().unsubscribeFromStickerUpdates(this); } - @Override - public void onStickerSetUpdated (TdApi.StickerSet stickerSet) { - tdlib.ui().post(() -> { - if (!isDestroyed() && info.id == stickerSet.id) { - setStickers(stickerSet.stickers, stickerSet.stickerType, stickerSet.emojis); - buildCells(); - } - }); - } - @Override public void onResult (final TdApi.Object object) { switch (object.getConstructor()) { @@ -385,7 +509,7 @@ public void onResult (final TdApi.Object object) { tdlib.ui().post(() -> { if (!isDestroyed()) { - buildCells(); + buildCells(false); } }); @@ -420,17 +544,6 @@ public boolean needsLongDelay (StickerSmallView view) { return true; } - public int indexOfSticker (TGStickerObj obj) { - int i = 0; - for (TGStickerObj sticker : adapter.stickers) { - if (sticker.equals(obj)) { - return i + 1; - } - i++; - } - return -1; - } - @Override public void onStickerPreviewOpened (StickerSmallView view, TGStickerObj sticker) { @@ -475,160 +588,290 @@ public void scrollBy (int y) { recyclerView.smoothScrollBy(0, y); } - public interface OffsetProvider { - int provideOffset (); - int provideReverseOffset (); - void onContentScroll (float shadowFactor); - void onScrollFinished (); - } - - private static int calculateSpanCount (int width, int height) { + private static int calculateSpanCount (int width, int height, boolean isEmoji) { int minSide = Math.min(width, height); - int minWidth = minSide / 4; + int minWidth = isEmoji ? Screen.dp(42) : (minSide / 4); return minWidth != 0 ? width / minWidth : 4; } public static int getEstimateColumnResolution () { - int spanCount = calculateSpanCount(Screen.currentWidth(), Screen.currentHeight()); + int spanCount = calculateSpanCount(Screen.currentWidth(), Screen.currentHeight(), false); return Screen.currentWidth() / spanCount; } - private static class StickerHolder extends RecyclerView.ViewHolder { - public static final int TYPE_PADDING = 0; - public static final int TYPE_STICKER = 1; - public static final int TYPE_PROGRESS = 2; + public int indexOfSticker (TGStickerObj obj) { + int i = 0; + /*for (TGStickerObj sticker : adapter.stickers) { + if (sticker.equals(obj)) { + return i + 1; + } + i++; + }*/ + return -1; + } - public StickerHolder (View itemView) { - super(itemView); - } - public static StickerHolder create (Context context, Tdlib tdlib, int viewType, StickerSmallView.StickerMovementCallback callback, final OffsetProvider provider) { - switch (viewType) { - case TYPE_PADDING: { - View view = new View(context) { - @Override - protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension( - getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), - MeasureSpec.makeMeasureSpec(provider.provideOffset(), MeasureSpec.EXACTLY)); - } - }; - return new StickerHolder(view); + + /* * */ + + private boolean isInLoadSetsProgress; + private int loadSetsKey = -1; + private int loadSetsCounter; + + private @Nullable TdApi.StickerSetInfo stickerSetInfoToLoad; + private @Nullable long[] stickerSetIdsToLoad; + + private RunnableData> loadStickerSetsListener; + + public void setLoadStickerSetsListener (RunnableData> loadStickerSetsListener) { + this.loadStickerSetsListener = loadStickerSetsListener; + } + + private void startLoadStickerSets () { + if (isInLoadSetsProgress || stickerSetIdsToLoad == null) return; + isInLoadSetsProgress = true; + loadSetsCounter = stickerSetIdsToLoad.length; + loadSetsKey += 1; + + final int currentKey = loadSetsKey; + final long[] currentStickerSetIds = stickerSetIdsToLoad; + final TdApi.StickerSet[] loadSetsResult = new TdApi.StickerSet[stickerSetIdsToLoad.length]; + for (long id : stickerSetIdsToLoad) { + tdlib.send(new TdApi.GetStickerSet(id), (stickerSet, error) -> UI.post(() -> { + if (currentKey != loadSetsKey || isDestroyed()) return; + if (stickerSet != null) { + int index = ArrayUtils.indexOf(currentStickerSetIds, stickerSet.id); + if (index > -1 && index < loadSetsResult.length) { + loadSetsResult[index] = stickerSet; + } } - case TYPE_PROGRESS: { - FrameLayoutFix contentView = new FrameLayoutFix(context) { - @Override - protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(provider.provideReverseOffset(), MeasureSpec.EXACTLY)); + loadSetsCounter -= 1; + if (loadSetsCounter == 0) { + stickerSections.clear(); + + isInLoadSetsProgress = false; + ArrayList sets = new ArrayList<>(loadSetsResult.length); + for (TdApi.StickerSet set : loadSetsResult) { + if (set != null) { + sets.add(set); + stickerSections.add(new StickerSection(tdlib, set, Math.max(16, spanCount * 2))); } - }; - ProgressComponentView view = new ProgressComponentView(context); - view.initBig(1f); - view.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); - contentView.addView(view); - return new StickerHolder(contentView); - } - case TYPE_STICKER: { - StickerSmallView stickerSmallView = new StickerSmallView(context); - stickerSmallView.init(tdlib); - stickerSmallView.setStickerMovementCallback(callback); - stickerSmallView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - return new StickerHolder(stickerSmallView); + } + + if (loadStickerSetsListener != null) { + loadStickerSetsListener.runWithData(sets); + } + + buildCells(true); + if (headerView != null) { + headerView.setTitle(this); + } } - } - throw new IllegalArgumentException("viewType == " + viewType); + })); } } - private static class StickersAdapter extends RecyclerView.Adapter { - private final ViewController context; - private final StickerSmallView.StickerMovementCallback callback; - private final ArrayList stickers; - private final OffsetProvider provider; - private final RecyclerView parent; - public StickersAdapter (ViewController context, RecyclerView parent, StickerSmallView.StickerMovementCallback callback, OffsetProvider provider) { - this.context = context; - this.parent = parent; - this.callback = callback; - this.provider = provider; - this.stickers = new ArrayList<>(); - } - private boolean noProgress; + /* Listeners */ - public void setItems (final ArrayList stickers) { - noProgress = true; - notifyItemRemoved(1); - StickersAdapter.this.stickers.addAll(stickers); - notifyItemRangeInserted(1, stickers.size()); - } + @Override // todo:: update sticker pack + public void onStickerSetUpdated (TdApi.StickerSet stickerSet) { } - @Override - public StickerHolder onCreateViewHolder (ViewGroup parent, int viewType) { - return StickerHolder.create(context.context(), context.tdlib(), viewType, callback, provider); + @Override + public void onInstalledStickerSetsUpdated (long[] stickerSetIds, TdApi.StickerType stickerType) { + final LongSparseArray sets = new LongSparseArray<>(stickerSetIds.length); + for (long setId : stickerSetIds) { + sets.put(setId, null); } + runOnUiThreadOptional(() -> { + for (StickerSection stickerSection : stickerSections) { + if (stickerSection.info == null) continue; - @Override - public void onBindViewHolder (StickerHolder holder, int position) { - switch (holder.getItemViewType()) { - case StickerHolder.TYPE_STICKER: { - ((StickerSmallView) holder.itemView).setSticker(stickers.get(position - 1)); - break; + int i = sets.indexOfKey(stickerSection.info.getId()); + if (i >= 0) { + stickerSection.info.setIsInstalled(); + } else { + stickerSection.info.setIsNotInstalled(); } + adapter.updateDone(stickerSection.info); } + }); + } + + @Override + public void onStickerSetArchived (TdApi.StickerSetInfo stickerSet) { + final long stickerSetId = stickerSet.id; + runOnUiThreadOptional(() -> { + TGStickerSetInfo info = getStickerSetInfoById(stickerSetId); + if (info != null) { + info.setIsArchived(); + adapter.updateDone(info); + } + }); + } + + @Override + public void onStickerSetRemoved (TdApi.StickerSetInfo stickerSet) { + final long stickerSetId = stickerSet.id; + runOnUiThreadOptional(() -> { + TGStickerSetInfo info = getStickerSetInfoById(stickerSetId); + if (info != null) { + info.setIsNotInstalled(); + info.setIsNotArchived(); + adapter.updateDone(info); + } + }); + } + + @Override + public void onStickerSetInstalled (TdApi.StickerSetInfo stickerSet) { + final long stickerSetId = stickerSet.id; + runOnUiThreadOptional(() -> { + TGStickerSetInfo info = getStickerSetInfoById(stickerSetId); + if (info != null) { + info.setIsInstalled(); + adapter.updateDone(info); + } + }); + } + + + /* Data */ + + private final ArrayList stickerSections = new ArrayList<>(); + + private @Nullable TGStickerSetInfo getStickerSetInfoById (long id) { + for (StickerSection stickerSection : stickerSections) { + if (stickerSection.info != null && id == stickerSection.info.getId()) { + return stickerSection.info; + } + } + return null; + } + + private void buildCells (boolean needInfo) { + if (itemAnimatorRunnable != null) { + itemAnimatorRunnable.cancel(); + itemAnimatorRunnable = null; } - @Override - public void onViewAttachedToWindow (StickerHolder holder) { - switch (holder.getItemViewType()) { - case StickerHolder.TYPE_STICKER: { - ((StickerSmallView) holder.itemView).attach(); - break; + ArrayList items = new ArrayList<>(); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_PADDING_OFFSETABLE)); + if (stickerSections.isEmpty()) { + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_PROGRESS_OFFSETABLE)); + } else { + for (int a = 0; a < stickerSections.size(); a++) { + StickerSection section = stickerSections.get(a); + if (section.info != null && needInfo) { + section.info.setStartIndex(items.size()); } - case StickerHolder.TYPE_PROGRESS: { - ((ProgressComponentView) ((ViewGroup) holder.itemView).getChildAt(0)).attach(); - break; + items.addAll(section.toItems(needInfo)); + if (a != stickerSections.size() - 1 && !(section.info != null && section.info.isCollapsableEmojiSet() && section.info.isCollapsed())) { + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_SEPARATOR)); } } } - @Override - public void onViewDetachedFromWindow (StickerHolder holder) { - switch (holder.getItemViewType()) { - case StickerHolder.TYPE_STICKER: { - ((StickerSmallView) holder.itemView).detach(); - break; - } - case StickerHolder.TYPE_PROGRESS: { - ((ProgressComponentView) ((ViewGroup) holder.itemView).getChildAt(0)).detach(); - break; + adapter.setItems(items); + } + + private int getStickerSetSectionIndexById (long id) { + int i = 0; + for (StickerSection section : stickerSections) { + if (section.info != null && section.info.getId() == id) { + return i; + } + i++; + } + return -1; + } + + private static class StickerSection { + public @Nullable TGStickerSetInfo info; + public ArrayList stickers; + + public StickerSection (Tdlib tdlib, TdApi.StickerSet stickerSet, int trimToSize) { + this.info = new TGStickerSetInfo(tdlib, Td.toStickerSetInfo(stickerSet), trimToSize); + this.stickers = new ArrayList<>(stickerSet.stickers.length); + + for (TdApi.Sticker sticker : stickerSet.stickers) { + stickers.add(new TGStickerObj(tdlib, sticker, "", sticker.fullType)); + } + } + + public StickerSection (Tdlib tdlib, TdApi.Sticker[] stickers, TdApi.StickerType stickerType, TdApi.Emojis[] emojis, boolean canViewPack) { + this.stickers = new ArrayList<>(stickers.length); + int i = 0; + for (TdApi.Sticker sticker : stickers) { + TGStickerObj obj = new TGStickerObj(tdlib, sticker, stickerType, emojis[i].emojis); + if (!canViewPack) { + obj.setNoViewPack(); } + this.stickers.add(obj); + i++; } } - @Override - public void onViewRecycled (StickerHolder holder) { - switch (holder.getItemViewType()) { - case StickerHolder.TYPE_STICKER: { - ((StickerSmallView) holder.itemView).performDestroy(); - break; + public ArrayList toItems (boolean needInfo) { + ArrayList items = new ArrayList<>((info != null && needInfo ? 1 : 0) + stickers.size()); + if (info != null && needInfo) { + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_HEADER_TRENDING, info)); + } + + if (info != null && info.isCollapsed()) { + int i = 0; + for (TGStickerObj stickerObj : stickers) { + if (i++ == info.getSize()) { + break; + } + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, stickerObj)); } - case StickerHolder.TYPE_PROGRESS: { - ((ProgressComponentView) ((ViewGroup) holder.itemView).getChildAt(0)).performDestroy(); - break; + if (info.isCollapsableEmojiSet() && info.isCollapsed()) { + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_SEPARATOR_COLLAPSABLE, info)); + } + } else { + for (TGStickerObj stickerObj : stickers) { + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, stickerObj)); } } + + return items; } + } + + @Override + public void buildMenuStickerPreview (ArrayList menuItems, @NonNull TGStickerObj sticker, @NonNull StickerSmallView stickerSmallView) { + final NavigationController navigation = context.navigation(); + final ViewController c = navigation != null ? navigation.getCurrentStackItem() : null; + final boolean canWriteMessages = c instanceof MessagesController && ((MessagesController) c).canWriteMessages(); + + final boolean needViewPackButton = sticker.needViewPackButton(); + final boolean isEmoji = sticker.isCustomEmoji(); + + final @StringRes int sendText = isEmoji ? (canWriteMessages ? R.string.PasteCustomEmoji : R.string.ShareCustomEmoji) : R.string.SendSticker; - @Override - public int getItemViewType (int position) { - return stickers.isEmpty() ? (position == 1 ? StickerHolder.TYPE_PROGRESS : StickerHolder.TYPE_PADDING) : (position == 0 ? StickerHolder.TYPE_PADDING : StickerHolder.TYPE_STICKER); + menuItems.add(new StickerPreviewView.MenuItem(StickerPreviewView.MenuItem.MENU_ITEM_TEXT, + Lang.getString(sendText).toUpperCase(), R.id.btn_send, ColorId.textNeutral)); + + if (needViewPackButton) { + menuItems.add(new StickerPreviewView.MenuItem(StickerPreviewView.MenuItem.MENU_ITEM_TEXT, + Lang.getString(R.string.ViewPackPreview).toUpperCase(), R.id.btn_view, ColorId.textNeutral)); } + } - @Override - public int getItemCount () { - return stickers.isEmpty() ? noProgress ? 1 : 2 : 1 + stickers.size(); + @Override + public void onMenuStickerPreviewClick (View v, ViewController context, @NonNull TGStickerObj sticker, @NonNull StickerSmallView stickerSmallView) { + final int viewId = v.getId(); + if (viewId == R.id.btn_send) { + if (stickerSmallView.onSendSticker(v, sticker, Td.newSendOptions())) { + stickerSmallView.closePreviewIfNeeded(); + } + } else if (viewId == R.id.btn_view) { + if (context != null) { + tdlib.ui().showStickerSet(context, sticker.getStickerSetId(), null); + stickerSmallView.closePreviewIfNeeded(); + } } } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/StickersTrendingController.java b/app/src/main/java/org/thunderdog/challegram/ui/StickersTrendingController.java index 58aacb1c28..6b7da2457e 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/StickersTrendingController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/StickersTrendingController.java @@ -39,15 +39,32 @@ import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.util.CancellableResultHandler; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.StringUtils; +import me.vkryl.core.collection.LongSet; import me.vkryl.core.lambda.CancellableRunnable; +import me.vkryl.core.lambda.RunnableData; public class StickersTrendingController extends ViewController implements StickerSmallView.StickerMovementCallback, Client.ResultHandler, TGStickerObj.DataProvider, StickersListener, TGStickerSetInfo.ViewCallback { - public StickersTrendingController (Context context, Tdlib tdlib) { + private final boolean isEmoji; + private final boolean needKeyboardTop; + + public StickersTrendingController (Context context, Tdlib tdlib, boolean isEmoji) { super(context, tdlib); + this.isEmoji = isEmoji; + this.needKeyboardTop = false; + } + + public StickersTrendingController (Context context, Tdlib tdlib, boolean isEmoji, boolean needKeyboardTop) { + super(context, tdlib); + this.isEmoji = isEmoji; + this.needKeyboardTop = needKeyboardTop; } @Override @@ -60,20 +77,20 @@ public int getId () { @Override protected View onCreateView (Context context) { - adapter = new MediaStickersAdapter(this, this, true, this); + adapter = new MediaStickersAdapter(this, this, !isEmoji, this); adapter.setIsBig(); - GridLayoutManager manager = new GridLayoutManager(context, 5); + GridLayoutManager manager = new GridLayoutManager(context, getSpanCount()); manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize (int position) { - return adapter.getItemViewType(position) == MediaStickersAdapter.StickerHolder.TYPE_STICKER ? 1 : 5; + return adapter.getItemViewType(position) == MediaStickersAdapter.StickerHolder.TYPE_STICKER ? 1 : getSpanCount(); } }); recyclerView = (RecyclerView) Views.inflate(context(), R.layout.recycler, null); Views.setScrollBarPosition(recyclerView); - recyclerView.setItemAnimator(null); + // recyclerView.setItemAnimator(null); recyclerView.setLayoutManager(manager); recyclerView.setAdapter(adapter); ViewSupport.setThemedBackground(recyclerView, ColorId.filling, this); @@ -115,6 +132,10 @@ public boolean isTrendingLoaded () { return !loadingTrending; } + private int getSpanCount () { + return isEmoji ? 8 : 5; + } + @Override public void destroy () { super.destroy(); @@ -127,7 +148,7 @@ public void onRecentStickersUpdated (int[] stickerIds, boolean isAttached) { } @Override public void onInstalledStickerSetsUpdated (long[] stickerSetIds, TdApi.StickerType stickerType) { - if (stickerType.getConstructor() != TdApi.StickerTypeRegular.CONSTRUCTOR) + if (isNeedIgnoreStickersUpdate(stickerType)) return; final LongSparseArray sets = new LongSparseArray<>(stickerSetIds.length); for (long setId : stickerSetIds) { @@ -148,7 +169,7 @@ public void onInstalledStickerSetsUpdated (long[] stickerSetIds, TdApi.StickerTy @Override public void onStickerSetArchived (TdApi.StickerSetInfo stickerSet) { - if (stickerSet.stickerType.getConstructor() != TdApi.StickerTypeRegular.CONSTRUCTOR) + if (isNeedIgnoreStickersUpdate(stickerSet.stickerType)) return; final long stickerSetId = stickerSet.id; runOnUiThreadOptional(() -> { @@ -164,7 +185,7 @@ public void onStickerSetArchived (TdApi.StickerSetInfo stickerSet) { @Override public void onStickerSetRemoved (TdApi.StickerSetInfo removedStickerSet) { - if (removedStickerSet.stickerType.getConstructor() != TdApi.StickerTypeRegular.CONSTRUCTOR) + if (isNeedIgnoreStickersUpdate(removedStickerSet.stickerType)) return; final long removedStickerSetId = removedStickerSet.id; runOnUiThreadOptional(() -> { @@ -181,7 +202,7 @@ public void onStickerSetRemoved (TdApi.StickerSetInfo removedStickerSet) { @Override public void onStickerSetInstalled (TdApi.StickerSetInfo installedStickerSet) { - if (installedStickerSet.stickerType.getConstructor() != TdApi.StickerTypeRegular.CONSTRUCTOR) + if (isNeedIgnoreStickersUpdate(installedStickerSet.stickerType)) return; final long installedStickerSetId = installedStickerSet.id; runOnUiThreadOptional(() -> { @@ -199,60 +220,111 @@ public void onStickerSetInstalled (TdApi.StickerSetInfo installedStickerSet) { public boolean canFindChildViewUnder (StickerSmallView view, int recyclerX, int recyclerY) { return true; } - +/* @Override public void onTrendingStickersUpdated (final TdApi.StickerType stickerType, final TdApi.TrendingStickerSets stickerSets, int unreadCount) { - if (stickerType.getConstructor() != TdApi.StickerTypeRegular.CONSTRUCTOR || stickerSets.sets.length == 0) + if (isNeedIgnoreStickersUpdate(stickerType) || stickerSets.sets.length == 0) return; runOnUiThreadOptional(() -> { - if (!loadingTrending) { + if (!loadingTrending && StringUtils.isEmpty(searchRequest)) { loadTrending(0, 20, 0); } }); } +*/ @Override public void onFavoriteStickersUpdated (int[] stickerIds) { } private boolean loadingTrending, canLoadMoreTrending; + private @Nullable String searchRequest; + + public void search (@Nullable String request) { + if (trendingHandler != null) { + trendingHandler.cancel(); + } + loadingTrending = false; + canLoadMoreTrending = false; + stickerSets.clear(); + searchRequest = request; + if (getWrapUnchecked() != null) { + adapter.setItem(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_PROGRESS)); + loadTrending(0, 20, 0); + } + } + private void loadTrending (int offset, int limit, int cellCount) { if (!loadingTrending) { loadingTrending = true; - tdlib.client().send(new TdApi.GetTrendingStickerSets(new TdApi.StickerTypeRegular(), offset, limit), result -> { - switch (result.getConstructor()) { - case TdApi.TrendingStickerSets.CONSTRUCTOR: { - final TdApi.TrendingStickerSets trendingStickerSets = (TdApi.TrendingStickerSets) result; - final ArrayList stickerItems = new ArrayList<>(); - final ArrayList stickerSets; - - TdApi.StickerSetInfo[] sets = trendingStickerSets.sets; - if (sets.length > 0) { - stickerSets = new ArrayList<>(sets.length); - EmojiMediaListController.parseTrending(tdlib, stickerSets, stickerItems, cellCount, sets, this, this, true, false); - } else { - stickerSets = null; - if (offset == 0) - stickerItems.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_COME_AGAIN_LATER)); - } - tdlib.ui().post(() -> { - if (!isDestroyed()) { - addStickerSets(stickerSets, stickerItems, offset, cellCount); - getParentOrSelf().executeScheduledAnimation(); - } - }); - break; + TdApi.StickerType stickerType = getStickerType(); + String searchRequest = this.searchRequest; + CancellableResultHandler handler = trendingHandler(offset, cellCount, searchRequest); + + if (StringUtils.isEmpty(searchRequest)) { + tdlib.client().send( + new TdApi.GetTrendingStickerSets(stickerType, offset, limit), + handler + ); + return; + } + + // TODO: rework properly to tdlib.ui().getEmojiStickers(..) + + if (offset > 0) { + handler.onResult(new TdApi.StickerSets(0, new TdApi.StickerSetInfo[0])); + return; + } + + tdlib.send(new TdApi.SearchInstalledStickerSets(stickerType, searchRequest, 200), (foundInstalledStickerSets, error) -> { + if (handler.isCancelled()) + return; + if (error != null || foundInstalledStickerSets.sets.length == 0) { + tdlib.client().send(new TdApi.SearchStickerSets(stickerType, searchRequest), handler); + return; + } + tdlib.send(new TdApi.SearchStickerSets(stickerType, searchRequest), (foundStickerSets, error1) -> { + if (error1 != null || foundInstalledStickerSets.sets.length == 0) { + handler.onResult(foundInstalledStickerSets); + return; } - case TdApi.Error.CONSTRUCTOR: { - UI.showError(result); - break; + List stickerSets = new ArrayList<>(); + Collections.addAll(stickerSets, foundInstalledStickerSets.sets); + LongSet idsSet = new LongSet(foundInstalledStickerSets.sets.length); + for (TdApi.StickerSetInfo setInfo : foundInstalledStickerSets.sets) { + idsSet.add(setInfo.id); } - } + for (TdApi.StickerSetInfo setInfo : foundStickerSets.sets) { + if (!idsSet.has(setInfo.id)) { + stickerSets.add(setInfo); + } + } + TdApi.StickerSets mergedStickerSets = new TdApi.StickerSets( + foundInstalledStickerSets.totalCount + foundStickerSets.totalCount, + stickerSets.toArray(new TdApi.StickerSetInfo[0]) + ); + handler.onResult(mergedStickerSets); + }); }); } } + private TdApi.StickerType getStickerType () { + if (isEmoji) { + return new TdApi.StickerTypeCustomEmoji(); + } else { + return new TdApi.StickerTypeRegular(); + } + } + + private boolean isNeedIgnoreStickersUpdate (final TdApi.StickerType stickerType) { + if (isEmoji) { + return stickerType.getConstructor() != TdApi.StickerTypeCustomEmoji.CONSTRUCTOR; + } + return stickerType.getConstructor() != TdApi.StickerTypeRegular.CONSTRUCTOR; + } + private final ArrayList stickerSets = new ArrayList<>(); private void addStickerSets (@Nullable ArrayList stickerSets, ArrayList stickerItems, int offset, int cellCount) { @@ -426,4 +498,56 @@ public boolean needsLongDelay (StickerSmallView view) { public int getStickersListTop () { return Views.getLocationInWindow(recyclerView)[1]; } + + + + private CancellableResultHandler trendingHandler; + + private CancellableResultHandler trendingHandler (int offset, int cellCount, String searchRequest) { + return trendingHandler = new CancellableResultHandler() { + private void processResultImpl (TdApi.StickerSetInfo[] sets) { + final ArrayList stickerItems = new ArrayList<>(); + final ArrayList stickerSets; + + if (sets.length > 0) { + stickerSets = new ArrayList<>(sets.length); + if (offset == 0 && needKeyboardTop) + stickerItems.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_KEYBOARD_TOP)); + EmojiMediaListController.parseTrending(tdlib, stickerSets, stickerItems, cellCount, sets, StickersTrendingController.this, StickersTrendingController.this, true, false, searchRequest); + } else { + stickerSets = null; + if (offset == 0) + stickerItems.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_COME_AGAIN_LATER)); + } + + tdlib.ui().post(() -> { + if (!isDestroyed()) { + addStickerSets(stickerSets, stickerItems, offset, cellCount); + getParentOrSelf().executeScheduledAnimation(); + } + }); + } + + @Override + public void processResult (TdApi.Object result) { + switch (result.getConstructor()) { + case TdApi.TrendingStickerSets.CONSTRUCTOR: { + final TdApi.TrendingStickerSets trendingStickerSets = (TdApi.TrendingStickerSets) result; + processResultImpl(trendingStickerSets.sets); + break; + } + case TdApi.StickerSets.CONSTRUCTOR: { + final TdApi.StickerSets stickerSets = (TdApi.StickerSets) result; + processResultImpl(stickerSets.sets); + break; + } + case TdApi.Error.CONSTRUCTOR: { + UI.showError(result); + processResultImpl(new TdApi.StickerSetInfo[0]); + break; + } + } + } + }; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ThemeListController.java b/app/src/main/java/org/thunderdog/challegram/ui/ThemeListController.java index b250cd46d8..5787811423 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/ThemeListController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/ThemeListController.java @@ -324,7 +324,7 @@ private int parseAnyColor (String v) { String arg1 = matcher.group(2); String arg2 = matcher.group(3); String arg3 = matcher.group(4); - String arg4 = groupCount > 5 ? matcher.group(5): null; + String arg4 = groupCount > 5 ? matcher.group(5) : null; int alpha = arg4 != null ? parseAlpha(arg4) : 255; switch (type) { case "rgb": @@ -1490,7 +1490,7 @@ private ListItem newItem (ThemeDelegate theme, int id, boolean isProperty) { case ColorId.badge: case ColorId.badgeMuted: case ColorId.badgeFailed: - modifier.setCounter(id == ColorId.badgeFailed ? Tdlib.CHAT_FAILED: 1); + modifier.setCounter(id == ColorId.badgeFailed ? Tdlib.CHAT_FAILED : 1); modifier.noColorPreview = true; break; case ColorId.textSelectionHighlight: @@ -1662,7 +1662,7 @@ public void afterDraw (View view, Canvas c) { rectF.set(cx, cy, cx + width, cy + avatarRadius + avatarRadius); c.drawRoundRect(rectF, avatarRadius, avatarRadius, Paints.fillingPaint(theme.getColor(ColorId.headerRemoveBackground))); c.drawCircle(cx + avatarRadius, cy + avatarRadius, avatarRadius, Paints.fillingPaint(theme.getColor(ColorId.headerRemoveBackgroundHighlight))); - Drawables.draw(c, icon, cx + avatarRadius - icon.getMinimumWidth() / 2, cy + avatarRadius - icon.getMinimumHeight() / 2, Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, icon, cx + avatarRadius - icon.getMinimumWidth() / 2, cy + avatarRadius - icon.getMinimumHeight() / 2, Paints.whitePorterDuffPaint()); c.drawText(text, cx + avatarRadius * 2 + padding, cy + avatarRadius + Screen.dp(5f), paint); } }); @@ -1885,18 +1885,17 @@ private CharSequence makeDescription (@StringRes int resId) { if (entities != null) { List spans = new ArrayList<>(); for (TdApi.TextEntity entity : entities) { - switch (entity.type.getConstructor()) { - case TdApi.TextEntityTypeMention.CONSTRUCTOR: { - String username = text.subSequence(entity.offset + 1, entity.offset + entity.length).toString(); - spans.add(new ClickableSpan() { - @Override - public void onClick (@NonNull View widget) { - tdlib.ui().switchInline(ThemeListController.this, username, "", true); - } - }); - spans.add(new CustomTypefaceSpan(null, ColorId.textLink).setEntityType(entity.type).setRemoveUnderline(true)); - break; - } + if (Td.isMention(entity.type)) { + String username = text.subSequence(entity.offset + 1, entity.offset + entity.length).toString(); + spans.add(new ClickableSpan() { + @Override + public void onClick (@NonNull View widget) { + tdlib.ui().switchInline(ThemeListController.this, username, "", true); + } + }); + CustomTypefaceSpan span = new CustomTypefaceSpan(null, ColorId.textLink).setRemoveUnderline(true); + span.setTextEntityType(entity.type); + spans.add(span); } if (!spans.isEmpty()) { if (!(text instanceof SpannableStringBuilder)) { diff --git a/app/src/main/java/org/thunderdog/challegram/ui/TranslationControllerV2.java b/app/src/main/java/org/thunderdog/challegram/ui/TranslationControllerV2.java index e9e42e5820..990a4795ce 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/TranslationControllerV2.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/TranslationControllerV2.java @@ -38,14 +38,13 @@ import org.thunderdog.challegram.navigation.ToggleHeaderView2; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.RippleSupport; -import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.ColorState; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Fonts; -import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; @@ -182,7 +181,7 @@ protected void onDraw (Canvas canvas) { } linearLayout.addView(senderTextView, LayoutHelper.createLinear(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 2, Gravity.LEFT | Gravity.CENTER_VERTICAL)); - if (!message.isFakeMessage()) { + if (!message.isFakeMessage() && !message.isSponsoredMessage()) { dateTextView = new TextView(context); dateTextView.setTextColor(Theme.getColor(ColorId.textLight)); dateTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); @@ -246,7 +245,7 @@ public int getItemCount () { } text.replace(makeTextWrapper(currentText = originalText), false); - mTranslationsManager.requestTranslation(StringUtils.isEmpty(parent.defaultLanguageToTranslate) ? Lang.getDefaultLanguageToTranslateV2(messageOriginalLanguage): parent.defaultLanguageToTranslate); + mTranslationsManager.requestTranslation(StringUtils.isEmpty(parent.defaultLanguageToTranslate) ? Lang.getDefaultLanguageToTranslateV2(messageOriginalLanguage) : parent.defaultLanguageToTranslate); if (parent.translationApplyCallback != null) { // todo remove cond ??? wrapView.setPadding(0, 0, 0, Screen.needsKeyboardPadding(context()) ? Screen.getNavigationBarFrameDifference() : 0); } @@ -282,7 +281,7 @@ public void setHeaderPosition (float y) { } private void showTranslateOptions () { - int y = (int) Math.max(headerView != null ? headerView.getTranslationY(): 0, 0); + int y = (int) Math.max(headerView != null ? headerView.getTranslationY() : 0, 0); int maxY = parent.getTargetHeight() - Screen.dp(280 + 16); int pivotY = Screen.dp(8); if (y > maxY) { @@ -337,7 +336,7 @@ private void scrollCompensation (int heightDiff) { private void measureText (int width) { currentTextWidth = width; - for (ListAnimator.Entry entry: text) { + for (ListAnimator.Entry entry : text) { entry.item.prepare(width); entry.item.requestMedia(textMediaReceiver, 0, Integer.MAX_VALUE); } @@ -345,7 +344,7 @@ private void measureText (int width) { private int getTextAnimatedHeight () { float height = 0; - for (ListAnimator.Entry entry: text) { + for (ListAnimator.Entry entry : text) { height += entry.item.getHeight() * entry.getVisibility(); } return (int) height; @@ -684,7 +683,7 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { @Override protected void onDraw (Canvas canvas) { float alpha = translationCounterDrawable.getLoadingTextAlpha(); - for (ListAnimator.Entry entry: text) { + for (ListAnimator.Entry entry : text) { entry.item.draw(canvas, Screen.dp(18), Screen.dp(6), null, alpha * entry.getVisibility(), textMediaReceiver); } invalidate(); @@ -705,7 +704,7 @@ protected void onDetachedFromWindow () { @Override public boolean onTouchEvent (MotionEvent event) { if (super.onTouchEvent(event)) return true; - for (ListAnimator.Entry entry: text) { + for (ListAnimator.Entry entry : text) { if (entry.getVisibility() == 1f && entry.item.onTouchEvent(this, event)) { return true; } @@ -800,13 +799,13 @@ public LanguageAdapter (Context context, @Nullable ViewController themeProvid addLanguage(selected); addLanguage(original); - for (String lang: recents) { + for (String lang : recents) { if (StringUtils.equalsOrBothEmpty(lang, selected)) continue; if (StringUtils.equalsOrBothEmpty(lang, original)) continue; addLanguage(lang); } - for (String lang: Lang.getSupportedLanguagesForTranslate()) { + for (String lang : Lang.getSupportedLanguagesForTranslate()) { if (StringUtils.equalsOrBothEmpty(lang, selected)) continue; if (StringUtils.equalsOrBothEmpty(lang, original)) continue; if (recents.contains(lang)) continue; @@ -868,9 +867,9 @@ public void bind (String language, boolean isOriginal, boolean isSelected, boole languageView.isOriginal = isOriginal; languageView.isRecent = isRecent; languageView.titleView.setText(Lang.getLanguageName(language, language)); - /*languageView.titleView.setTranslationY(isOriginal ? -Screen.dp(9.5f): 0);*/ - languageView.subtitleView.setVisibility(/*isOriginal ? View.VISIBLE: */ View.GONE); - languageView.setPadding(Screen.dp(16), 0, Screen.dp((isSelected || isOriginal || isRecent) ? 40: 16), 0); + /*languageView.titleView.setTranslationY(isOriginal ? -Screen.dp(9.5f) : 0);*/ + languageView.subtitleView.setVisibility(/*isOriginal ? View.VISIBLE : */ View.GONE); + languageView.setPadding(Screen.dp(16), 0, Screen.dp((isSelected || isOriginal || isRecent) ? 40 : 16), 0); languageView.updateDrawable(); languageView.invalidate(); } @@ -932,7 +931,7 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { protected void dispatchDraw (Canvas canvas) { super.dispatchDraw(canvas); if (drawable != null) { - Drawables.draw(canvas, drawable, getMeasuredWidth() - Screen.dp(40), Screen.dp(13), Paints.getPorterDuffPaint(Theme.getColor(isSelected ? ColorId.iconActive: ColorId.icon))); + Drawables.draw(canvas, drawable, getMeasuredWidth() - Screen.dp(40), Screen.dp(13), PorterDuffPaint.get(isSelected ? ColorId.iconActive : ColorId.icon)); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/camera/CameraControlButton.java b/app/src/main/java/org/thunderdog/challegram/ui/camera/CameraControlButton.java index 492556027c..e1e9e25446 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/camera/CameraControlButton.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/camera/CameraControlButton.java @@ -281,7 +281,7 @@ protected void onDraw (Canvas c) { } } - Paint paint = isSmall ? Paints.getIconGrayPorterDuffPaint() : Paints.getPorterDuffPaint(0xffffffff); + Paint paint = isSmall ? Paints.getIconGrayPorterDuffPaint() : Paints.whitePorterDuffPaint(); if (changeFactor == 0f) { Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2, paint); } else { diff --git a/app/src/main/java/org/thunderdog/challegram/ui/camera/CameraController.java b/app/src/main/java/org/thunderdog/challegram/ui/camera/CameraController.java index 94b088e4f5..a56ef659d6 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/camera/CameraController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/camera/CameraController.java @@ -39,6 +39,7 @@ import android.widget.TextView; import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -51,7 +52,9 @@ import org.thunderdog.challegram.loader.ImageGalleryFile; import org.thunderdog.challegram.loader.ImageReader; import org.thunderdog.challegram.loader.ImageStrictCache; +import org.thunderdog.challegram.mediaview.AvatarPickerMode; import org.thunderdog.challegram.mediaview.MediaSelectDelegate; +import org.thunderdog.challegram.mediaview.MediaSendDelegate; import org.thunderdog.challegram.mediaview.MediaSpoilerSendDelegate; import org.thunderdog.challegram.mediaview.MediaViewController; import org.thunderdog.challegram.mediaview.MediaViewDelegate; @@ -126,6 +129,7 @@ public interface QrCodeListener { private boolean qrCodeConfirmed; private int qrSubtitleRes; private boolean qrModeDebug; + private @AvatarPickerMode int avatarPickerMode = AvatarPickerMode.NONE; public void setQrListener (@Nullable QrCodeListener qrCodeListener, @StringRes int subtitleRes, boolean qrModeDebug) { this.qrCodeListener = qrCodeListener; @@ -137,6 +141,16 @@ public void setQrListener (@Nullable QrCodeListener qrCodeListener, @StringRes i } } + public void setAvatarPickerMode (@AvatarPickerMode int avatarPickerMode) { + this.avatarPickerMode = avatarPickerMode; + } + + public void setMediaEditorDelegates (MediaViewDelegate delegate, MediaSelectDelegate selectDelegate, MediaSendDelegate sendDelegate) { + this.delegate = delegate; + this.selectDelegate = selectDelegate; + this.sendDelegate = sendDelegate; + } + public void setMode (int mode, @Nullable ReadyListener readyListener) { this.qrCodeConfirmed = false; this.readyListener = readyListener; @@ -285,9 +299,12 @@ private void setForceLegacy (boolean forceLegacy) { } } + private Throwable debugDestroy; + public void checkLegacyMode () { if (contentView != null && isLegacy() != needLegacy()) { if (manager != null) { + debugDestroy = Log.generateException(); contentView.removeView(this.manager.getView()); manager.destroy(); manager = null; @@ -330,11 +347,21 @@ public void setUseFastInitialization (boolean useFastInitialization) { } } - public CameraManager getManager () { + private void ensureManager () { + if (manager == null) { + if (debugDestroy != null) + throw new IllegalStateException(debugDestroy); + throw new IllegalStateException(); + } + } + + public @NonNull CameraManager getManager () { + ensureManager(); return manager; } - public CameraManagerLegacy getLegacyManager () { + public @NonNull CameraManagerLegacy getLegacyManager () { + ensureManager(); return (CameraManagerLegacy) manager; } @@ -1441,6 +1468,10 @@ private boolean onSendMedia (ImageGalleryFile file, TdApi.MessageSendOptions opt return false; } + public MediaViewDelegate delegate; + public MediaSelectDelegate selectDelegate; + public MediaSendDelegate sendDelegate; + @Override public void onMediaTaken (final ImageGalleryFile file) { boolean awaitLayout = applyFakeRotation(); @@ -1452,7 +1483,7 @@ public void onMediaTaken (final ImageGalleryFile file) { MediaItem item = new MediaItem(context, tdlib, file); stack.set(item); MessagesController m = findOutputController(); - MediaViewController.Args args = new MediaViewController.Args(CameraController.this, MediaViewController.MODE_GALLERY, new MediaViewDelegate() { + MediaViewController.Args args = MediaViewController.Args.fromGallery(CameraController.this, delegate != null ? delegate : new MediaViewDelegate() { @Override public MediaViewThumbLocation getTargetLocation (int indexInStack, MediaItem item) { MediaViewThumbLocation location = new MediaViewThumbLocation(0, 0, contentView.getMeasuredWidth(), contentView.getMeasuredHeight()); @@ -1465,7 +1496,7 @@ public MediaViewThumbLocation getTargetLocation (int indexInStack, MediaItem ite public void setMediaItemVisible (int index, MediaItem item, boolean isVisible) { } - }, new MediaSelectDelegate() { + }, selectDelegate != null ? selectDelegate : new MediaSelectDelegate() { @Override public boolean isMediaItemSelected (int index, MediaItem item) { return false; @@ -1501,13 +1532,13 @@ public long getOutputChatId () { public ArrayList getSelectedMediaItems (boolean copy) { return null; } - }, new MediaSpoilerSendDelegate() { + }, sendDelegate != null ? sendDelegate : new MediaSpoilerSendDelegate() { @Override public boolean sendSelectedItems (View view, ArrayList images, TdApi.MessageSendOptions options, boolean disableMarkdown, boolean asFiles, boolean hasSpoiler) { ImageGalleryFile galleryFile = (ImageGalleryFile) images.get(0); return onSendMedia(galleryFile, options, disableMarkdown, asFiles, hasSpoiler); } - }, stack).setOnlyScheduled(m != null && m.areScheduledOnly()); + }, stack, m != null && m.areScheduledOnly()).setAvatarPickerMode(avatarPickerMode); if (m != null) { args.setReceiverChatId(m.getChatId()); } diff --git a/app/src/main/java/org/thunderdog/challegram/unsorted/NLoader.java b/app/src/main/java/org/thunderdog/challegram/unsorted/NLoader.java index b8e21aa52e..b34ed9c662 100644 --- a/app/src/main/java/org/thunderdog/challegram/unsorted/NLoader.java +++ b/app/src/main/java/org/thunderdog/challegram/unsorted/NLoader.java @@ -68,8 +68,8 @@ public static synchronized boolean loadLibrary () { try { ReLinkerInstance reLinker = ReLinker.recursively().log(NLoader.instance()); loadLibraryImpl(reLinker, "c++_shared", BuildConfig.NDK_VERSION); - loadLibraryImpl(reLinker, "cryptox", BuildConfig.OPENSSL_VERSION); - loadLibraryImpl(reLinker, "sslx", BuildConfig.OPENSSL_VERSION); + loadLibraryImpl(reLinker, "cryptox", BuildConfig.OPENSSL_VERSION_FULL); + loadLibraryImpl(reLinker, "sslx", BuildConfig.OPENSSL_VERSION_FULL); loadLibraryImpl(reLinker, "tdjni", BuildConfig.TDLIB_VERSION); loadLibraryImpl(reLinker, "leveldbjni", BuildConfig.LEVELDB_VERSION); loadLibraryImpl(reLinker, "tgcallsjni", BuildConfig.JNI_VERSION /*TODO: separate variable?*/); diff --git a/app/src/main/java/org/thunderdog/challegram/unsorted/Passcode.java b/app/src/main/java/org/thunderdog/challegram/unsorted/Passcode.java index 3aa7cc1b04..f883f9fec7 100644 --- a/app/src/main/java/org/thunderdog/challegram/unsorted/Passcode.java +++ b/app/src/main/java/org/thunderdog/challegram/unsorted/Passcode.java @@ -136,7 +136,7 @@ public void setDisplayNotifications (boolean display) { @Override public void onUiStateChanged (int newState) { - if (newState == UI.STATE_PAUSED) { + if (newState == UI.State.PAUSED) { trackUserActivity(true); } } @@ -268,7 +268,7 @@ public boolean isLocked () { public boolean isLockedAndVisible () { if (isLocked()) { BaseActivity activity = UI.getUiContext(); - return UI.getUiState() != UI.STATE_RESUMED || activity == null || activity.isPasscodeShowing(); + return UI.getUiState() != UI.State.RESUMED || activity == null || activity.isPasscodeShowing(); } return false; } diff --git a/app/src/main/java/org/thunderdog/challegram/unsorted/Settings.java b/app/src/main/java/org/thunderdog/challegram/unsorted/Settings.java index 19dafce2b4..717e7d7d01 100644 --- a/app/src/main/java/org/thunderdog/challegram/unsorted/Settings.java +++ b/app/src/main/java/org/thunderdog/challegram/unsorted/Settings.java @@ -50,6 +50,8 @@ import org.thunderdog.challegram.emoji.RecentInfo; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.player.TGPlayerController; +import org.thunderdog.challegram.telegram.ChatFolderOptions; +import org.thunderdog.challegram.telegram.ChatFolderStyle; import org.thunderdog.challegram.telegram.EmojiMediaType; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibAccount; @@ -97,7 +99,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -106,6 +107,7 @@ import me.vkryl.core.ColorUtils; import me.vkryl.core.DateUtils; import me.vkryl.core.FileUtils; +import me.vkryl.core.ObjectUtils; import me.vkryl.core.StringUtils; import me.vkryl.core.lambda.CancellableRunnable; import me.vkryl.core.lambda.RunnableBool; @@ -174,7 +176,10 @@ public class Settings { private static final int VERSION_40 = 40; // drop legacy crash management ids private static final int VERSION_41 = 41; // clear all application log files private static final int VERSION_42 = 42; // drop __ - private static final int VERSION = VERSION_42; + private static final int VERSION_43 = 43; // optimize recent custom emoji + private static final int VERSION_44 = 44; // 8-bit -> 32-bit account flags + private static final int VERSION_45 = 45; // Reset "Big emoji" setting to default + private static final int VERSION = VERSION_45; private static final AtomicBoolean hasInstance = new AtomicBoolean(false); private static volatile Settings instance; @@ -195,10 +200,13 @@ public static Settings instance () { private static final String KEY_VERSION = "version"; private static final String KEY_OTHER = "settings_other"; private static final String KEY_OTHER_NEW = "settings_other2"; + private static final String KEY_EXPERIMENTS = "settings_experiments"; private static final @Deprecated String KEY_MARKDOWN_MODE = "settings_markdown"; private static final String KEY_MAP_PROVIDER_TYPE = "settings_map_provider"; private static final String KEY_MAP_PROVIDER_TYPE_CLOUD = "settings_map_provider_cloud"; private static final String KEY_STICKER_MODE = "settings_sticker"; + private static final String KEY_EMOJI_MODE = "settings_emoji"; + private static final String KEY_REACTION_AVATARS_MODE = "settings_reaction_avatars"; private static final String KEY_AUTO_UPDATE_MODE = "settings_auto_update"; private static final String KEY_INCOGNITO = "settings_incognito"; private static final String KEY_NIGHT_MODE = "settings_night_mode"; @@ -230,6 +238,8 @@ public static Settings instance () { private static final String KEY_CAMERA_ASPECT_RATIO = "settings_camera_ratio"; private static final String KEY_CAMERA_TYPE = "settings_camera_type"; private static final String KEY_CAMERA_VOLUME_CONTROL = "settings_camera_control"; + private static final String KEY_CHAT_FOLDER_STYLE = "settings_folders_style"; + private static final String KEY_CHAT_FOLDER_OPTIONS = "settings_folders_options"; private static final String KEY_TDLIB_VERBOSITY = "settings_tdlib_verbosity"; private static final String KEY_TDLIB_DEBUG_PREFIX = "settings_tdlib_allow_debug"; @@ -244,6 +254,11 @@ public static Settings instance () { public static final String KEY_ACCOUNT_INFO_SUFFIX_FLAGS = "flags"; // premium, verified, etc public static final String KEY_ACCOUNT_INFO_SUFFIX_NAME1 = "name1"; // first_name public static final String KEY_ACCOUNT_INFO_SUFFIX_NAME2 = "name2"; // last_name + public static final String KEY_ACCOUNT_INFO_SUFFIX_ACCENT_COLOR_ID = "accent_id"; // accent_color_id + public static final String KEY_ACCOUNT_INFO_SUFFIX_ACCENT_BUILT_IN_ACCENT_COLOR_ID = "accent_builtin"; // accent_color_id + public static final String KEY_ACCOUNT_INFO_SUFFIX_LIGHT_THEME_COLORS = "accent_light"; // accent_light + public static final String KEY_ACCOUNT_INFO_SUFFIX_DARK_THEME_COLORS = "accent_dark"; // accent_dark + public static final String KEY_ACCOUNT_INFO_SUFFIX_MIN_CHAT_BOOST_LEVEL = "min_boost_level"; // min_chat_boost_level public static final String KEY_ACCOUNT_INFO_SUFFIX_USERNAME = "username"; // username public static final String KEY_ACCOUNT_INFO_SUFFIX_USERNAMES_ACTIVE = "usernames_active"; // username public static final String KEY_ACCOUNT_INFO_SUFFIX_USERNAMES_DISABLED = "usernames_disabled"; // last_name @@ -307,7 +322,7 @@ public static String accountInfoPrefix (int accountId) { private static final @Deprecated String KEY_PUSH_USER_IDS = "push_user_ids"; private static final @Deprecated String KEY_PUSH_USER_ID = "push_user_id"; private static final String KEY_PUSH_DEVICE_TOKEN_TYPE = "push_device_token_type"; - private static final String KEY_PUSH_DEVICE_TOKEN = "push_device_token"; + private static final String KEY_PUSH_DEVICE_TOKEN_OR_ENDPOINT = "push_device_token"; private static final String KEY_PUSH_STATS_TOTAL_COUNT = "push_stats_total"; private static final String KEY_PUSH_STATS_CURRENT_APP_VERSION_COUNT = "push_stats_app"; private static final String KEY_PUSH_STATS_CURRENT_TOKEN_COUNT = "push_stats_token"; @@ -389,6 +404,7 @@ private static String key (String key, int accountId) { public static final long SETTING_FLAG_FORCE_EXO_PLAYER_EXTENSIONS = 1 << 7; public static final long SETTING_FLAG_NO_AUDIO_COMPRESSION = 1 << 8; public static final long SETTING_FLAG_DOWNLOAD_BETAS = 1 << 9; + public static final long SETTING_FLAG_NO_ANIMATED_EMOJI_LOOP = 1 << 10; public static final long SETTING_FLAG_CAMERA_NO_FLIP = 1 << 10; public static final long SETTING_FLAG_CAMERA_KEEP_DISCARDED_MEDIA = 1 << 11; @@ -398,6 +414,10 @@ private static String key (String key, int accountId) { public static final long SETTING_FLAG_LIMIT_STICKERS_FPS = 1 << 14; public static final long SETTING_FLAG_EXPAND_RECENT_STICKERS = 1 << 15; + public static final long EXPERIMENT_FLAG_ALLOW_EXPERIMENTS = 1; + public static final long EXPERIMENT_FLAG_ENABLE_FOLDERS = 1 << 1; + public static final long EXPERIMENT_FLAG_SHOW_PEER_IDS = 1 << 2; + private static final @Deprecated int DISABLED_FLAG_OTHER_NEED_RAISE_TO_SPEAK = 1 << 2; private static final @Deprecated int DISABLED_FLAG_OTHER_AUTODOWNLOAD_IN_BACKGROUND = 1 << 3; private static final @Deprecated int DISABLED_FLAG_OTHER_DEFAULT_CRASH_MANAGER = 1 << 5; @@ -416,7 +436,7 @@ private static String key (String key, int accountId) { @Nullable private Integer _settings; @Nullable - private Long _newSettings; + private Long _newSettings, _experiments; public static final int NIGHT_MODE_NONE = 0; public static final int NIGHT_MODE_AUTO = 1; @@ -437,8 +457,14 @@ private static String key (String key, int accountId) { public static final int STICKER_MODE_ONLY_INSTALLED = 1; public static final int STICKER_MODE_NONE = 2; - @Nullable - private Integer _stickerMode; + @Nullable private Integer _stickerMode; + @Nullable private Integer _emojiMode; + + public static final int REACTION_AVATARS_MODE_NEVER = 0; + public static final int REACTION_AVATARS_MODE_SMART_FILTER = 1; + public static final int REACTION_AVATARS_MODE_ALWAYS = 2; + + @Nullable private Integer _reactionAvatarsMode; public static final int AUTO_UPDATE_MODE_PROMPT = 0; public static final int AUTO_UPDATE_MODE_NEVER = 1; @@ -473,6 +499,7 @@ private static String key (String key, int accountId) { public static final long TUTORIAL_BRUSH_COLOR_TONE = 1 << 17; public static final long TUTORIAL_QR_SCAN = 1 << 18; public static final long TUTORIAL_SELECT_LANGUAGE_INLINE_MODE = 1 << 19; + public static final long TUTORIAL_MULTIPLE_LINK_PREVIEWS = 1 << 20; @Nullable private Long _tutorialFlags; @@ -563,12 +590,12 @@ public void setMaxFileSize (long bytes) { public List getModules () { List modules; - TdApi.Object object = Client.execute(new TdApi.GetLogTags()); - if (object instanceof TdApi.LogTags) { - String[] tags = ((TdApi.LogTags) object).tags; + try { + TdApi.LogTags logTags = Client.execute(new TdApi.GetLogTags()); + String[] tags = logTags.tags; modules = new ArrayList<>(tags.length + (_modules != null ? _modules.size() : 0)); Collections.addAll(modules, tags); - } else { + } catch (Client.ExecutionException error) { modules = new ArrayList<>(_modules != null ? _modules.size() : 0); } if (_modules != null) { @@ -582,13 +609,21 @@ public List getModules () { } private boolean setLogTagVerbosityLevel (String module, int verbosityLevel) { - TdApi.Object result = Client.execute(new TdApi.SetLogTagVerbosityLevel(module, verbosityLevel)); - return result instanceof TdApi.Ok; + try { + Client.execute(new TdApi.SetLogTagVerbosityLevel(module, verbosityLevel)); + return true; + } catch (Client.ExecutionException error) { + return false; + } } private boolean setLogVerbosityLevel (int globalVerbosityLevel) { - TdApi.Object result = Client.execute(new TdApi.SetLogVerbosityLevel(globalVerbosityLevel)); - return result instanceof TdApi.Ok; + try { + Client.execute(new TdApi.SetLogVerbosityLevel(globalVerbosityLevel)); + return true; + } catch (Client.ExecutionException error) { + return false; + } } public int getVerbosity (@Nullable String module) { @@ -630,8 +665,9 @@ public void setVerbosity (@Nullable String module, int verbosity) { int defaultVerbosityLevel = value != null ? value[1] : queryLogVerbosityLevel(module); int currentVerbosityLevel = value != null ? value[0] : defaultVerbosityLevel; if (verbosity != currentVerbosityLevel) { - TdApi.Object result = Client.execute(new TdApi.SetLogTagVerbosityLevel(module, verbosity)); - if (result instanceof TdApi.Ok) { + try { + Client.execute(new TdApi.SetLogTagVerbosityLevel(module, verbosity)); + if (value != null) value[0] = verbosity; else @@ -640,7 +676,7 @@ public void setVerbosity (@Nullable String module, int verbosity) { remove(verbosityKey + "_" + module); else putInt(verbosityKey + "_" + module, verbosity); - } + } catch (Client.ExecutionException ignored) { } } } } @@ -658,10 +694,13 @@ public void reset () { } private int queryLogVerbosityLevel (@Nullable String module) { - TdApi.Object object = Client.execute(StringUtils.isEmpty(module) ? new TdApi.GetLogVerbosityLevel() : new TdApi.GetLogTagVerbosityLevel(module)); - if (object instanceof TdApi.LogVerbosityLevel) - return ((TdApi.LogVerbosityLevel) object).verbosityLevel; - return TDLIB_LOG_VERBOSITY_UNKNOWN; + try { + TdApi.Function function = StringUtils.isEmpty(module) ? new TdApi.GetLogVerbosityLevel() : new TdApi.GetLogTagVerbosityLevel(module); + TdApi.LogVerbosityLevel logVerbosityLevel = Client.execute(function); + return logVerbosityLevel.verbosityLevel; + } catch (Client.ExecutionException error) { + return TDLIB_LOG_VERBOSITY_UNKNOWN; + } } public void apply (boolean async) { @@ -702,10 +741,11 @@ public void apply (boolean async) { stream = new TdApi.LogStreamEmpty(); } } - TdApi.Object result = Client.execute(new TdApi.SetLogStream(stream)); - if (result.getConstructor() == TdApi.Error.CONSTRUCTOR) { + try { + Client.execute(new TdApi.SetLogStream(stream)); + } catch (Client.ExecutionException error) { Runnable act = () -> { - Tracer.onTdlibFatalError(null, TdApi.SetLogStream.class, (TdApi.Error) result, new RuntimeException().getStackTrace()); + Tracer.onTdlibFatalError(null, TdApi.SetLogStream.class, error.error, new RuntimeException().getStackTrace()); }; if (async) { UI.post(act); @@ -887,6 +927,14 @@ public int getInt (String key, int defValue) { return pmc.getInt(key, defValue); } + public int[] getIntArray (String key) { + return pmc.getIntArray(key); + } + + public void putIntArray (String key, int[] value) { + pmc.putIntArray(key, value); + } + public void putFloat (String key, float value) { pmc.putFloat(key, value).apply(); } @@ -1194,6 +1242,65 @@ public void setChatListMode (int mode) { } } + + public interface ChatFolderSettingsListener { + default void onChatFolderOptionsChanged (@ChatFolderOptions int newOptions) {} + default void onChatFolderStyleChanged (@ChatFolderStyle int newStyle) {} + } + private final ReferenceList chatFolderSettingsListeners = new ReferenceList<>(); + + public void addChatFolderSettingsListener (ChatFolderSettingsListener listener) { + chatFolderSettingsListeners.add(listener); + } + + public void removeChatFolderSettingsListener (ChatFolderSettingsListener listener) { + chatFolderSettingsListeners.remove(listener); + } + + private Integer _chatFolderOptions, _chatFolderStyle; + + public void setChatFolderOptions (@ChatFolderOptions int options) { + if (getChatFolderOptions() != options) { + if (options == TdlibSettingsManager.DEFAULT_CHAT_FOLDER_OPTIONS) { + pmc.remove(KEY_CHAT_FOLDER_OPTIONS); + } else { + pmc.putInt(KEY_CHAT_FOLDER_OPTIONS, options); + } + _chatFolderOptions = options; + for (ChatFolderSettingsListener listener : chatFolderSettingsListeners) { + listener.onChatFolderOptionsChanged(options); + } + } + } + + public @ChatFolderOptions int getChatFolderOptions () { + if (_chatFolderOptions == null) { + _chatFolderOptions = pmc.getInt(KEY_CHAT_FOLDER_OPTIONS, TdlibSettingsManager.DEFAULT_CHAT_FOLDER_OPTIONS); + } + return _chatFolderOptions; + } + + public void setChatFolderStyle (@ChatFolderStyle int style) { + if (getChatFolderStyle() != style) { + if (style == TdlibSettingsManager.DEFAULT_CHAT_FOLDER_STYLE) { + pmc.remove(KEY_CHAT_FOLDER_STYLE); + } else { + pmc.putInt(KEY_CHAT_FOLDER_STYLE, style); + } + _chatFolderStyle = style; + for (ChatFolderSettingsListener listener : chatFolderSettingsListeners) { + listener.onChatFolderStyleChanged(style); + } + } + } + + public @ChatFolderStyle int getChatFolderStyle () { + if (_chatFolderStyle == null) { + _chatFolderStyle = pmc.getInt(KEY_CHAT_FOLDER_STYLE, TdlibSettingsManager.DEFAULT_CHAT_FOLDER_STYLE); + } + return _chatFolderStyle; + } + private long makeDefaultNewSettings () { long settings = 0; @@ -1225,6 +1332,34 @@ private boolean setNewSettings (long newSettings) { return false; } + private static long makeDefaultExperiments () { + // TODO: this flag allows implementing later a global toggle that enables/disables all experiments + // while preserving specific experiments toggle values. + return EXPERIMENT_FLAG_ALLOW_EXPERIMENTS; + } + + private long getExperiments () { + if (_experiments == null) + _experiments = pmc.getLong(KEY_EXPERIMENTS, makeDefaultExperiments()); + return _experiments; + } + + public boolean isExperimentEnabled (long key) { + long experiments = getExperiments(); + return BitwiseUtils.hasAllFlags(experiments, EXPERIMENT_FLAG_ALLOW_EXPERIMENTS | key); + } + + public boolean setExperimentEnabled (long key, boolean enabled) { + long oldExperiments = getExperiments(); + long newExperiments = BitwiseUtils.setFlag(oldExperiments, key, enabled); + if (oldExperiments != newExperiments) { + this._experiments = newExperiments; + pmc.putLong(KEY_EXPERIMENTS, newExperiments); + return true; + } + return false; + } + public interface SettingsChangeListener { void onSettingsChanged (long newSettings, long oldSettings); } @@ -1527,6 +1662,8 @@ public LevelDB pmc () { return pmc; } + private boolean ignoreFurtherAccountConfigUpgrades; + private void upgradePmc (LevelDB pmc, SharedPreferences.Editor editor, int version) { switch (version) { case VERSION_10: { @@ -1708,7 +1845,7 @@ private void upgradePmc (LevelDB pmc, SharedPreferences.Editor editor, int versi case VERSION_24: { int accountNum = TdlibManager.readAccountNum(); for (int accountId = 0; accountId < accountNum; accountId++) { - editor.remove(TdlibSettingsManager.key(TdlibSettingsManager.DEVICE_TOKEN_KEY, accountId)); + editor.remove(TdlibSettingsManager.key(TdlibSettingsManager.DEVICE_TOKEN_OR_ENDPOINT_KEY, accountId)); editor.remove(TdlibSettingsManager.key(TdlibSettingsManager.DEVICE_UID_KEY, accountId)); editor.remove(TdlibSettingsManager.key(TdlibSettingsManager.DEVICE_OTHER_UID_KEY, accountId)); } @@ -1956,36 +2093,7 @@ public TGWallpaper restoreWallpaper (Tdlib tdlib, int usageIdentifier) { } } - File oldConfigFile = TdlibManager.getAccountConfigFile(); - File backupFile = new File(oldConfigFile.getParentFile(), oldConfigFile.getName() + ".bak." + TdlibAccount.VERSION_1); - if (oldConfigFile.exists() && !backupFile.exists()) { - TdlibManager.AccountConfig config = null; - try (RandomAccessFile r = new RandomAccessFile(oldConfigFile, TdlibManager.MODE_R)) { - config = TdlibManager.readAccountConfig(null, r, TdlibAccount.VERSION_1, false); - } catch (IOException e) { - Log.e(e); - } - if (config != null) { - File newConfigFile = new File(oldConfigFile.getParentFile(), oldConfigFile.getName() + ".tmp"); - try { - if (newConfigFile.exists() || newConfigFile.createNewFile()) { - try (RandomAccessFile r = new RandomAccessFile(newConfigFile, TdlibManager.MODE_RW)) { - TdlibManager.writeAccountConfigFully(r, config); - } catch (IOException e) { - Tracer.onLaunchError(e); - throw new DeviceStorageError(e); - } - } - if (!oldConfigFile.renameTo(backupFile)) - throw new DeviceStorageError("Cannot backup old config"); - if (!newConfigFile.renameTo(oldConfigFile)) - throw new DeviceStorageError("Cannot save new config"); - } catch (Throwable t) { - Tracer.onLaunchError(t); - throw new DeviceStorageError(t); - } - } - } + upgradeAccountsConfig(TdlibAccount.VERSION_1); break; } case VERSION_39: { @@ -2011,6 +2119,96 @@ public TGWallpaper restoreWallpaper (Tdlib tdlib, int usageIdentifier) { } break; } + case VERSION_43: { + String[] emojis = pmc.getStringArray(KEY_EMOJI_RECENTS); + if (emojis != null && emojis.length > 0) { + Map infos = new HashMap<>(); + getBinaryMap(KEY_EMOJI_COUNTERS, infos, RecentInfo.class); + + int changedCount = 0; + int changedEmojiCounters = 0; + for (int index = 0; index < emojis.length; index++) { + final String oldEmoji = emojis[index]; + // Save 15*2 bytes per recent custom emoji by simply reducing prefix size + if (oldEmoji.startsWith(Emoji.CUSTOM_EMOJI_CACHE_OLD)) { + String newEmoji = Emoji.CUSTOM_EMOJI_CACHE + oldEmoji.substring(Emoji.CUSTOM_EMOJI_CACHE_OLD.length()); + emojis[index] = newEmoji; + changedCount++; + + RecentInfo recentInfo = infos.remove(oldEmoji); + if (recentInfo != null) { + infos.put(newEmoji, recentInfo); + changedEmojiCounters++; + } + } + } + if (changedCount > 0) { + pmc.putStringArray(KEY_EMOJI_RECENTS, emojis); + } + if (changedEmojiCounters > 0) { + saveBinaryMap(KEY_EMOJI_COUNTERS, infos); + } + } + break; + } + case VERSION_44: { + upgradeAccountsConfig(TdlibAccount.VERSION_2); + break; + } + case VERSION_45: { + resetOtherFlag(pmc, editor, FLAG_OTHER_DISABLE_BIG_EMOJI, false); + break; + } + } + } + + private void resetOtherFlag (LevelDB pmc, SharedPreferences.Editor editor, int flag, boolean value) { + int defaultSettings = makeDefaultSettings(); + int oldSettings = pmc.getInt(KEY_OTHER, defaultSettings); + int newSettings = BitwiseUtils.setFlag(oldSettings, flag, value); + if (oldSettings != newSettings) { + if (newSettings != defaultSettings) { + editor.putInt(KEY_OTHER, newSettings); + } else { + editor.remove(KEY_OTHER); + } + } + } + + private void upgradeAccountsConfig (int fromConfigVersion) { + if (ignoreFurtherAccountConfigUpgrades) { + return; + } + File oldConfigFile = TdlibManager.getAccountConfigFile(); + File backupFile = new File(oldConfigFile.getParentFile(), oldConfigFile.getName() + ".bak." + fromConfigVersion); + if (oldConfigFile.exists() && !backupFile.exists()) { + TdlibManager.AccountConfig config = null; + try (RandomAccessFile r = new RandomAccessFile(oldConfigFile, TdlibManager.MODE_R)) { + config = TdlibManager.readAccountConfig(null, r, fromConfigVersion, false); + } catch (IOException e) { + Log.e(e); + } + if (config != null) { + File newConfigFile = new File(oldConfigFile.getParentFile(), oldConfigFile.getName() + ".tmp"); + try { + if (newConfigFile.exists() || newConfigFile.createNewFile()) { + try (RandomAccessFile r = new RandomAccessFile(newConfigFile, TdlibManager.MODE_RW)) { + TdlibManager.writeAccountConfigFully(r, config); + ignoreFurtherAccountConfigUpgrades = true; + } catch (IOException e) { + Tracer.onLaunchError(e); + throw new DeviceStorageError(e); + } + } + if (!oldConfigFile.renameTo(backupFile)) + throw new DeviceStorageError("Cannot backup old config"); + if (!newConfigFile.renameTo(oldConfigFile)) + throw new DeviceStorageError("Cannot save new config"); + } catch (Throwable t) { + Tracer.onLaunchError(t); + throw new DeviceStorageError(t); + } + } } } @@ -2393,7 +2591,8 @@ public boolean needTutorial (@NonNull TdApi.ChatSource source) { case TdApi.ChatSourceMtprotoProxy.CONSTRUCTOR: return needTutorial(TUTORIAL_PROXY_SPONSOR); default: - throw new UnsupportedOperationException(source.toString()); + Td.assertChatSource_12b21238(); + throw Td.unsupported(source); } } @@ -2578,6 +2777,32 @@ public void setStickerMode (int mode) { } } + public int getEmojiMode () { + if (_emojiMode == null) + _emojiMode = pmc.getInt(KEY_EMOJI_MODE, STICKER_MODE_ALL); + return _emojiMode; + } + + public void setEmojiMode (int mode) { + this._emojiMode = mode; + if (mode == STICKER_MODE_ALL) { + remove(KEY_EMOJI_MODE); + } else { + putInt(KEY_EMOJI_MODE, mode); + } + } + + public int getReactionAvatarsMode () { + if (_reactionAvatarsMode == null) + _reactionAvatarsMode = pmc.getInt(KEY_REACTION_AVATARS_MODE, REACTION_AVATARS_MODE_SMART_FILTER); + return _reactionAvatarsMode; + } + + public void setReactionAvatarsMode (int mode) { + this._reactionAvatarsMode = mode; + putInt(KEY_REACTION_AVATARS_MODE, mode); + } + public int getAutoUpdateMode () { if (_autoUpdateMode == null) _autoUpdateMode = pmc.getInt(KEY_AUTO_UPDATE_MODE, AUTO_UPDATE_MODE_PROMPT); @@ -2917,7 +3142,7 @@ public boolean isUnlimited () { @Override public int hashCode () { - return Objects.hash(majorSize, minorSize); + return ObjectUtils.hash(majorSize, minorSize); } } @@ -2963,7 +3188,7 @@ public boolean equals (Object o) { @Override public int hashCode () { - return Objects.hash(size, fps, bitrate); + return ObjectUtils.hashCode(size, fps, bitrate); } public VideoLimit () { @@ -4359,8 +4584,11 @@ public static String getProxyUsername (@NonNull TdApi.ProxyType type) { return ((TdApi.ProxyTypeHttp) type).username; case TdApi.ProxyTypeMtproto.CONSTRUCTOR: return null; + default: { + Td.assertProxyType_bc1a1076(); + throw Td.unsupported(type); + } } - throw new UnsupportedOperationException(type.toString()); } public static String getProxyPassword (@NonNull TdApi.ProxyType type) { @@ -4371,8 +4599,11 @@ public static String getProxyPassword (@NonNull TdApi.ProxyType type) { return ((TdApi.ProxyTypeHttp) type).password; case TdApi.ProxyTypeMtproto.CONSTRUCTOR: return null; + default: { + Td.assertProxyType_bc1a1076(); + throw Td.unsupported(type); + } } - throw new UnsupportedOperationException(type.toString()); } public static int getProxyDefaultOrder (@NonNull TdApi.ProxyType type) { @@ -4383,8 +4614,10 @@ public static int getProxyDefaultOrder (@NonNull TdApi.ProxyType type) { return 2; case TdApi.ProxyTypeHttp.CONSTRUCTOR: return 3; + default: + Td.assertProxyType_bc1a1076(); + throw Td.unsupported(type); } - throw new UnsupportedOperationException(type.toString()); } private static @Proxy.Type int getProxyType (@NonNull TdApi.ProxyType type) { @@ -4395,8 +4628,10 @@ public static int getProxyDefaultOrder (@NonNull TdApi.ProxyType type) { return Proxy.TYPE_MTPROTO; case TdApi.ProxyTypeHttp.CONSTRUCTOR: return Proxy.TYPE_HTTP; + default: + Td.assertProxyType_bc1a1076(); + throw Td.unsupported(type); } - throw new UnsupportedOperationException(type.toString()); } private static byte[] serializeProxy (@NonNull TdApi.InternalLinkTypeProxy proxy) { @@ -4753,7 +4988,8 @@ public CharSequence getName () { stringRes = R.string.ProxyHttp; break; default: - throw new UnsupportedOperationException(proxy.type.toString()); + Td.assertProxyType_bc1a1076(); + throw Td.unsupported(proxy.type); } return Lang.getString(stringRes, (target, argStart, argEnd, argIndex, needFakeBold) -> new CustomTypefaceSpan(null, ColorId.textLight), name); } @@ -6135,47 +6371,67 @@ public long getPeriodicSyncFrequencySeconds () { // Push token + public static void storeDeviceToken (@NonNull TdApi.DeviceToken deviceToken, SharedPreferences.Editor editor, final String keyTokenType, final String keyTokenOrEndpoint) { + @DeviceTokenType int tokenType = TdlibNotificationUtils.getDeviceTokenType(deviceToken); + final String tokenOrEndpoint; + switch (tokenType) { + case DeviceTokenType.FIREBASE_CLOUD_MESSAGING: + tokenOrEndpoint = ((TdApi.DeviceTokenFirebaseCloudMessaging) deviceToken).token; + break; + case DeviceTokenType.HUAWEI_PUSH_SERVICE: + tokenOrEndpoint = ((TdApi.DeviceTokenHuaweiPush) deviceToken).token; + break; + case DeviceTokenType.SIMPLE_PUSH_SERVICE: + tokenOrEndpoint = ((TdApi.DeviceTokenSimplePush) deviceToken).endpoint; + break; + default: + Td.assertDeviceToken_de4a4f61(); + throw Td.unsupported(deviceToken); + } + editor + .putInt(keyTokenType, tokenType) + .putString(keyTokenOrEndpoint, tokenOrEndpoint); + } + public void setDeviceToken (TdApi.DeviceToken token) { if (token == null) { pmc.edit() .remove(KEY_PUSH_DEVICE_TOKEN_TYPE) - .remove(KEY_PUSH_DEVICE_TOKEN) + .remove(KEY_PUSH_DEVICE_TOKEN_OR_ENDPOINT) .apply(); } else if (!Td.equalsTo(token, getDeviceToken())) { resetTokenPushMessageCount(); - int tokenType = TdlibNotificationUtils.getDeviceTokenType(token); - switch (token.getConstructor()) { - case TdApi.DeviceTokenFirebaseCloudMessaging.CONSTRUCTOR: { - TdApi.DeviceTokenFirebaseCloudMessaging fcmToken = (TdApi.DeviceTokenFirebaseCloudMessaging) token; - pmc.edit() - .putInt(KEY_PUSH_DEVICE_TOKEN_TYPE, tokenType) - .putString(KEY_PUSH_DEVICE_TOKEN, fcmToken.token) - .apply(); - break; - } - default: { - throw new UnsupportedOperationException(token.toString()); - } - } + SharedPreferences.Editor editor = pmc.edit(); + Settings.storeDeviceToken(token, editor, + KEY_PUSH_DEVICE_TOKEN_TYPE, + KEY_PUSH_DEVICE_TOKEN_OR_ENDPOINT + ); + editor.apply(); } } - @Nullable - public TdApi.DeviceToken getDeviceToken () { - @DeviceTokenType int tokenType = pmc.getInt(KEY_PUSH_DEVICE_TOKEN_TYPE, DeviceTokenType.FIREBASE_CLOUD_MESSAGING); + public static TdApi.DeviceToken newDeviceToken (@DeviceTokenType int tokenType, @Nullable String tokenOrEndpoint) { + if (StringUtils.isEmpty(tokenOrEndpoint)) { + return null; + } switch (tokenType) { case DeviceTokenType.FIREBASE_CLOUD_MESSAGING: - default: { - String token = pmc.getString(KEY_PUSH_DEVICE_TOKEN, null); - if (!StringUtils.isEmpty(token)) { - return new TdApi.DeviceTokenFirebaseCloudMessaging(token, true); - } - break; - } + return new TdApi.DeviceTokenFirebaseCloudMessaging(tokenOrEndpoint, true); + case DeviceTokenType.SIMPLE_PUSH_SERVICE: + return new TdApi.DeviceTokenSimplePush(tokenOrEndpoint); + case DeviceTokenType.HUAWEI_PUSH_SERVICE: + return new TdApi.DeviceTokenHuaweiPush(tokenOrEndpoint, true); } return null; } + @Nullable + public TdApi.DeviceToken getDeviceToken () { + @DeviceTokenType int tokenType = pmc.getInt(KEY_PUSH_DEVICE_TOKEN_TYPE, DeviceTokenType.FIREBASE_CLOUD_MESSAGING); + String tokenOrEndpoint = pmc.getString(KEY_PUSH_DEVICE_TOKEN_OR_ENDPOINT, null); + return newDeviceToken(tokenType, tokenOrEndpoint); + } + // Device ID used to anonymously identify crashes from the same client private String crashDeviceId; @@ -6227,6 +6483,7 @@ public CloudSetting (TdApi.Message message, String requiredHashtag, int builtInS boolean isValidSetting = false, hideName = false; if (document.caption != null && document.caption.entities != null && document.caption.entities.length > 0) { for (TdApi.TextEntity entity : document.caption.entities) { + //noinspection SwitchIntDef switch (entity.type.getConstructor()) { case TdApi.TextEntityTypeHashtag.CONSTRUCTOR: { String hashtag = Td.substring(document.caption.text, entity); @@ -6619,8 +6876,8 @@ public AppBuildInfo getPreviousBuildInformation () { public String getPushMessageStats () { return "total: " + getReceivedPushMessageCountTotal() + " " + - "by_token: " + getReceivedPushMessageCountByToken() + " " + - "by_app_version: " + getReceivedPushMessageCountByAppVersion() + " "; + "by_token: " + getReceivedPushMessageCountByToken() + " " + + "by_app_version: " + getReceivedPushMessageCountByAppVersion() + " "; } public long getReceivedPushMessageCountTotal () { @@ -6716,4 +6973,12 @@ public String getDefaultLanguageForTranslateDraft () { public void setDefaultLanguageForTranslateDraft (String language) { pmc.putString(KEY_DEFAULT_LANGUAGE_FOR_TRANSLATE_DRAFT, language); } + + public boolean chatFoldersEnabled () { + return Config.CHAT_FOLDERS_ENABLED && isExperimentEnabled(EXPERIMENT_FLAG_ENABLE_FOLDERS); + } + + public boolean showPeerIds () { + return isExperimentEnabled(EXPERIMENT_FLAG_SHOW_PEER_IDS); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/util/AdapterSubListUpdateCallback.java b/app/src/main/java/org/thunderdog/challegram/util/AdapterSubListUpdateCallback.java new file mode 100644 index 0000000000..ae41f9e79b --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/util/AdapterSubListUpdateCallback.java @@ -0,0 +1,50 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 06/01/2023 + */ +package org.thunderdog.challegram.util; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.ListUpdateCallback; +import androidx.recyclerview.widget.RecyclerView; + +public class AdapterSubListUpdateCallback implements ListUpdateCallback { + private final RecyclerView.Adapter adapter; + private final int fromIndex; + + public AdapterSubListUpdateCallback (@NonNull RecyclerView.Adapter adapter, int fromIndex) { + this.adapter = adapter; + this.fromIndex = fromIndex; + } + + @Override + public void onInserted (int position, int count) { + adapter.notifyItemRangeInserted(position + fromIndex, count); + } + + @Override + public void onRemoved (int position, int count) { + adapter.notifyItemRangeRemoved(position + fromIndex, count); + } + + @Override + public void onMoved (int fromPosition, int toPosition) { + adapter.notifyItemMoved(fromPosition + fromIndex, toPosition + fromIndex); + } + + @Override + public void onChanged (int position, int count, @Nullable Object payload) { + adapter.notifyItemRangeChanged(position + fromIndex, count, payload); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thunderdog/challegram/util/AppInstallationUtil.java b/app/src/main/java/org/thunderdog/challegram/util/AppInstallationUtil.java new file mode 100644 index 0000000000..ba885c2236 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/util/AppInstallationUtil.java @@ -0,0 +1,225 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 13/11/2023 + */ +package org.thunderdog.challegram.util; + +import android.os.Build; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thunderdog.challegram.BuildConfig; +import org.thunderdog.challegram.Log; +import org.thunderdog.challegram.tool.UI; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import me.vkryl.core.StringUtils; + +public class AppInstallationUtil { + public static final String VENDOR_GOOGLE_PLAY = "com.android.vending"; + public static final String VENDOR_GALAXY_STORE = "com.sec.android.app.samsungapps"; + public static final String VENDOR_HUAWEI_APPGALLERY = "com.huawei.appmarket"; + public static final String VENDOR_AMAZON_APPSTORE = "com.amazon.venezia"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + InstallerId.UNKNOWN, + InstallerId.GOOGLE_PLAY, + InstallerId.GALAXY_STORE, + InstallerId.HUAWEI_APPGALLERY, + InstallerId.AMAZON_APPSTORE + }) + public @interface InstallerId { + int + UNKNOWN = 0, + GOOGLE_PLAY = 1, + GALAXY_STORE = 2, + HUAWEI_APPGALLERY = 3, + AMAZON_APPSTORE = 4; + } + + private static Integer installerId; + + public static synchronized @InstallerId int getInstallerId () { + if (installerId == null) { + installerId = getInstallerIdImpl(); + } + return installerId; + } + + private static @InstallerId int getInstallerIdImpl () { + final String installerPackageName = getInstallerPackageName(); + if (!StringUtils.isEmpty(installerPackageName)) { + //noinspection ConstantConditions + switch (installerPackageName) { + case VENDOR_GOOGLE_PLAY: + return InstallerId.GOOGLE_PLAY; + case VENDOR_GALAXY_STORE: + return InstallerId.GALAXY_STORE; + case VENDOR_HUAWEI_APPGALLERY: + return InstallerId.HUAWEI_APPGALLERY; + case VENDOR_AMAZON_APPSTORE: + return InstallerId.AMAZON_APPSTORE; + } + } + return InstallerId.UNKNOWN; + } + + // Checks initiator and installer id for current app installation + + @Nullable + public static String getInitiatorPackageName () { + final String packageName = UI.getAppContext().getPackageName(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + android.content.pm.InstallSourceInfo sourceInfo = UI.getAppContext().getPackageManager().getInstallSourceInfo(packageName); + String initiatingId = sourceInfo.getInitiatingPackageName(); + if (!StringUtils.isEmpty(initiatingId)) { + return initiatingId; + } + } catch (Throwable t) { + Log.v("Unable to determine initiator package name", t); + } + } + return null; + } + + @Nullable + public static String getInstallerPackageName () { + final String packageName = UI.getAppContext().getPackageName(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + android.content.pm.InstallSourceInfo sourceInfo = UI.getAppContext().getPackageManager().getInstallSourceInfo(packageName); + String installerId = sourceInfo.getInstallingPackageName(); + if (!StringUtils.isEmpty(installerId)) { + return installerId; + } + String initiatingId = sourceInfo.getInitiatingPackageName(); + if (!StringUtils.isEmpty(initiatingId)) { + return initiatingId; + } + } catch (Throwable t) { + Log.v("Unable to determine installer package via modern API", t); + } + } + try { + String installerPackageName = UI.getAppContext().getPackageManager().getInstallerPackageName(packageName); + if (StringUtils.isEmpty(installerPackageName)) { + return null; + } + return installerPackageName; + } catch (Throwable t) { + Log.v("Unable to determine installer package", t); + return null; + } + } + + public static @Nullable String getInstallerPrettyName () { + switch (getInstallerId()) { + case InstallerId.UNKNOWN: + return getInstallerPackageName(); + case InstallerId.GOOGLE_PLAY: + return "Google Play"; + case InstallerId.GALAXY_STORE: + return "Galaxy Store"; + case InstallerId.HUAWEI_APPGALLERY: + return "Huawei AppGallery"; + case InstallerId.AMAZON_APPSTORE: + return "Amazon AppStore"; + } + throw new UnsupportedOperationException(); + } + + // Checks whether app is installed from unofficial source (e.g. directly via an APK) + + public static boolean isAppSideLoaded () { + return getInstallerId() == InstallerId.UNKNOWN; + } + + // Do not allow in-app updates from Google Play, if we are installed from market that doesn't allow it + + public static boolean allowInAppGooglePlayUpdates () { + switch (getInstallerId()) { + case InstallerId.UNKNOWN: + case InstallerId.GOOGLE_PLAY: { + //noinspection ObsoleteSdkInt + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !BuildConfig.SIDE_LOAD_ONLY; + } + case InstallerId.GALAXY_STORE: + case InstallerId.HUAWEI_APPGALLERY: + case InstallerId.AMAZON_APPSTORE: + return false; + default: + throw new UnsupportedOperationException(); + } + } + + // Do not allow in-app updates via Telegram channel, unless it's a direct APK installation + + public static boolean allowInAppTelegramUpdates () { + return !BuildConfig.EXPERIMENTAL && getInstallerId() == InstallerId.UNKNOWN; + } + + // Do not allow non-store URLs for compliance + + public static class DownloadUrl { + public final @InstallerId int installerId; + public final String url; + + public DownloadUrl (int installerId, String url) { + this.installerId = installerId; + this.url = url; + } + } + + @SuppressWarnings("ConstantConditions") + public static @NonNull DownloadUrl getDownloadUrl (@Nullable String remoteDownloadUrl) { + @InstallerId int installerId = getInstallerId(); + switch (installerId) { + case InstallerId.UNKNOWN: // primary distribution channel, no need to force URL. + break; + case InstallerId.GOOGLE_PLAY: + if (!StringUtils.isEmpty(BuildConfig.GOOGLE_PLAY_URL)) { + return new DownloadUrl(installerId, BuildConfig.GOOGLE_PLAY_URL); + } + break; + + case InstallerId.GALAXY_STORE: + if (!StringUtils.isEmpty(BuildConfig.GALAXY_STORE_URL)) { + return new DownloadUrl(installerId, BuildConfig.GALAXY_STORE_URL); + } + break; + case InstallerId.HUAWEI_APPGALLERY: + if (!StringUtils.isEmpty(BuildConfig.HUAWEI_APPGALLERY_URL)) { + return new DownloadUrl(installerId, BuildConfig.HUAWEI_APPGALLERY_URL); + } + break; + case InstallerId.AMAZON_APPSTORE: + if (!StringUtils.isEmpty(BuildConfig.AMAZON_APPSTORE_URL)) { + return new DownloadUrl(installerId, BuildConfig.AMAZON_APPSTORE_URL); + } + break; + } + if (remoteDownloadUrl != null) { + return new DownloadUrl(InstallerId.UNKNOWN, remoteDownloadUrl); + } + if (StringUtils.isEmpty(BuildConfig.DOWNLOAD_URL)) { + throw new UnsupportedOperationException(); + } + return new DownloadUrl(InstallerId.UNKNOWN, BuildConfig.DOWNLOAD_URL); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/util/AppUpdater.java b/app/src/main/java/org/thunderdog/challegram/util/AppUpdater.java index e8c3b81c8f..2cd11a13c6 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/AppUpdater.java +++ b/app/src/main/java/org/thunderdog/challegram/util/AppUpdater.java @@ -15,7 +15,6 @@ import android.app.Activity; import android.content.Intent; import android.content.IntentSender; -import android.os.Build; import androidx.annotation.IntDef; import androidx.annotation.NonNull; @@ -37,7 +36,6 @@ import org.thunderdog.challegram.BuildConfig; import org.thunderdog.challegram.Log; import org.thunderdog.challegram.R; -import org.thunderdog.challegram.U; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.navigation.ViewController; @@ -85,12 +83,15 @@ default void onAppUpdateDownloadProgress (long bytesDownloaded, long totalBytesT @Retention(RetentionPolicy.SOURCE) @IntDef({ + FlowType.NONE, FlowType.TELEGRAM_CHANNEL, FlowType.GOOGLE_PLAY }) public @interface FlowType { - int TELEGRAM_CHANNEL = 1; - int GOOGLE_PLAY = 2; + int + NONE = 0, + TELEGRAM_CHANNEL = 1, + GOOGLE_PLAY = 2; } private final BaseActivity context; @@ -119,7 +120,7 @@ public AppUpdater (BaseActivity context) { this.context = context; this.listeners = new ReferenceList<>(); AppUpdateManager appUpdateManager = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !BuildConfig.SIDE_LOAD_ONLY) { + if (AppInstallationUtil.allowInAppGooglePlayUpdates()) { try { appUpdateManager = AppUpdateManagerFactory.create(context); } catch (Throwable t) { @@ -182,7 +183,7 @@ private boolean preferTelegramChannelFlow () { // TODO: add server config to force return googlePlayUpdateManager == null || forceTelegramChannelFlow || - (googlePlayFlowError && U.isAppSideLoaded()); + (googlePlayFlowError && AppInstallationUtil.isAppSideLoaded()); } private void setState (@State int state) { @@ -218,7 +219,7 @@ private void checkForGooglePlayUpdates () { } case UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS: case UpdateAvailability.UPDATE_NOT_AVAILABLE: { - if (U.isAppSideLoaded()) { + if (AppInstallationUtil.isAppSideLoaded()) { onGooglePlayFlowError(); } else { onUpdateUnavailable(); @@ -303,7 +304,11 @@ public void onStateUpdate (@NonNull InstallState state) { private void checkForTelegramChannelUpdates () { Tdlib tdlib = context.hasTdlib() ? context.currentTdlib() : null; - if (BuildConfig.EXPERIMENTAL || tdlib == null || tdlib.context().inRecoveryMode() || !tdlib.isAuthorized()) { + if (tdlib == null || tdlib.context().inRecoveryMode() || !tdlib.isAuthorized()) { + onUpdateUnavailable(); + return; + } + if (!AppInstallationUtil.allowInAppTelegramUpdates() && !tdlib.hasUrgentInAppUpdate()) { onUpdateUnavailable(); return; } @@ -430,6 +435,9 @@ private void onUpdateAvailable (@FlowType int flowType, long bytesDownloaded, lo public void offerUpdate () { if (!updateOffered) { switch (flowType) { + case FlowType.NONE: + // Do nothing. + break; case FlowType.GOOGLE_PLAY: { updateOffered = offerGooglePlayUpdate(); break; @@ -447,6 +455,9 @@ public void downloadUpdate () { return; } switch (flowType) { + case FlowType.NONE: + // Do nothing. + break; case FlowType.GOOGLE_PLAY: { updateOffered = offerGooglePlayUpdate(); break; @@ -472,6 +483,9 @@ public void installUpdate () { return; } switch (flowType) { + case FlowType.NONE: + // Do nothing. + break; case FlowType.TELEGRAM_CHANNEL: { // TODO guide on how to allow installing APKs UI.openFile(new TdlibContext(context, telegramChannelTdlib), telegramChannelFile.document.fileName, new File(telegramChannelFile.document.document.local.path), telegramChannelFile.document.mimeType, 0); diff --git a/app/src/main/java/org/thunderdog/challegram/util/CustomTypefaceSpan.java b/app/src/main/java/org/thunderdog/challegram/util/CustomTypefaceSpan.java index e813158305..362f4189e1 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/CustomTypefaceSpan.java +++ b/app/src/main/java/org/thunderdog/challegram/util/CustomTypefaceSpan.java @@ -24,6 +24,7 @@ import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.component.chat.WallpaperView; +import org.thunderdog.challegram.telegram.TdlibEntitySpan; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ThemeDelegate; @@ -33,8 +34,9 @@ import me.vkryl.core.BitwiseUtils; import me.vkryl.core.ColorUtils; +import me.vkryl.td.Td; -public class CustomTypefaceSpan extends MetricAffectingSpan { +public class CustomTypefaceSpan extends MetricAffectingSpan implements TdlibEntitySpan { private static final int FLAG_FAKE_BOLD = 1; private static final int FLAG_REMOVE_UNDERLINE = 1 << 1; private static final int FLAG_NO_BACKGROUND_TRANSPARENCY = 1 << 2; @@ -139,18 +141,23 @@ public CustomTypefaceSpan setForcedTheme (@Nullable ThemeDelegate theme) { return this; } - public CustomTypefaceSpan setEntityType (TdApi.TextEntityType type) { + @Override + public void setTextEntityType (TdApi.TextEntityType type) { this.type = type; if (type != null) { - setNeedUnderline(type.getConstructor() == TdApi.TextEntityTypeUnderline.CONSTRUCTOR); - setNeedStrikethrough(type.getConstructor() == TdApi.TextEntityTypeStrikethrough.CONSTRUCTOR); - setNeedRevealOnTap(type.getConstructor() == TdApi.TextEntityTypeSpoiler.CONSTRUCTOR); + setNeedUnderline(Td.isUnderline(type)); + setNeedStrikethrough(Td.isStrikethrough(type)); + setNeedRevealOnTap(Td.isSpoiler(type)); } else { setNeedUnderline(false); setNeedStrikethrough(false); setNeedRevealOnTap(false); } - return this; + } + + @Override + public TdApi.TextEntityType getTextEntityType () { + return type; } public CustomTypefaceSpan setTransparencyColorId (@ColorId int transparentColorId, WallpaperView wallpaperView) { @@ -168,10 +175,6 @@ public Object getTag () { return tag; } - public TdApi.TextEntityType getEntityType () { - return type; - } - @Nullable public Typeface getTypeface () { return typeface; diff --git a/app/src/main/java/org/thunderdog/challegram/util/DeviceTokenType.java b/app/src/main/java/org/thunderdog/challegram/util/DeviceTokenType.java index a6b3ea8234..01fe12fbb6 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/DeviceTokenType.java +++ b/app/src/main/java/org/thunderdog/challegram/util/DeviceTokenType.java @@ -1,3 +1,17 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 01/06/2023 + */ package org.thunderdog.challegram.util; import androidx.annotation.IntDef; @@ -7,9 +21,13 @@ @Retention(RetentionPolicy.SOURCE) @IntDef({ - DeviceTokenType.FIREBASE_CLOUD_MESSAGING + DeviceTokenType.FIREBASE_CLOUD_MESSAGING, + DeviceTokenType.HUAWEI_PUSH_SERVICE, + DeviceTokenType.SIMPLE_PUSH_SERVICE }) public @interface DeviceTokenType { - // TODO more push services. When adding new types, check usages to confirm support in other places - int FIREBASE_CLOUD_MESSAGING = 0; + int + FIREBASE_CLOUD_MESSAGING = 0, + HUAWEI_PUSH_SERVICE = 1, + SIMPLE_PUSH_SERVICE = 2; } diff --git a/app/src/main/java/org/thunderdog/challegram/util/EmojiStatusHelper.java b/app/src/main/java/org/thunderdog/challegram/util/EmojiStatusHelper.java index 1e2284e07d..9aee3ce685 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/EmojiStatusHelper.java +++ b/app/src/main/java/org/thunderdog/challegram/util/EmojiStatusHelper.java @@ -34,8 +34,11 @@ import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibAccount; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.util.text.Text; @@ -72,7 +75,7 @@ public void setClickListener (@Nullable Text.ClickListener clickListenerToSet) { } public void invalidateEmojiStatusReceiver (@Nullable TextMedia specificMedia) { - invalidateEmojiStatusReceiver(emojiStatusDrawable != null ? emojiStatusDrawable.emojiStatus: null, specificMedia); + invalidateEmojiStatusReceiver(emojiStatusDrawable != null ? emojiStatusDrawable.emojiStatus : null, specificMedia); } public void invalidateEmojiStatusReceiver (Text text, @Nullable TextMedia specificMedia) { @@ -131,11 +134,11 @@ public void updateEmoji (Tdlib tdlib, @Nullable TdApi.User user, TextColorSet te } public int getLastDrawX () { - return emojiStatusDrawable != null ? emojiStatusDrawable.lastDrawX: 0; + return emojiStatusDrawable != null ? emojiStatusDrawable.lastDrawX : 0; } public int getLastDrawY () { - return emojiStatusDrawable != null ? emojiStatusDrawable.lastDrawY: 0; + return emojiStatusDrawable != null ? emojiStatusDrawable.lastDrawY : 0; } public void attach () { @@ -156,11 +159,11 @@ public boolean needDrawEmojiStatus () { } public int getWidth () { - return emojiStatusDrawable != null ? emojiStatusDrawable.getWidth(): 0; + return emojiStatusDrawable != null ? emojiStatusDrawable.getWidth() : 0; } public int getWidth (int offset) { - return emojiStatusDrawable != null ? emojiStatusDrawable.getWidth(offset): 0; + return emojiStatusDrawable != null ? emojiStatusDrawable.getWidth(offset) : 0; } public void draw (Canvas c, int startX, int startY) { @@ -229,7 +232,7 @@ public static class EmojiStatusDrawable implements Destroyable { private final @Nullable ImageReceiver preview; private final @Nullable ImageReceiver imageReceiver; private final @Nullable GifReceiver gifReceiver; - private final boolean needRepainting; + private final boolean needThemedColorFilter; private int lastDrawX, lastDrawY; private float lastDrawScale = 1f; private boolean ignoreDraw; @@ -241,11 +244,11 @@ private EmojiStatusDrawable (@Nullable String sharedUsageId, Tdlib tdlib, @Nulla this.textColorSet = textColorSet; this.textMediaListener = textMediaListener; this.clickListener = clickListener; - this.starDrawable = emojiStatus == null && needDrawEmojiStatus ? Drawables.get(defaultStarIconId): null; + this.starDrawable = emojiStatus == null && needDrawEmojiStatus ? Drawables.get(defaultStarIconId) : null; this.preview = null; this.imageReceiver = null; this.gifReceiver = null; - this.needRepainting = false; + this.needThemedColorFilter = false; } private EmojiStatusDrawable (View v, @Nullable String sharedUsageId, boolean isPremium, @Nullable TdApi.Sticker sticker, @Nullable Text.ClickListener clickListener, @Nullable TextColorSet textColorSet, int defaultStarIconId, int textSize) { @@ -255,8 +258,8 @@ private EmojiStatusDrawable (View v, @Nullable String sharedUsageId, boolean isP this.textColorSet = textColorSet; this.textMediaListener = null; this.clickListener = clickListener; - this.starDrawable = needDrawEmojiStatus && (sticker == null || !TD.isFileLoaded(sticker.sticker)) ? Drawables.get(defaultStarIconId): null; - this.needRepainting = TD.needRepainting(sticker); + this.starDrawable = needDrawEmojiStatus && (sticker == null || !TD.isFileLoaded(sticker.sticker)) ? Drawables.get(defaultStarIconId) : null; + this.needThemedColorFilter = TD.needThemedColorFilter(sticker); if (sticker != null && TD.isFileLoaded(sticker.sticker)) { this.imageReceiver = new ImageReceiver(v, 0); @@ -342,10 +345,10 @@ public boolean onTouchEvent (View v, MotionEvent e) { return emojiStatus.onTouchEvent(v, e); } - int width = starDrawable != null ? starDrawable.getMinimumWidth(): - imageReceiver != null ? imageReceiver.getWidth(): 0; - int height = starDrawable != null ? starDrawable.getMinimumHeight(): - imageReceiver != null ? imageReceiver.getHeight(): 0; + int width = starDrawable != null ? starDrawable.getMinimumWidth() : + imageReceiver != null ? imageReceiver.getWidth() : 0; + int height = starDrawable != null ? starDrawable.getMinimumHeight() : + imageReceiver != null ? imageReceiver.getHeight() : 0; if (clickListener == null || width == 0 || height == 0) return false; @@ -403,11 +406,20 @@ public void draw (Canvas c, int startX, int startY, float alpha, float scale, Co lastDrawY = startY; lastDrawScale = scale; if (imageReceiver != null && gifReceiver != null && preview != null) { - int repaintRestoreToCount = -1; - if (needRepainting) { - repaintRestoreToCount = c.saveLayerAlpha(startX, startY, startX + Screen.dp(EmojiStatusHelper.textSizeToEmojiSize(textSize)), startY + Screen.dp(EmojiStatusHelper.textSizeToEmojiSize(textSize)), 255, Canvas.ALL_SAVE_FLAG); + if (!needThemedColorFilter) { + gifReceiver.disablePorterDuffColorFilter(); + imageReceiver.disablePorterDuffColorFilter(); + preview.disablePorterDuffColorFilter(); + } else if (textColorSet != null) { + long complexColor = textColorSet.mediaTextComplexColor(); + Theme.applyComplexColor(gifReceiver, complexColor); + Theme.applyComplexColor(imageReceiver, complexColor); + Theme.applyComplexColor(preview, complexColor); + } else { + gifReceiver.setThemedPorterDuffColorId(ColorId.icon); + imageReceiver.setThemedPorterDuffColorId(ColorId.icon); + preview.setThemedPorterDuffColorId(ColorId.icon); } - imageReceiver.setBounds(startX, startY, startX + Screen.dp(EmojiStatusHelper.textSizeToEmojiSize(textSize)), startY + Screen.dp(EmojiStatusHelper.textSizeToEmojiSize(textSize))); preview.setBounds(startX, startY, startX + Screen.dp(EmojiStatusHelper.textSizeToEmojiSize(textSize)), startY + Screen.dp(EmojiStatusHelper.textSizeToEmojiSize(textSize))); gifReceiver.setBounds(startX, startY, startX + Screen.dp(EmojiStatusHelper.textSizeToEmojiSize(textSize)), startY + Screen.dp(EmojiStatusHelper.textSizeToEmojiSize(textSize))); @@ -422,16 +434,16 @@ public void draw (Canvas c, int startX, int startY, float alpha, float scale, Co } imageReceiver.draw(c); } - if (needRepainting) { - if (textColorSet != null) { - c.drawRect(startX, startY, startX + Screen.dp(EmojiStatusHelper.textSizeToEmojiSize(textSize)), startY + Screen.dp(EmojiStatusHelper.textSizeToEmojiSize(textSize)), Paints.getSrcInPaint(textColorSet.emojiStatusColor())); - } - Views.restore(c, repaintRestoreToCount); - } } else if (emojiStatus != null) { emojiStatus.draw(c, startX, startY, null, alpha, emojiStatusReceiver); } else if (starDrawable != null && textColorSet != null) { - Paint p = Paints.getPorterDuffPaint(ColorUtils.alphaColor(alpha, textColorSet.emojiStatusColor())); + long complexColor = textColorSet.mediaTextComplexColor(); + Paint p; + if (Theme.isColorId(complexColor)) { + p = PorterDuffPaint.get(Theme.extractColorValue(complexColor), alpha); + } else { + p = Paints.getPorterDuffPaint(ColorUtils.alphaColor(alpha, Theme.extractColorValue(complexColor))); + } Drawables.draw(c, starDrawable, startX, startY + (Screen.dp(textSize + 2) - starDrawable.getMinimumHeight()) / 2f, p); } if (isScaled) { diff --git a/app/src/main/java/org/thunderdog/challegram/util/FlowListAnimator.java b/app/src/main/java/org/thunderdog/challegram/util/FlowListAnimator.java new file mode 100644 index 0000000000..af2073e443 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/util/FlowListAnimator.java @@ -0,0 +1,608 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 06/01/2023 + */ +package org.thunderdog.challegram.util; + +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.animation.Interpolator; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.Px; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import me.vkryl.android.animator.Animatable; +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.android.animator.VariableFloat; +import me.vkryl.android.animator.VariableRect; +import me.vkryl.core.ArrayUtils; +import me.vkryl.core.MathUtils; +import me.vkryl.core.lambda.Destroyable; + +public final class FlowListAnimator implements Iterable> { + public static class Entry implements Comparable> { + public final T item; + private int index; + + private final VariableFloat position; + private final VariableFloat visibility; + private final VariableRect measuredPositionRect; + + public Entry (T item, int index, boolean isVisible) { + this.item = item; + this.index = index; + this.visibility = new VariableFloat(isVisible ? 1f : 0f); + this.position = new VariableFloat(index); + this.measuredPositionRect = new VariableRect(); + finishAnimation(false); + } + + public boolean isJunk () { + return getVisibility() == 0f && !isAffectingList(); + } + + private boolean isBeingRemoved = false; + + private void onPrepareRemove () { + visibility.setTo(0f); + isBeingRemoved = true; + } + + private void onPrepareAppear () { + visibility.setTo(1f); + isBeingRemoved = false; + } + + @Override + public int compareTo(Entry o) { + return Integer.compare(index, o.index); + } + + public float getPosition () { + return position.get(); + } + + public int getIndex () { + return index; + } + + public float getVisibility () { + return MathUtils.clamp(visibility.get()); + } + + // State + + public boolean isAffectingList () { + return !isBeingRemoved; + } + + private void onRecycled () { + if (item instanceof Destroyable) { + ((Destroyable) item).performDestroy(); + } + } + + // Measured + + public RectF getRectF () { + return measuredPositionRect.toRectF(); + } + + public void getBounds (Rect outRect) { + outRect.set((int) measuredPositionRect.getLeft(), (int) measuredPositionRect.getTop(), (int) measuredPositionRect.getRight(), (int) measuredPositionRect.getBottom()); + } + + // Animation + + private void finishAnimation (boolean applyFutureState) { + this.position.finishAnimation(applyFutureState); + this.visibility.finishAnimation(applyFutureState); + this.measuredPositionRect.finishAnimation(applyFutureState); + if (item instanceof Animatable) { + ((Animatable) this.item).finishAnimation(applyFutureState); + } + } + + private boolean applyAnimation (float factor) { + boolean haveChanges; + haveChanges = position.applyAnimation(factor); + haveChanges = visibility.applyAnimation(factor) || haveChanges; + haveChanges = measuredPositionRect.applyAnimation(factor) || haveChanges; + if (item instanceof Animatable) { + haveChanges = ((Animatable) item).applyAnimation(factor) || haveChanges; + } + return haveChanges; + } + } + + public interface Measurable { + int getWidth (); + int getHeight (); + } + + public interface Callback { + void onItemsChanged (FlowListAnimator animator); + } + + public static class Metadata { + private final VariableFloat size = new VariableFloat(0); + private final VariableFloat visibility = new VariableFloat(0); + private final VariableFloat totalWidth = new VariableFloat(0f); + private final VariableFloat totalHeight = new VariableFloat(0f); + private final VariableFloat lastLineWidth = new VariableFloat(0f); + private final VariableFloat lastLineHeight = new VariableFloat(0f); + + private Metadata () { } + + public boolean applyAnimation (float factor) { + boolean haveChanges; + haveChanges = size.applyAnimation(factor); + haveChanges = totalWidth.applyAnimation(factor) || haveChanges; + haveChanges = totalHeight.applyAnimation(factor) || haveChanges; + haveChanges = visibility.applyAnimation(factor) || haveChanges; + haveChanges = lastLineWidth.applyAnimation(factor) || haveChanges; + haveChanges = lastLineHeight.applyAnimation(factor) || haveChanges; + return haveChanges; + } + + public void finishAnimation (boolean applyFuture) { + size.finishAnimation(applyFuture); + totalWidth.finishAnimation(applyFuture); + totalHeight.finishAnimation(applyFuture); + visibility.finishAnimation(applyFuture); + lastLineWidth.finishAnimation(applyFuture); + lastLineHeight.finishAnimation(applyFuture); + } + + private void setSize (int size, boolean animated) { + if (animated) { + this.size.setTo(size); + this.visibility.setTo(size > 0 ? 1.0f : 0.0f); + } else { + this.size.set(size); + this.visibility.set(size > 0 ? 1.0f : 0.0f); + } + } + + public float getTotalWidth () { + return totalWidth.get(); + } + + public float getTotalHeight () { + return totalHeight.get(); + } + + public float getSize () { + return size.get(); + } + + public float getVisibility () { + return visibility.get(); + } + + public float getLastLineWidth () { + return lastLineWidth.get(); + } + + public float getLastLineHeight () { + return lastLineHeight.get(); + } + } + + private final Callback callback; + private final ArrayList> entries; + private final @Nullable FactorAnimator animator; + private final Metadata metadata; + private final ArrayList> actualList; // list after all animations finished + + private @Px int itemSpacing = 0; + private @Px int lineSpacing = 0; + private @Px int maxWidth = Integer.MAX_VALUE; + private @Px int maxItemsInRow = Integer.MAX_VALUE; + + public FlowListAnimator (@NonNull Callback callback, @Nullable Interpolator interpolator, long duration) { + this.callback = callback; + this.metadata = new Metadata(); + this.entries = new ArrayList<>(); + this.actualList = new ArrayList<>(); + if (interpolator != null && duration > 0) { + this.animator = new FactorAnimator(0, new FactorAnimator.Target() { + @Override + public void onFactorChanged(int id, float factor, float fraction, FactorAnimator callee) { + applyAnimation(factor); + } + + @Override + public void onFactorChangeFinished(int id, float finalFactor, FactorAnimator callee) { + applyAnimation(finalFactor); + } + }, interpolator, duration); + } else { + this.animator = null; + } + } + + public void setItemSpacing (@Px int itemSpacing) { + this.itemSpacing = itemSpacing; + } + + public void setLineSpacing (@Px int lineSpacing) { + this.lineSpacing = lineSpacing; + } + + public void setMaxItemsInRow (@Px int maxItemsInRow) { + this.maxItemsInRow = maxItemsInRow; + } + + public void setMaxWidth (@Px int maxWidth) { + this.maxWidth = maxWidth; + } + + public int getMaxWidth () { + return maxWidth; + } + + public int size () { + return entries.size(); + } + + public Entry getEntry (int index) { + return entries.get(index); + } + + public Metadata getMetadata () { + return metadata; + } + + public void applyAnimation (float factor) { + boolean haveChanges = metadata.applyAnimation(factor); + for (Entry entry : entries) { + haveChanges = entry.applyAnimation(factor) || haveChanges; + } + if (haveChanges) { + callback.onItemsChanged(FlowListAnimator.this); + if (factor == 1f) { + removeJunk(true); + } + } + } + + @NonNull + @Override + public Iterator> iterator() { + return entries.iterator(); + } + + private void removeJunk (boolean applyFuture) { + boolean haveRemovedEntries = false; + for (int i = entries.size() - 1; i >= 0; i--) { + Entry entry = entries.get(i); + entry.finishAnimation(applyFuture); + if (entry.isJunk()) { + entries.remove(i); + entry.onRecycled(); + haveRemovedEntries = true; + } + } + if (haveRemovedEntries) { + entries.trimToSize(); + } + metadata.finishAnimation(applyFuture); + } + + public void stopAnimation (boolean applyFuture) { + if (animator != null) { + animator.cancel(); + removeJunk(applyFuture); + animator.forceFactor(0f); + } else { + removeJunk(applyFuture); + } + } + + private int indexOfItem (T item) { + int index = 0; + if (item == null) { + for (Entry entry : entries) { + if (entry.item == null) + return index; + index++; + } + } else { + for (Entry entry : entries) { + if (item.equals(entry.item)) + return index; + index++; + } + } + return -1; + } + + public void clear (boolean animated) { + reset(null, animated); + } + + private boolean foundListChanges; + + private void onBeforeListChanged () { + if (!foundListChanges) { + foundListChanges = true; + stopAnimation(false); + } + } + + private void onApplyListChanges () { + if (foundListChanges) { + foundListChanges = false; + if (animator != null) { + animator.animateTo(1f); + } + } else { + if (animator == null) { + for (Entry entry : entries) { + entry.visibility.setFrom(entry.visibility.get()); + entry.position.setFrom(entry.position.get()); + } + } + } + } + + public void measure (boolean animated) { + if (!animated) { + stopAnimation(true); + } + measureImpl(animated); + if (animated) { + onApplyListChanges(); + } + } + + private void measureImpl (boolean animated) { + int itemsInRow = 0; + int itemLeft = 0, itemTop = 0; + int lineHeight = 0, totalWidth = 0; + for (Entry entry : actualList) { + int itemWidth = Math.min(entry.item.getWidth(), maxWidth); + int itemHeight = entry.item.getHeight(); + if (itemLeft + itemWidth > maxWidth || itemsInRow >= maxItemsInRow) { + itemsInRow = 0; + itemLeft = 0; + itemTop += lineHeight + lineSpacing; + } + itemsInRow++; + int itemRight = itemLeft + itemWidth; + int itemBottom = itemTop + itemHeight; + if (animated && entry.getVisibility() > 0f) { + if (entry.measuredPositionRect.differs(itemLeft, itemTop, itemRight, itemBottom)) { + onBeforeListChanged(); + entry.measuredPositionRect.setTo(itemLeft, itemTop, itemRight, itemBottom); + } + } else { + entry.measuredPositionRect.set(itemLeft, itemTop, itemRight, itemBottom); + } + itemLeft = itemRight + itemSpacing; + lineHeight = Math.max(lineHeight, itemHeight); + totalWidth = Math.max(totalWidth, itemRight); + } + int totalHeight = itemTop + lineHeight; + int lineWidth = itemLeft - itemSpacing; + for (Entry entry : entries) { + if (entry.item instanceof Animatable) { + Animatable animatable = (Animatable) entry.item; + if (animated) { + if (animatable.hasChanges()) { + onBeforeListChanged(); + animatable.prepareChanges(); + } + } else { + animatable.applyChanges(); + } + } + } + + if (animated) { + if (metadata.totalWidth.differs(totalWidth)) { + onBeforeListChanged(); + metadata.totalWidth.setTo(totalWidth); + } + if (metadata.totalHeight.differs(totalHeight)) { + onBeforeListChanged(); + metadata.totalHeight.setTo(totalHeight); + } + if (metadata.lastLineWidth.differs(lineWidth)) { + onBeforeListChanged(); + metadata.lastLineWidth.setTo(lineWidth); + } + if (metadata.lastLineHeight.differs(lineHeight)) { + onBeforeListChanged(); + metadata.lastLineHeight.setTo(lineHeight); + } + } else { + metadata.totalWidth.set(totalWidth); + metadata.totalHeight.set(totalHeight); + metadata.lastLineWidth.set(lineWidth); + metadata.lastLineHeight.set(lineHeight); + } + } + + public interface ResetCallback { + void onItemRemoved (T item); // item is now removing + void onItemAdded (T item, boolean isReturned); // item is now adding + } + + public void reset (@Nullable List newItems, boolean animated) { + reset(newItems, animated, null); + } + + public boolean compareContents (@Nullable List items) { + if (items == null || items.isEmpty()) { + return this.actualList.isEmpty(); + } else { + if (this.actualList.size() != items.size()) + return false; + for (int i = 0; i < items.size(); i++) { + if (!this.actualList.get(i).item.equals(items.get(i))) + return false; + } + return true; + } + } + + public void reset (@Nullable List newItems, boolean animated, @Nullable ResetCallback resetCallback) { + if (!animated) { + stopAnimation(false); + for (int i = entries.size() - 1; i >= 0; i--) { + entries.get(i).onRecycled(); + } + entries.clear(); + actualList.clear(); + int size = newItems != null ? newItems.size() : 0; + if (size > 0) { + entries.ensureCapacity(size); + actualList.ensureCapacity(size); + for (T item : newItems) { + Entry entry = new Entry<>(item, actualList.size(), true); + entries.add(entry); + actualList.add(entry); + } + entries.trimToSize(); + actualList.trimToSize(); + } + metadata.setSize(size, false); + measureImpl(false); + callback.onItemsChanged(this); + return; + } + + if (compareContents(newItems)) + return; + + onBeforeListChanged(); + + boolean needSort = false; + if (newItems != null && !newItems.isEmpty()) { + // First, detect removals & changes + + int foundItemCount = 0; + + boolean needSortActual = false; + for (int i = 0; i < entries.size(); i++) { + Entry entry = entries.get(i); + int newIndex = newItems.indexOf(entry.item); + if (newIndex != -1) { + foundItemCount++; + if (entry.position.differs(newIndex)) { + onBeforeListChanged(); + entry.position.setTo(newIndex); + } + if (entry.index != newIndex) { + entry.index = newIndex; + needSort = true; + needSortActual = needSortActual || entry.isAffectingList(); + } + if (entry.visibility.differs(1f)) { + onBeforeListChanged(); + entry.onPrepareAppear(); + actualList.add(entry); + needSortActual = true; + metadata.setSize(actualList.size(), true); + if (resetCallback != null) { + resetCallback.onItemAdded(entry.item, true); + } + } + } else { + if (entry.visibility.differs(0f)) { + onBeforeListChanged(); + entry.onPrepareRemove(); + boolean removed = needSortActual ? actualList.remove(entry) : ArrayUtils.removeSorted(actualList, entry); + if (!removed) { + throw new IllegalArgumentException(); + } + metadata.setSize(actualList.size(), true); + if (resetCallback != null) { + resetCallback.onItemRemoved(entry.item); + } + } + } + } + + if (needSortActual) { + Collections.sort(actualList); + } + + // Second, find additions + + if (foundItemCount < newItems.size()) { + entries.ensureCapacity(entries.size() + (newItems.size() - foundItemCount)); + int index = 0; + for (T newItem : newItems) { + int existingIndex = indexOfItem(newItem); + if (existingIndex == -1) { + if (index != entries.size()) { + needSort = true; + } + onBeforeListChanged(); + Entry entry = new Entry<>(newItem, index, false); + entry.onPrepareAppear(); + entries.add(entry); + ArrayUtils.addSorted(actualList, entry); + metadata.setSize(actualList.size(), true); + if (resetCallback != null) { + resetCallback.onItemAdded(entry.item, false); + } + } + index++; + } + } + } else { + if (!foundListChanges) { + // Triggering the removeJunk call + for (Entry entry : entries) { + if (entry.visibility.differs(0f)) { + onBeforeListChanged(); + break; + } + } + } + if (foundListChanges) { + for (Entry entry : entries) { + if (entry.visibility.differs(0f)) { + onBeforeListChanged(); + entry.onPrepareRemove(); + ArrayUtils.removeSorted(actualList, entry); + metadata.setSize(actualList.size(), true); + if (resetCallback != null) { + resetCallback.onItemRemoved(entry.item); + } + } + } + } + } + + // Then, sort and run animation, if needed + + if (needSort) { + Collections.sort(entries); + } + + measureImpl(true); + + onApplyListChanges(); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/util/HapticMenuHelper.java b/app/src/main/java/org/thunderdog/challegram/util/HapticMenuHelper.java index eef001db6b..9d435940ec 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/HapticMenuHelper.java +++ b/app/src/main/java/org/thunderdog/challegram/util/HapticMenuHelper.java @@ -310,9 +310,9 @@ private void openMenu (View view, List items, int anchorMode) { int centerY = out[1] + viewHeight / 2; int resultCenterX = Math.max(viewWidth / 2, Math.min(parentWidth - viewWidth / 2, targetCenterX)); - int resultCenterY = targetCenterY - targetHeight / 2 - (anchorMode == MenuMoreWrap.ANCHOR_MODE_CENTER ? Screen.dp(12): viewHeight / 2); + int resultCenterY = targetCenterY - targetHeight / 2 - (anchorMode == MenuMoreWrap.ANCHOR_MODE_CENTER ? Screen.dp(12) : viewHeight / 2); - v.setTranslationX(resultCenterX - centerX + (anchorMode == MenuMoreWrap.ANCHOR_MODE_CENTER ? Screen.dp(8): 0)); + v.setTranslationX(resultCenterX - centerX + (anchorMode == MenuMoreWrap.ANCHOR_MODE_CENTER ? Screen.dp(8) : 0)); v.setTranslationY(resultCenterY - centerY); if (anchorMode == MenuMoreWrap.ANCHOR_MODE_CENTER) { diff --git a/app/src/main/java/org/thunderdog/challegram/util/ListItemDiffUtilCallback.java b/app/src/main/java/org/thunderdog/challegram/util/ListItemDiffUtilCallback.java new file mode 100644 index 0000000000..8cd5f0d74d --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/util/ListItemDiffUtilCallback.java @@ -0,0 +1,44 @@ +package org.thunderdog.challegram.util; + +import androidx.recyclerview.widget.DiffUtil; + +import org.thunderdog.challegram.ui.ListItem; + +import java.util.List; + +public abstract class ListItemDiffUtilCallback extends DiffUtil.Callback { + private final List oldList; + private final List newList; + + public ListItemDiffUtilCallback (List oldList, List newList) { + this.oldList = oldList; + this.newList = newList; + } + + @Override + public final int getOldListSize () { + return oldList.size(); + } + + @Override + public final int getNewListSize () { + return newList.size(); + } + + @Override + public final boolean areItemsTheSame (int oldItemPosition, int newItemPosition) { + ListItem oldItem = oldList.get(oldItemPosition); + ListItem newItem = newList.get(newItemPosition); + return areItemsTheSame(oldItem, newItem); + } + + @Override + public final boolean areContentsTheSame (int oldItemPosition, int newItemPosition) { + ListItem oldItem = oldList.get(oldItemPosition); + ListItem newItem = newList.get(newItemPosition); + return areContentsTheSame(oldItem, newItem); + } + + public abstract boolean areItemsTheSame (ListItem oldItem, ListItem newItem); + public abstract boolean areContentsTheSame (ListItem oldItem, ListItem newItem); +} diff --git a/app/src/main/java/org/thunderdog/challegram/util/NonBubbleEmojiLayout.java b/app/src/main/java/org/thunderdog/challegram/util/NonBubbleEmojiLayout.java new file mode 100644 index 0000000000..fc3a47a634 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/util/NonBubbleEmojiLayout.java @@ -0,0 +1,248 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 02/10/2023 + */ +package org.thunderdog.challegram.util; + +import android.text.Spannable; + +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.emoji.Emoji; +import org.thunderdog.challegram.emoji.EmojiInfo; +import org.thunderdog.challegram.emoji.EmojiSpan; + +import java.util.ArrayList; + +import me.vkryl.core.StringUtils; + +public class NonBubbleEmojiLayout { + public final ArrayList items = new ArrayList<>(); + + @Nullable + public static NonBubbleEmojiLayout create (TdApi.FormattedText text) { + NonBubbleEmojiLayout layout = new NonBubbleEmojiLayout(); + if (isValidEmojiText(text, layout)) { + return layout; + } else { + return null; + } + } + + private void addSpan (String emoji, long customEmojiId, @Nullable EmojiInfo info) { + items.add(new Item(Item.EMOJI, emoji, customEmojiId, info)); + } + + private void addRow () { + items.add(new Item(Item.LINE_BREAK)); + } + + private void addSpace () { + items.add(new Item(Item.SPACE)); + } + + public static boolean isValidEmojiText (TdApi.FormattedText formattedText) { + return isValidEmojiText(formattedText, null); + } + + private static boolean isValidEmojiText (TdApi.FormattedText formattedText, @Nullable NonBubbleEmojiLayout layout) { + if (StringUtils.isEmpty(formattedText.text)) { + return false; + } + + final int textSize = formattedText.text.length(); + int index = 0; + if (formattedText.entities != null) { + for (TdApi.TextEntity entity : formattedText.entities) { + switch (entity.type.getConstructor()) { + case TdApi.TextEntityTypeBold.CONSTRUCTOR: + case TdApi.TextEntityTypeItalic.CONSTRUCTOR: + case TdApi.TextEntityTypeUnderline.CONSTRUCTOR: + case TdApi.TextEntityTypeStrikethrough.CONSTRUCTOR: + continue; + case TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR: { + int start = entity.offset; + int end = start + entity.length; + if (index != start && !isValidTextBlock(formattedText.text.substring(index, start), layout)) { + return false; + } + index = end; + if (layout != null) { + layout.addSpan(formattedText.text.substring(start, end), ((TdApi.TextEntityTypeCustomEmoji) entity.type).customEmojiId, null); + } + continue; + } + default: + return false; + } + } + } + + return index == textSize || isValidTextBlock(formattedText.text.substring(index, textSize), layout); + } + + private static boolean isValidTextBlock (String text, @Nullable NonBubbleEmojiLayout layout) { + CharSequence parsed = Emoji.instance().replaceEmoji(text); + if (!(parsed instanceof Spannable)) { + return isValidAllowedSymbols(text, layout); + } + + Spannable spannable = (Spannable) parsed; + for (int index = 0; index < spannable.length(); ) { + EmojiSpan[] spans = spannable.getSpans(index, index, EmojiSpan.class); + if (spans == null || spans.length == 0) { + if (!isValidAllowedSymbols(text.substring(index, index + 1), layout)) { + return false; + } + index += 1; + } else { + boolean spanIsFound = false; + for (EmojiSpan span : spans) { + int start = spannable.getSpanStart(span); + int end = spannable.getSpanEnd(span); + if (start < index) { + continue; + } + if (index == end) { + return false; + } + index = end; + if (spanIsFound) { + return false; + } + spanIsFound = true; + if (layout != null) { + layout.addSpan(text.substring(start, end), span.getCustomEmojiId(), span.getBuiltInEmojiInfo()); + } + } + if (!spanIsFound) { + if (isValidAllowedSymbols(text.substring(index, index + 1), layout)) { + index++; + } else { + return false; + } + } + } + } + + return true; + } + + private static boolean isValidAllowedSymbols (String text, @Nullable NonBubbleEmojiLayout layout) { + for (int a = 0; a < text.length(); a++) { + char c = text.charAt(a); + switch (c) { + case ' ': { + if (layout != null) { + layout.addSpace(); + } + continue; + } + case '\n': { + if (layout != null) { + layout.addRow(); + } + continue; + } + } + return false; + } + return true; + } + + private float lastMaxRowSize; + private float lastSpaceSize; + private LayoutBuildResult lastLayoutResult; + + public LayoutBuildResult layout (float maxRowSize, float spaceSize) { + if (maxRowSize == lastMaxRowSize && spaceSize == lastSpaceSize && lastLayoutResult != null) { + return lastLayoutResult; + } + + lastMaxRowSize = maxRowSize; + lastSpaceSize = spaceSize; + return lastLayoutResult = new LayoutBuildResult(items, maxRowSize, spaceSize); + } + + public static class LayoutBuildResult { + public final ArrayList representations = new ArrayList<>(); + public final float maxLineSize; + public final int linesCount; + public final boolean hasClassicEmoji; + + private LayoutBuildResult (ArrayList items, float maxRowSize, float spaceSize) { + float currentX = 0; + float maxRowSizeReal = 0; + boolean hasClassicEmoji = false; + int currentRow = 0; + + for (Item item : items) { + if (item.type == Item.SPACE) { + currentX += spaceSize; + } else if (item.type == Item.LINE_BREAK) { + currentX = 0; + currentRow += 1; + } else if (item.type == Item.EMOJI) { + hasClassicEmoji |= item.customEmojiId == 0; + if ((currentX + 1f) > maxRowSize) { + currentX = 0; + currentRow += 1; + } + representations.add(new Representation(item.emoji, item.customEmojiId, currentX, currentRow)); + currentX += 1f; + maxRowSizeReal = Math.max(maxRowSizeReal, currentX); + } + } + this.hasClassicEmoji = hasClassicEmoji; + this.maxLineSize = maxRowSizeReal; + this.linesCount = currentRow + 1; + } + } + + public static class Representation { + public final String emoji; + public final long customEmojiId; + public final float x; + public final int y; + + private Representation (String emoji, long customEmojiId, float x, int y) { + this.customEmojiId = customEmojiId; + this.emoji = emoji; + this.x = x; + this.y = y; + } + } + + public static class Item { + public static final int EMOJI = 0; + public static final int SPACE = 1; + public static final int LINE_BREAK = 2; + + public final String emoji; + public final long customEmojiId; + public final int type; + public final EmojiInfo info; + + private Item (int type) { + this(type, null, 0, null); + } + + private Item (int type, String emoji, long customEmojiId, EmojiInfo info) { + this.type = type; + this.emoji = emoji; + this.customEmojiId = customEmojiId; + this.info = info; + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/util/ProfilePhotoDrawModifier.java b/app/src/main/java/org/thunderdog/challegram/util/ProfilePhotoDrawModifier.java new file mode 100644 index 0000000000..ca7304090a --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/util/ProfilePhotoDrawModifier.java @@ -0,0 +1,71 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 12/11/2023, 15:38. + */ + +package org.thunderdog.challegram.util; + +import android.graphics.Canvas; +import android.view.View; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.loader.AvatarReceiver; +import org.thunderdog.challegram.loader.ComplexReceiver; +import org.thunderdog.challegram.loader.ComplexReceiverProvider; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.tool.Screen; + +import me.vkryl.td.Td; + +public class ProfilePhotoDrawModifier implements DrawModifier { + @Override + public void afterDraw (View view, Canvas c) { + ComplexReceiver complexReceiver = view instanceof ComplexReceiverProvider ? ((ComplexReceiverProvider) view).getComplexReceiver() : null; + if (complexReceiver == null) return; + + AvatarReceiver avatarReceiver = complexReceiver.getAvatarReceiver(0); + if (avatarReceiver.isEmpty()) return; + + int size = Screen.dp(48); + int x = view.getMeasuredWidth() - size - Screen.dp(20); + int y = Screen.dp(8); + + avatarReceiver.setBounds(x, y, x + size, y + size); + if (avatarReceiver.needPlaceholder()) { + avatarReceiver.drawPlaceholder(c); + } + avatarReceiver.draw(c); + } + + public ProfilePhotoDrawModifier requestFiles (ComplexReceiver complexReceiver, Tdlib tdlib) { + AvatarReceiver avatarReceiver = complexReceiver.getAvatarReceiver(0); + + TdApi.UserFullInfo info = tdlib.myUserFull(); + if (info != null && info.publicPhoto != null && info.publicPhoto.sizes != null && info.publicPhoto.sizes.length > 0) { + TdApi.ChatPhotoInfo chatPhotoInfo = new TdApi.ChatPhotoInfo( + Td.findSmallest(info.publicPhoto.sizes).photo, + Td.findBiggest(info.publicPhoto.sizes).photo, + info.publicPhoto.minithumbnail, info.publicPhoto.animation != null, false); + avatarReceiver.requestSpecific(tdlib, chatPhotoInfo, AvatarReceiver.Options.NO_UPDATES); + } else { + avatarReceiver.clear(); + } + + return this; + } + + @Override + public int getWidth () { + return Screen.dp(48); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/util/RateLimiter.java b/app/src/main/java/org/thunderdog/challegram/util/RateLimiter.java new file mode 100644 index 0000000000..52941693d1 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/util/RateLimiter.java @@ -0,0 +1,72 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 26/10/2023 + */ +package org.thunderdog.challegram.util; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; + +import androidx.annotation.Nullable; + +public final class RateLimiter implements Runnable { + private final Runnable act; + private final long delayMs; + private final Handler handler; + + private long lastExecutionTime; + private boolean isScheduled, delayFirstExecution; + + public RateLimiter (Runnable act, long delayMs, @Nullable Looper looper) { + this.act = act; + this.delayMs = delayMs; + this.handler = new Handler(looper != null ? looper : Looper.getMainLooper(), (msg) -> { + runImpl(true); + return true; + }); + } + + public void setDelayFirstExecution (boolean delayFirstExecution) { + this.delayFirstExecution = delayFirstExecution; + } + + public void cancelIfScheduled () { + if (isScheduled) { + handler.removeMessages(0); + isScheduled = false; + } + } + + @Override + public void run () { + long now = SystemClock.uptimeMillis(); + if ((lastExecutionTime == 0 || (now - lastExecutionTime) >= delayMs) && runImpl(false)) { + return; + } + if (!isScheduled) { + long delayMs = lastExecutionTime != 0 ? (lastExecutionTime + this.delayMs) - now : this.delayMs; + handler.sendMessageDelayed(handler.obtainMessage(0), delayMs); + } + } + + private boolean runImpl (boolean byTimeout) { + if (byTimeout || !delayFirstExecution) { + lastExecutionTime = SystemClock.uptimeMillis(); + isScheduled = false; + act.run(); + return true; + } + return false; + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/util/ReactionModifier.java b/app/src/main/java/org/thunderdog/challegram/util/ReactionModifier.java index 9cbe9a2043..9b5718e6db 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/ReactionModifier.java +++ b/app/src/main/java/org/thunderdog/challegram/util/ReactionModifier.java @@ -3,110 +3,144 @@ import android.graphics.Canvas; import android.view.View; +import androidx.annotation.NonNull; + import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGReaction; import org.thunderdog.challegram.loader.ComplexReceiver; -import org.thunderdog.challegram.loader.DoubleImageReceiver; +import org.thunderdog.challegram.loader.ComplexReceiverProvider; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.ImageReceiver; -import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.loader.gif.GifReceiver; +import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.tool.DrawAlgorithms; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; -import java.util.ArrayList; -import java.util.List; +public class ReactionModifier implements DrawModifier { + public static final int MODE_GRID = 0; + public static final int MODE_INLINE = 1; -import me.vkryl.core.lambda.Destroyable; + private final TGReaction[] reactions; + private int mode = MODE_GRID; -public class ReactionModifier implements DrawModifier { - private final float[] previewScales, gifScales; - private final ImageReceiver[] previewReceivers; - private final GifReceiver[] gifReceivers; - private final int offset; - private final int size; - private final int totalWidth; - private final int totalHeight; - private final int padding; - - public ReactionModifier (ComplexReceiver complexReceiver, TGReaction... reactions) { - this(complexReceiver, 18, reactions); + private int offset = 18; + + public ReactionModifier (Tdlib tdlib, String[] keys) { + this(toReactions(tdlib, keys)); + } + + public ReactionModifier (TGReaction... reactions) { + this.reactions = reactions; } - public ReactionModifier (ComplexReceiver complexReceiver, int offsetInDp, TGReaction... reactions) { - size = Screen.dp(reactions.length == 1 ? 40 : 20); - totalWidth = Screen.dp(40); - totalHeight = reactions.length > 2 ? totalWidth : size; - padding = Screen.dp(reactions.length == 1 ? 0 : -8); - offset = offsetInDp; + @Override + public void afterDraw (View view, Canvas c) { + ComplexReceiver complexReceiver = view instanceof ComplexReceiverProvider ? ((ComplexReceiverProvider) view).getComplexReceiver() : null; + if (complexReceiver == null) return; + + if (mode == MODE_GRID) { + drawGridMode(view, c, complexReceiver); + } else if (mode == MODE_INLINE) { + drawInlineMode(view, c, complexReceiver); + } + } - previewReceivers = new ImageReceiver[reactions.length]; - gifReceivers = new GifReceiver[reactions.length]; - previewScales = new float[reactions.length]; - gifScales = new float[reactions.length]; + @Override + public int getWidth () { + return Screen.dp(mode == MODE_INLINE ? (26 * reactions.length + 14) : 48); + } + public ReactionModifier requestFiles (ComplexReceiver complexReceiver) { for (int a = 0; a < reactions.length; a++) { - previewReceivers[a] = complexReceiver.getImageReceiver(a * 2L + 1); - gifReceivers[a] = complexReceiver.getGifReceiver(a * 2L); TGReaction reaction = reactions[a]; - if (reaction != null) { - TGStickerObj centerAnimation = reaction.staticCenterAnimationSicker(); - TGStickerObj staticSticker = reaction.staticCenterAnimationSicker(); - previewScales[a] = centerAnimation.getDisplayScale(); - gifScales[a] = staticSticker.getDisplayScale(); - ImageFile previewImage = centerAnimation.getImage(); - GifFile gifFile = staticSticker.getPreviewAnimation(); - previewReceivers[a].requestFile(previewImage); - gifReceivers[a].requestFile(gifFile); - } + TGStickerObj centerAnimation = reaction.staticCenterAnimationSicker(); + TGStickerObj staticSticker = reaction.staticCenterAnimationSicker(); + ImageFile previewImage = centerAnimation.getImage(); + GifFile gifFile = staticSticker.getPreviewAnimation(); + complexReceiver.getImageReceiver(a * 2L + 1).requestFile(previewImage); + complexReceiver.getGifReceiver(a * 2L).requestFile(gifFile); } + + return this; } - @Override - public void afterDraw (View view, Canvas c) { + public ReactionModifier setOffset (int offset) { + this.offset = offset; + return this; + } + + public ReactionModifier setMode (int mode) { + this.mode = mode; + return this; + } + + private void drawGridMode (View view, Canvas c, @NonNull ComplexReceiver complexReceiver) { + int size = Screen.dp(reactions.length == 1 ? 40 : 20); + int totalWidth = Screen.dp(40); + int totalHeight = reactions.length > 2 ? totalWidth : size; + int padding = Screen.dp(reactions.length == 1 ? 0 : -8); int sx = 0; int sy = 0; final int saveCount = Views.save(c); c.translate(view.getMeasuredWidth() - Screen.dp(offset) - totalWidth, view.getMeasuredHeight() / 2f - totalHeight / 2f); - if (gifReceivers.length > 0) { - DrawAlgorithms.drawReceiver(c, previewReceivers[0], gifReceivers[0], false, true, - padding, padding, size - padding, size - padding, - previewScales[0], gifScales[0] - ); - sx += size; - } - if (gifReceivers.length > 1) { - DrawAlgorithms.drawReceiver(c, previewReceivers[1], gifReceivers[1], false, true, - sx + padding, sy + padding, size + sx - padding, size + sy - padding, - previewScales[1], gifScales[1] - ); - sx = gifReceivers.length == 4 ? 0 : size / 2 ; sy += size; - } + for (int a = 0; a < reactions.length; a++) { + TGReaction reaction = reactions[a]; + ImageReceiver imageReceiver = complexReceiver.getImageReceiver(a * 2L + 1); + GifReceiver gifReceiver = complexReceiver.getGifReceiver(a * 2L); + final float previewScale = reaction.staticCenterAnimationSicker().getDisplayScale(); + final float gifScale = reaction.staticCenterAnimationSicker().getDisplayScale(); + + DrawAlgorithms.drawReceiver(c, imageReceiver, gifReceiver, false, true, + sx + padding, sy + padding, size + sx - padding, size + sy - padding, previewScale, gifScale); - if (gifReceivers.length > 2) { - DrawAlgorithms.drawReceiver(c, previewReceivers[2], gifReceivers[2], false, true, - sx + padding, sy + padding, size + sx - padding, size + sy - padding, - previewScales[2], gifScales[2] - ); - sx += size; + if (a == 0) { + sx += size; + } + if (a == 1) { + sx = reactions.length == 4 ? 0 : size / 2; + sy += size; + } + if (a == 2) { + sx += size; + } } - if (gifReceivers.length > 3) { - DrawAlgorithms.drawReceiver(c, previewReceivers[3], gifReceivers[3], false, true, - sx + padding, sy + padding, size + sx- padding, size + sy - padding, - previewScales[3], gifScales[3] - ); + Views.restore(c, saveCount); + } + + private void drawInlineMode (View view, Canvas c, @NonNull ComplexReceiver complexReceiver) { + final int saveCount = Views.save(c); + c.translate(view.getMeasuredWidth() - Screen.dp(offset) - getWidth(), view.getMeasuredHeight() / 2f - Screen.dp(20)); + + int size = Screen.dp(40); + int sx = 0; + int sy = 0; + + for (int a = 0; a < reactions.length; a++) { + TGReaction reaction = reactions[a]; + ImageReceiver imageReceiver = complexReceiver.getImageReceiver(a * 2L + 1); + GifReceiver gifReceiver = complexReceiver.getGifReceiver(a * 2L); + final float previewScale = reaction.staticCenterAnimationSicker().getDisplayScale(); + final float gifScale = reaction.staticCenterAnimationSicker().getDisplayScale(); + DrawAlgorithms.drawReceiver(c, imageReceiver, gifReceiver, false, true, + sx, sy, size + sx, size + sy, previewScale, gifScale); + + sx += Screen.dp(26); } Views.restore(c, saveCount); } - @Override - public int getWidth () { - return Screen.dp(48); + private static TGReaction[] toReactions (Tdlib tdlib, String[] keys) { + TGReaction[] reactions = new TGReaction[keys.length]; + for (int a = 0; a < keys.length; a++) { + reactions[a] = tdlib.getReaction(TD.toReactionType(keys[a])); + } + return reactions; } } diff --git a/app/src/main/java/org/thunderdog/challegram/util/ReactionsCounterDrawable.java b/app/src/main/java/org/thunderdog/challegram/util/ReactionsCounterDrawable.java index 4f6a0c2310..c66b20a3b3 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/ReactionsCounterDrawable.java +++ b/app/src/main/java/org/thunderdog/challegram/util/ReactionsCounterDrawable.java @@ -47,6 +47,18 @@ private float getVisibility (ReactionsListAnimator.Entry item) { return visibility; } + private float getTargetVisibility (ReactionsListAnimator.Entry item) { + float position = item.getPosition(); + float visibility = item.isAffectingList() ? 1f: 0f; + if (position > 3f) { + return 0f; + } else if (position > 2f) { + return Math.min(visibility, 3f - position); + } + + return visibility; + } + @Override public int getMinimumWidth () { float width = 0f; @@ -57,6 +69,15 @@ public int getMinimumWidth () { return Math.max((int) width - Screen.dp(3), 0); } + public int getTargetWidth () { + float width = 0f; + for (int a = 0; a < topReactions.size(); a++) { + ReactionsListAnimator.Entry item = topReactions.getEntry(a); + width += Screen.dp(15) * getTargetVisibility(item); + } + return Math.max((int) width - Screen.dp(3), 0); + } + @Override public void draw (@NonNull Canvas c) { // Never use draw(c, 0, 0); diff --git a/app/src/main/java/org/thunderdog/challegram/util/ReactionsListAnimator.java b/app/src/main/java/org/thunderdog/challegram/util/ReactionsListAnimator.java index 3b739b93af..5cb4925306 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/ReactionsListAnimator.java +++ b/app/src/main/java/org/thunderdog/challegram/util/ReactionsListAnimator.java @@ -355,7 +355,7 @@ public void measureImpl (boolean animated) { for (Entry entry : actualList) { TGReactions.MessageReactionEntry item = entry.item; - int itemWidth = item.getBubbleWidth(); + int itemWidth = item.getBubbleTargetWidth(); // item.getBubbleWidth(); int itemHeight = item.getBubbleHeight(); int left = item.getX(); diff --git a/app/src/main/java/org/thunderdog/challegram/util/ScrollJumpCompensator.java b/app/src/main/java/org/thunderdog/challegram/util/ScrollJumpCompensator.java index 164bc6b64d..9649be53f1 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/ScrollJumpCompensator.java +++ b/app/src/main/java/org/thunderdog/challegram/util/ScrollJumpCompensator.java @@ -14,21 +14,28 @@ */ package org.thunderdog.challegram.util; -import android.util.Log; import android.view.View; import android.view.ViewTreeObserver; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; public class ScrollJumpCompensator implements ViewTreeObserver.OnGlobalLayoutListener { private final RecyclerView recyclerView; private final ViewTreeObserver observer; + private final @Nullable Runnable after; private int offset; + @Deprecated public ScrollJumpCompensator (RecyclerView r, View v, int offset) { + this(r, v, offset, null); + } + + private ScrollJumpCompensator (RecyclerView r, View v, int offset, @Nullable Runnable after) { this.recyclerView = r; this.observer = v.getViewTreeObserver(); this.offset = offset; + this.after = after; } public void add () { @@ -40,6 +47,9 @@ public void onGlobalLayout () { if (offset != 0) { recyclerView.scrollBy(0, offset); offset = 0; + if (after != null) { + after.run(); + } } remove(observer, this); @@ -62,6 +72,11 @@ public static void compensate (RecyclerView r, int offset) { x.add(); } + public static void compensate (RecyclerView r, int offset, Runnable after) { + ScrollJumpCompensator x = new ScrollJumpCompensator(r, r, offset, after); + x.add(); + } + public static void compensate (RecyclerView r, View v, int offset) { ScrollJumpCompensator x = new ScrollJumpCompensator(r, v, offset); x.add(); diff --git a/app/src/main/java/org/thunderdog/challegram/util/StickerSetsDataProvider.java b/app/src/main/java/org/thunderdog/challegram/util/StickerSetsDataProvider.java new file mode 100644 index 0000000000..633bb23838 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/util/StickerSetsDataProvider.java @@ -0,0 +1,80 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 02/09/2023 + */ +package org.thunderdog.challegram.util; + +import org.drinkless.tdlib.Client; +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.tool.UI; + +import me.vkryl.core.collection.LongSparseIntArray; + +public abstract class StickerSetsDataProvider implements TGStickerObj.DataProvider { + public static final int FLAG_TRENDING = 0x01; + public static final int FLAG_REGULAR = 0x02; + + private final LongSparseIntArray loadingStickerSets = new LongSparseIntArray(); + private final Tdlib tdlib; + + public StickerSetsDataProvider (Tdlib tdlib) { + this.tdlib = tdlib; + } + + @Override + public void requestStickerData (TGStickerObj sticker, long stickerSetId) { + if (needIgnoreRequests(stickerSetId, sticker)) { + return; + } + + final int currentFlags = loadingStickerSets.get(stickerSetId, 0); + final int loadingFlags = getLoadingFlags(stickerSetId, sticker); + final boolean needRequestData = ((currentFlags ^ loadingFlags) & loadingFlags) != 0; + + loadingStickerSets.put(stickerSetId, currentFlags | loadingFlags); + + if (needRequestData) { + tdlib.client().send(new TdApi.GetStickerSet(stickerSetId), singleStickerSetHandler()); + } + } + + public void clear () { + loadingStickerSets.clear(); + } + + private Client.ResultHandler singleStickerSetHandler () { + return object -> { + switch (object.getConstructor()) { + case TdApi.StickerSet.CONSTRUCTOR: { + final TdApi.StickerSet stickerSet = (TdApi.StickerSet) object; + UI.post(() -> { + final int flags = loadingStickerSets.get(stickerSet.id); + loadingStickerSets.delete(stickerSet.id); + applyStickerSet(stickerSet, flags); + }); + break; + } + case TdApi.Error.CONSTRUCTOR: { + UI.showError(object); + break; + } + } + }; + } + + protected abstract boolean needIgnoreRequests (long stickerSetId, TGStickerObj stickerObj); + protected abstract int getLoadingFlags (long stickerSetId, TGStickerObj stickerObj); + protected abstract void applyStickerSet (TdApi.StickerSet stickerSet, int flags); +} diff --git a/app/src/main/java/org/thunderdog/challegram/util/StringList.java b/app/src/main/java/org/thunderdog/challegram/util/StringList.java index 1b153871d0..e2b4ecff4b 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/StringList.java +++ b/app/src/main/java/org/thunderdog/challegram/util/StringList.java @@ -68,4 +68,8 @@ public String[] get () { public boolean isEmpty () { return list.isEmpty(); } + + public void clear () { + list.clear(); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/util/TokenRetriever.java b/app/src/main/java/org/thunderdog/challegram/util/TokenRetriever.java index 610bba9bd6..b5d0df4cd2 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/TokenRetriever.java +++ b/app/src/main/java/org/thunderdog/challegram/util/TokenRetriever.java @@ -28,6 +28,9 @@ public final boolean initialize (Context context) { protected abstract boolean performInitialization (Context context); + public abstract @NonNull String getName (); + public abstract @Nullable String getConfiguration (); + public final void retrieveDeviceToken (int retryCount, RegisterCallback callback) { if (!isInitialized) { throw new IllegalStateException(); diff --git a/app/src/main/java/org/thunderdog/challegram/util/TranslationCounterDrawable.java b/app/src/main/java/org/thunderdog/challegram/util/TranslationCounterDrawable.java index dc13106857..7ccef10225 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/TranslationCounterDrawable.java +++ b/app/src/main/java/org/thunderdog/challegram/util/TranslationCounterDrawable.java @@ -7,12 +7,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.thunderdog.challegram.R; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; -import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.UI; import me.vkryl.android.AnimatorUtils; @@ -39,7 +39,7 @@ public class TranslationCounterDrawable extends Drawable implements FactorAnimat private Runnable invalidateCallback; private int defaultColorId = ColorId.icon; - private int backgroundColorId = ColorId.bubbleIn_time; + private @PorterDuffColorId int backgroundColorId = ColorId.bubbleIn_time; private int loadingColorId = ColorId.bubbleIn_textLink; public TranslationCounterDrawable (Drawable drawable) { @@ -51,7 +51,7 @@ public TranslationCounterDrawable (Drawable drawable) { UI.post(this::checkStatus); } - public void setColors (int defaultColorId, int backgroundColorId, int loadingColorId) { + public void setColors (int defaultColorId, @PorterDuffColorId int backgroundColorId, int loadingColorId) { this.defaultColorId = defaultColorId; this.backgroundColorId = backgroundColorId; this.loadingColorId = loadingColorId; @@ -97,7 +97,7 @@ public void draw (@NonNull Canvas canvas) { if (loadedProgress == 1f) { Drawables.draw(canvas, drawable, 0, 0, Paints.getPorterDuffPaint(iconColor)); } else { - Drawables.draw(canvas, drawableBg, 0, 0, Paints.getPorterDuffPaint(Theme.getColor(backgroundColorId))); + Drawables.draw(canvas, drawableBg, 0, 0, PorterDuffPaint.get(backgroundColorId)); float lineWidth = MathUtils.fromTo(0.571f, 1f, loadedProgress) * width; float offset = MathUtils.fromTo(MathUtils.fromTo(-lineWidth - width * 0.5f, width * 1.5f, offsetAnimator.getFloatValue()), 0, loadedProgress); @@ -147,10 +147,14 @@ public void onFactorChanged (int id, float factor, float fraction, FactorAnimato invalidate(); } + private final RateLimiter checkLimiter = new RateLimiter(this::checkStatus, 100L, null); + @Override public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { if (id == ANIMATOR_OFFSET) { - checkStatus(); + if (finalFactor == (offsetAnimator.getValue() ? 1f : 0f)) { + checkLimiter.run(); + } } } } diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/Counter.java b/app/src/main/java/org/thunderdog/challegram/util/text/Counter.java index 1b7c92d709..6780d41a67 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/Counter.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/Counter.java @@ -15,18 +15,19 @@ package org.thunderdog.challegram.util.text; import android.graphics.Canvas; +import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.view.Gravity; import android.view.View; +import androidx.annotation.Dimension; import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; -import org.thunderdog.challegram.R; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; -import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.tool.DrawAlgorithms; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; @@ -40,9 +41,9 @@ import me.vkryl.android.animator.CounterAnimator; import me.vkryl.android.animator.FactorAnimator; import me.vkryl.android.animator.ListAnimator; +import me.vkryl.core.BitwiseUtils; import me.vkryl.core.ColorUtils; import me.vkryl.core.MathUtils; -import me.vkryl.core.BitwiseUtils; public final class Counter implements FactorAnimator.Target, CounterAnimator.Callback, TextColorSet { public static Callback newCallback (View view) { @@ -61,12 +62,14 @@ public boolean needAnimateChanges (Counter counter) { private static final int FLAG_ALL_BOLD = 1; private static final int FLAG_NEED_BACKGROUND = 1 << 1; + private static final int FLAG_OUTLINE_AFFECTS_BACKGROUND_SIZE = 1 << 2; public static class Builder { public Builder () { } - private float textSize = 13f; - private int flags = FLAG_ALL_BOLD | FLAG_NEED_BACKGROUND; + private @Dimension(unit = Dimension.DP) float textSize = 13f; + private @Dimension(unit = Dimension.DP) float backgroundPadding = 3f; + private int flags = FLAG_ALL_BOLD | FLAG_NEED_BACKGROUND | FLAG_OUTLINE_AFFECTS_BACKGROUND_SIZE; private Callback callback; private int drawableRes; @@ -92,7 +95,12 @@ public Builder noBackground () { return this; } - public Builder textSize (float textSize) { + public Builder outlineAffectsBackgroundSize (boolean outlineAffectsBackgroundSize) { + this.flags = BitwiseUtils.setFlag(flags, FLAG_OUTLINE_AFFECTS_BACKGROUND_SIZE, outlineAffectsBackgroundSize); + return this; + } + + public Builder textSize (@Dimension(unit = Dimension.DP) float textSize) { this.textSize = textSize; return this; } @@ -148,11 +156,16 @@ public Builder visibleIfZero () { return this; } + public Builder backgroundPadding (@Dimension(unit = Dimension.DP) float backgroundPadding) { + this.backgroundPadding = backgroundPadding; + return this; + } + public Counter build () { return new Counter(textSize, callback, flags, textColorId, mutedTextColorId, failedTextColorId, outlineColorId, drawableRes, drawableWidthDp, drawableMarginDp, drawableGravity, - colorSet, extendedDrawable, visibleIfZero + colorSet, extendedDrawable, visibleIfZero, backgroundPadding ); } } @@ -176,7 +189,8 @@ public interface Callback { private final Drawable extendedDrawable; private final float drawableWidthDp, drawableMarginDp; private final int drawableGravity; - private boolean visibleIfZero; + private final boolean visibleIfZero; + private final @Dimension(unit = Dimension.DP) float backgroundPadding; @ColorId private final int textColorId, mutedTextColorId, failedTextColorId, outlineColorId; @@ -186,7 +200,7 @@ public interface Callback { private Counter (float textSize, Callback callback, int flags, @ColorId int textColorId, @ColorId int mutedTextColorId, @ColorId int failedTextColorId, @ColorId int outlineColorId, @DrawableRes int drawableRes, float drawableWidthDp, float drawableMarginDp, int drawableGravity, - @Nullable TextColorSet colorSet, Drawable counterDrawable, boolean visibleIfZero) { + @Nullable TextColorSet colorSet, Drawable counterDrawable, boolean visibleIfZero, @Dimension(unit = Dimension.DP) float backgroundPadding) { this.textSize = textSize; this.callback = callback; this.flags = flags; @@ -201,6 +215,7 @@ private Counter (float textSize, Callback callback, int flags, this.colorSet = colorSet; this.extendedDrawable = counterDrawable; this.visibleIfZero = visibleIfZero; + this.backgroundPadding = backgroundPadding; } public int getColor (float muteFactor, int mainColorId, int mutedColorId, int failedColorId) { @@ -252,7 +267,15 @@ public void showHide (boolean show, boolean animated) { } } - public void setCount (int count, boolean muted, boolean animated) { + public boolean getVisibilityTarget () { + return isVisibleTarget; + } + + public void setCount (long count, boolean muted, boolean animated) { + setCount(count, muted, null, animated); + } + + public void setCount (long count, boolean muted, @Nullable String textRepresentation, boolean animated) { if (animated && (callback == null || !callback.needAnimateChanges(this))) animated = false; if (animated && !UI.inUiThread()) @@ -266,7 +289,7 @@ public void setCount (int count, boolean muted, boolean animated) { } else if (count == Tdlib.CHAT_FAILED && drawableRes == 0) { counter.setCounter(count, "!", animateChanges); } else if (count > 0 || (visibleIfZero && count == 0)) { - counter.setCounter(count, Strings.buildCounter(count), animateChanges); + counter.setCounter(count, textRepresentation != null ? textRepresentation : Strings.buildCounter(count), animateChanges); } else { counter.hideCounter(animateChanges); } @@ -300,7 +323,7 @@ private Drawable getDrawable (DrawableProvider drawableProvider, @PorterDuffColo } public float getWidth () { - return DrawAlgorithms.getCounterWidth(textSize, BitwiseUtils.hasFlag(flags, FLAG_NEED_BACKGROUND), counter, getDrawableWidth()); + return DrawAlgorithms.getCounterWidth(textSize, BitwiseUtils.hasFlag(flags, FLAG_NEED_BACKGROUND), counter, getDrawableWidth(), Screen.dp(backgroundPadding)); } public float getTargetWidth () { @@ -309,7 +332,7 @@ public float getTargetWidth () { targetTotalWidth += entry.isAffectingList() ? entry.item.getWidth() : 0f; } - return DrawAlgorithms.getCounterWidth(textSize, BitwiseUtils.hasFlag(flags, FLAG_NEED_BACKGROUND), targetTotalWidth, getDrawableWidth()); + return DrawAlgorithms.getCounterWidth(textSize, BitwiseUtils.hasFlag(flags, FLAG_NEED_BACKGROUND), targetTotalWidth, getDrawableWidth(), Screen.dp(backgroundPadding)); } public float getScaledWidth (int addWidth) { @@ -336,14 +359,28 @@ public void draw (Canvas c, float cx, float cy, int gravity, float alpha) { draw(c, cx, cy, gravity, alpha, null, 0); } + public void draw (Canvas c, float cx, float cy, int gravity, float alpha, @Nullable RectF outDrawRect) { + draw(c, cx, cy, gravity, alpha, alpha, alpha, null, 0, outDrawRect); + } + public void draw (Canvas c, float cx, float cy, int gravity, float alpha, DrawableProvider drawableProvider, @PorterDuffColorId int drawableColorId) { - draw(c, cx, cy, gravity, alpha, alpha, drawableProvider, drawableColorId); + draw(c, cx, cy, gravity, alpha, alpha, alpha, drawableProvider, drawableColorId); + } + + public void draw (Canvas c, float cx, float cy, int gravity, float alpha, DrawableProvider drawableProvider, @PorterDuffColorId int drawableColorId, @Nullable RectF outputDrawRect) { + draw(c, cx, cy, gravity, alpha, alpha, alpha, drawableProvider, drawableColorId, outputDrawRect); + } + + public void draw (Canvas c, float cx, float cy, int gravity, float textAlpha, float backgroundAlpha, float drawableAlpha, DrawableProvider drawableProvider, @PorterDuffColorId int drawableColorId) { + draw(c, cx, cy, gravity, textAlpha, backgroundAlpha, drawableAlpha, drawableProvider, drawableColorId, null); } - public void draw (Canvas c, float cx, float cy, int gravity, float alpha, float drawableAlpha, DrawableProvider drawableProvider, @PorterDuffColorId int drawableColorId) { - if (alpha * getVisibility() > 0f) { + public void draw (Canvas c, float cx, float cy, int gravity, float textAlpha, float backgroundAlpha, float drawableAlpha, DrawableProvider drawableProvider, @PorterDuffColorId int drawableColorId, @Nullable RectF outputDrawRect) { + boolean needBackground = BitwiseUtils.hasFlag(flags, FLAG_NEED_BACKGROUND); + boolean outlineAffectsBackgroundSize = needBackground && BitwiseUtils.hasFlag(flags, FLAG_OUTLINE_AFFECTS_BACKGROUND_SIZE); + if (textAlpha * getVisibility() > 0f || (needBackground && backgroundAlpha * getVisibility() > 0f)) { Drawable drawable = getDrawable(drawableProvider, drawableColorId); - DrawAlgorithms.drawCounter(c, cx, cy, gravity, counter, textSize, BitwiseUtils.hasFlag(flags, FLAG_NEED_BACKGROUND),this, drawable, drawableGravity, drawableColorId, Screen.dp(drawableMarginDp), alpha * getVisibility(), drawableAlpha * getVisibility(), isVisible.getFloatValue()); + DrawAlgorithms.drawCounter(c, cx, cy, gravity, counter, textSize, textAlpha * getVisibility(), needBackground, outlineAffectsBackgroundSize, Screen.dp(backgroundPadding), this, drawable, drawableGravity, drawableColorId, Screen.dp(drawableMarginDp), backgroundAlpha * getVisibility(), drawableAlpha * getVisibility(), isVisible.getFloatValue(), outputDrawRect); } } @@ -376,7 +413,7 @@ public int backgroundColor (boolean isPressed) { @Override public int outlineColor (boolean isPressed) { - return outlineColorId != 0 ? Theme.getColor(outlineColorId) : 0; + return colorSet != null ? colorSet.outlineColor(isPressed) : (outlineColorId != 0 ? Theme.getColor(outlineColorId) : 0); } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/Highlight.java b/app/src/main/java/org/thunderdog/challegram/util/text/Highlight.java index 2e04d60bce..532b0c45db 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/Highlight.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/Highlight.java @@ -15,11 +15,15 @@ package org.thunderdog.challegram.util.text; -import android.util.Pair; +import android.text.Spannable; +import android.text.SpannableStringBuilder; import android.util.SparseArray; import androidx.annotation.Nullable; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.util.CustomTypefaceSpan; + import java.util.ArrayList; import java.util.List; @@ -95,6 +99,17 @@ private static int indexOfWeakCodePoint (String in, int startIndex, int endIndex return -1; } + private static boolean isSeparatorCodePoint (int codePoint) { + int codePointType = Character.getType(codePoint); + switch (codePointType) { + case Character.SPACE_SEPARATOR: + case Character.LINE_SEPARATOR: + case Character.CONTROL: + return true; + } + return false; + } + private static boolean isWeakCodePoint (int codePoint) { int codePointType = Character.getType(codePoint); if (Text.isSplitterCodePointType(codePoint, codePointType, true)) { @@ -111,6 +126,7 @@ private static boolean isWeakCodePoint (int codePoint) { case Character.SPACE_SEPARATOR: case Character.LINE_SEPARATOR: case Character.PARAGRAPH_SEPARATOR: + case Character.CONTROL: return true; } return false; @@ -133,15 +149,9 @@ public Highlight (String text, int start, int end, String highlight, int highlig int highlightIndex = 0; while (highlightIndex < highlightLength && matchingLength < (end - index)) { int highlightCodePoint = highlight.codePointAt(highlightStart + highlightIndex); - int highlightCodePointType = Character.getType(highlightCodePoint); - boolean highlightCodePointIsSeparator = - highlightCodePointType == Character.SPACE_SEPARATOR || - highlightCodePointType == Character.LINE_SEPARATOR; + boolean highlightCodePointIsSeparator = isSeparatorCodePoint(highlightCodePoint); int contentCodePoint = text.codePointAt(index + matchingLength); - int contentCodePointType = Character.getType(contentCodePoint); - boolean contentCodePointIsSeparator = - contentCodePointType == Character.SPACE_SEPARATOR || - contentCodePointType == Character.LINE_SEPARATOR; + boolean contentCodePointIsSeparator = isSeparatorCodePoint(contentCodePoint); if (highlightCodePoint == contentCodePoint || (highlightCodePointIsSeparator && contentCodePointIsSeparator) || StringUtils.normalizeCodePoint(highlightCodePoint) == StringUtils.normalizeCodePoint(contentCodePoint)) { // easy path: code points are equal or similar matchingLength += Character.charCount(contentCodePoint); @@ -385,4 +395,16 @@ public void clear () { highlights.clear(); } } + + public static CharSequence toSpannable (String text, String highlight) { + Highlight h = valueOf(text, highlight); + if (h == null) { + return text; + } + SpannableStringBuilder b = new SpannableStringBuilder(text); + for (Highlight.Part part : h.parts) { + b.setSpan(new CustomTypefaceSpan(null, ColorId.textSearchQueryHighlight), part.start, part.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return b; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/Text.java b/app/src/main/java/org/thunderdog/challegram/util/text/Text.java index ade1659e99..421dca42ee 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/Text.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/Text.java @@ -589,6 +589,7 @@ private static boolean acceptEntity (TdApi.TextEntity entity, int flags) { case TdApi.TextEntityTypeMediaTimestamp.CONSTRUCTOR: // TODO case TdApi.TextEntityTypeBold.CONSTRUCTOR: case TdApi.TextEntityTypeCode.CONSTRUCTOR: + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: case TdApi.TextEntityTypeItalic.CONSTRUCTOR: case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: case TdApi.TextEntityTypePre.CONSTRUCTOR: @@ -599,6 +600,9 @@ private static boolean acceptEntity (TdApi.TextEntity entity, int flags) { case TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR: case TdApi.TextEntityTypeSpoiler.CONSTRUCTOR: break; + default: + Td.assertTextEntityType_91234a79(); + throw Td.unsupported(entity.type); } return false; } @@ -641,6 +645,10 @@ public int getEmojiCount () { return builtInEmojiCount; } + public boolean hasBuiltInEmoji () { + return getEmojiCount() > 0; + } + @Override public boolean incrementEmojiCount () { builtInEmojiCount++; @@ -763,7 +771,7 @@ private void reset () { } private interface MediaCreator { - TextMedia onCreateMedia (String keyId, int id); + TextMedia onCreateMedia (String keyId, long id); } private TextMedia newOrExistingMedia (@Nullable String keyId, int start, int end, MediaCreator creator) { @@ -771,7 +779,7 @@ private TextMedia newOrExistingMedia (@Nullable String keyId, int start, int end throw new IllegalStateException(); if (media == null) media = new LinkedHashMap<>(); - final int nextMediaId = media.size(); + final long nextMediaId = media.size(); if (keyId == null) { keyId = "_" + nextMediaId + "x" + (start != end ? start + ".." + end : start); } @@ -2190,7 +2198,7 @@ public boolean invalidateMediaContent (ComplexReceiver textMediaReceiver, @Nulla // Force parent to call requestMedia() instead return false; } - final int displayMediaKey = specificMedia.getDisplayMediaKey(); + final long displayMediaKey = specificMedia.getDisplayMediaKey(); if (displayMediaKey == -1 || media.get(specificMedia.keyId) != specificMedia) { return false; // Don't invalidate what wasn't requested } @@ -2206,15 +2214,15 @@ public int requestMedia (ComplexReceiver textMediaReceiver) { return requestMedia(textMediaReceiver, -1, -1); } - public int requestMedia (ComplexReceiver textMediaReceiver, int keyOffset, int maxKeyCount) { + public int requestMedia (ComplexReceiver textMediaReceiver, long keyOffset, long maxKeyCount) { if (!hasMedia()) return 0; boolean clear = keyOffset == -1 && maxKeyCount == -1; if (clear) { keyOffset = 0; - maxKeyCount = Integer.MAX_VALUE; + maxKeyCount = Long.MAX_VALUE; } - int maxMediaId = -1; + long maxMediaId = -1; int mediaCount = 0; for (Map.Entry entry : media.entrySet()) { TextMedia media = entry.getValue(); @@ -2341,6 +2349,26 @@ private void drawPressHighlight (Canvas c, int startX, int endX, int endXBottomP private int lastStartX, lastEndX, lastEndXBottomPadding, lastStartY; + public void beginDrawBatch (@Nullable ComplexReceiver receiver, int externalBatchId) { + if (receiver != null) { + for (TextPart part : parts) { + part.beginDrawBatch(receiver, externalBatchId); + } + } + } + + public void finishAllDrawBatches (@Nullable ComplexReceiver receiver) { + finishDrawBatch(receiver, 0); + } + + public void finishDrawBatch (@Nullable ComplexReceiver receiver, int externalBatchId) { + if (receiver != null) { + for (TextPart part : parts) { + part.finishDrawBatch(receiver, externalBatchId); + } + } + } + public void draw (Canvas c, int startX, int startY) { draw(c, startX, startY, null, 1f); } @@ -2852,9 +2880,9 @@ public boolean onTouchEvent (View view, MotionEvent e, @Nullable ClickCallback c TextEntity entity = part.getClickableEntity(); boolean done = false; if (clickListener != null) { - done = clickListener.onClick(view, this, part, entity != null ? entity.openParameters(view, this, part) : new TdlibUi.UrlOpenParameters().tooltip(part.newTooltipBuilder(view))); + done = clickListener.onClick(view, this, part, entity != null ? entity.openParameters(view, this, part, false) : new TdlibUi.UrlOpenParameters().tooltip(part.newTooltipBuilder(view))); } else if (entity != null) { - entity.performClick(view, this, part, callback); + entity.performClick(view, this, part, callback, false); done = true; } cancelTouch(); @@ -2915,13 +2943,9 @@ public int getTextColor (@Nullable TextColorSet defaultTheme, @Nullable TextEnti return isClickable ? theme.clickableTextColor(isPressed) : theme.defaultTextColor(); } - public int getEmojiSize () { - return emojiSize; - } - - public int getEmojiStatusColor () { + public long getMediaTextComplexColor () { TextColorSet theme = pickTheme(null, null); - return theme.emojiStatusColor(); + return theme.mediaTextComplexColor(); } @ColorInt diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSet.java b/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSet.java index e62c67b8b7..3633170665 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSet.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSet.java @@ -14,6 +14,7 @@ import androidx.annotation.ColorInt; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Screen; import me.vkryl.core.BitwiseUtils; @@ -25,9 +26,9 @@ public interface TextColorSet { default int iconColor () { return defaultTextColor(); } - @ColorInt - default int emojiStatusColor () { - return defaultTextColor(); + + default long mediaTextComplexColor () { + return Theme.newComplexColor(false, defaultTextColor()); } @ColorInt diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSetOverride.java b/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSetOverride.java index 5f070b3aab..c946f95ee4 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSetOverride.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSetOverride.java @@ -28,6 +28,11 @@ public int defaultTextColor () { return colorSet.defaultTextColor(); } + @Override + public long mediaTextComplexColor () { + return colorSet.mediaTextComplexColor(); + } + @Override public int iconColor () { return colorSet.iconColor(); @@ -57,9 +62,4 @@ public int backgroundColorId (boolean isPressed) { public int outlineColorId (boolean isPressed) { return colorSet.outlineColorId(isPressed); } - - @Override - public int emojiStatusColor () { - return colorSet.emojiStatusColor(); - } } diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSets.java b/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSets.java index 151b18d116..1611c3127f 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSets.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/TextColorSets.java @@ -13,9 +13,12 @@ package org.thunderdog.challegram.util.text; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Screen; +import me.vkryl.core.ColorUtils; + public final class TextColorSets { public static final TextColorSetThemed WHITE = () -> ColorId.white; public static final TextColorSetThemed PLACEHOLDER = () -> ColorId.textPlaceholder; @@ -126,10 +129,16 @@ default int staticBackgroundColorId () { // NORMAL public interface Regular extends TextColorSetThemed { @Override + @PorterDuffColorId default int defaultTextColorId () { return ColorId.text; } + @Override + default long mediaTextComplexColor () { + return Theme.newComplexColor(true, defaultTextColorId()); + } + @Override default int iconColorId () { return ColorId.icon; @@ -214,10 +223,25 @@ public int defaultTextColorId () { return ColorId.messageAuthor; }*/ + @Override + public long mediaTextComplexColor () { + return Theme.newComplexColor(true, ColorId.messageAuthor); + } + @Override public int clickableTextColorId (boolean isPressed) { return ColorId.messageAuthor; } + + @Override + public int backgroundColor (boolean isPressed) { + return isPressed ? ColorUtils.alphaColor(.2f, Theme.getColor(ColorId.messageAuthor)) : ColorId.NONE; + } + + @Override + public int backgroundColorId (boolean isPressed) { + return isPressed ? ColorId.messageAuthor : ColorId.NONE; + } }, MESSAGE_AUTHOR_PSA = new Regular() { @Override @@ -265,6 +289,11 @@ public int defaultTextColorId () { return ColorId.bubbleOut_text; } + @Override + public long mediaTextComplexColor () { + return Theme.newComplexColor(true, ColorId.bubbleOut_messageAuthor); + } + @Override public int clickableTextColorId (boolean isPressed) { return ColorId.bubbleOut_messageAuthor; @@ -323,6 +352,11 @@ public int defaultTextColorId () { return ColorId.bubbleIn_text; } + @Override + public long mediaTextComplexColor () { + return Theme.newComplexColor(true, ColorId.messageAuthor); + } + @Override public int clickableTextColorId (boolean isPressed) { return ColorId.messageAuthor; diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/TextEntity.java b/app/src/main/java/org/thunderdog/challegram/util/text/TextEntity.java index d49c8a7c6f..7c4a64212b 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/TextEntity.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/TextEntity.java @@ -86,10 +86,15 @@ public TextEntity setCustomColorSet (TextColorSet customColorSet) { } @NonNull - public final TdlibUi.UrlOpenParameters openParameters (View view, Text text, TextPart part) { - if (this.openParameters != null && this.openParameters.tooltip != null) + public final TdlibUi.UrlOpenParameters openParameters (View view, Text text, TextPart part, boolean isFromLongPressMenu) { + if (!isFromLongPressMenu && this.openParameters != null && this.openParameters.tooltip != null) return this.openParameters; - TooltipOverlayView.TooltipBuilder b = part.newTooltipBuilder(view); + TooltipOverlayView.TooltipBuilder b; + if (isFromLongPressMenu) { + b = UI.getContext(view.getContext()).tooltipManager().builder(view); + } else { + b = part.newTooltipBuilder(view); + } // TODO highlight the text part & modify color, if needed return new TdlibUi.UrlOpenParameters(this.openParameters).tooltip(b); } @@ -184,7 +189,7 @@ final TextPaint getTextPaint (TextStyleProvider textStyleProvider, boolean force } public abstract boolean isMonospace (); public abstract boolean isSmall (); - public abstract void performClick (View view, Text text, TextPart textPart, @Nullable Text.ClickCallback callback); + public abstract void performClick (View view, Text text, TextPart textPart, @Nullable Text.ClickCallback callback, boolean isFromLongPressMenu); public abstract boolean performLongPress (View view, Text text, TextPart textPart, boolean allowShare, @Nullable Text.ClickCallback callback); protected abstract boolean equals (TextEntity b, int compareMode, String originalText); public abstract boolean isEssential (); diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/TextEntityCustom.java b/app/src/main/java/org/thunderdog/challegram/util/text/TextEntityCustom.java index 397af33c7b..fc487b82c0 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/TextEntityCustom.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/TextEntityCustom.java @@ -299,7 +299,7 @@ public boolean isFullWidth () { } @Override - public void performClick (View view, Text text, TextPart part, @Nullable Text.ClickCallback callback) { + public void performClick (View view, Text text, TextPart part, @Nullable Text.ClickCallback callback, boolean isFromLongPressMenu) { switch (linkType) { case LINK_TYPE_EMAIL: { if (callback == null || !callback.onEmailClick(link)) { @@ -314,7 +314,7 @@ public void performClick (View view, Text text, TextPart part, @Nullable Text.Cl break; } case LINK_TYPE_URL: { - TdlibUi.UrlOpenParameters openParameters = this.openParameters(view, text, part); + TdlibUi.UrlOpenParameters openParameters = this.openParameters(view, text, part, isFromLongPressMenu); if (callback == null || !callback.onUrlClick(view, link, !StringUtils.equalsOrBothEmpty(text.getText(), link), openParameters)) { if (context != null) { context.openLinkAlert(link, modifyUrlOpenParameters(openParameters, callback, link)); @@ -335,7 +335,7 @@ public void performClick (View view, Text text, TextPart part, @Nullable Text.Cl break; } case LINK_TYPE_REFERENCE: { - if (callback == null || !(callback.onReferenceClick(view, link, referenceAnchorName, this.openParameters(view, text, part))) || callback.onAnchorClick(view, link)) { + if (callback == null || !(callback.onReferenceClick(view, link, referenceAnchorName, this.openParameters(view, text, part, isFromLongPressMenu))) || callback.onAnchorClick(view, link)) { // TODO open pop-up with ${referenceText}? } break; @@ -410,7 +410,7 @@ public boolean performLongPress (final View view, final Text text, final TextPar TD.shareLink(new TdlibContext(context.context(), tdlib), copyText); } } else if (id == R.id.btn_openLink) { - performClick(view, text, part, clickCallback); + performClick(view, text, part, clickCallback, true); } return true; }, clickCallback != null ? clickCallback.getForcedTheme(view, text) : null); diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/TextEntityMessage.java b/app/src/main/java/org/thunderdog/challegram/util/text/TextEntityMessage.java index 8e88d1ee58..8673398fd5 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/TextEntityMessage.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/TextEntityMessage.java @@ -81,6 +81,7 @@ private static int addFlags (TdApi.TextEntityType type) { if (isFullWidth(type)) { flags |= FLAG_FULL_WIDTH; } + //noinspection SwitchIntDef switch (type.getConstructor()) { case TdApi.TextEntityTypeBold.CONSTRUCTOR: flags |= FLAG_BOLD; @@ -104,7 +105,7 @@ public TextEntityMessage (@Nullable Tdlib tdlib, String in, TdApi.TextEntity ent public TextEntityMessage (@Nullable Tdlib tdlib, String in, int offset, int end, TdApi.TextEntity entity, @Nullable List parentEntities, @Nullable TdlibUi.UrlOpenParameters openParameters) { this(tdlib, - (entity.type.getConstructor() == TdApi.TextEntityTypeBold.CONSTRUCTOR || hasEntityType(parentEntities, TdApi.TextEntityTypeBold.CONSTRUCTOR)) && Text.needFakeBold(in, offset, end), + (Td.isBold(entity.type) || hasEntityType(parentEntities, TdApi.TextEntityTypeBold.CONSTRUCTOR)) && Text.needFakeBold(in, offset, end), offset, end, entity, parentEntities, openParameters @@ -114,8 +115,8 @@ public TextEntityMessage (@Nullable Tdlib tdlib, String in, int offset, int end, private TextEntityMessage (@Nullable Tdlib tdlib, boolean needFakeBold, int offset, int end, TdApi.TextEntity entity, @Nullable List parentEntities, @Nullable TdlibUi.UrlOpenParameters openParameters) { super(tdlib, offset, end, needFakeBold, openParameters); TdApi.TextEntity clickableEntity = isClickable(entity.type) ? entity : null; - TdApi.TextEntity spoilerEntity = entity.type.getConstructor() == TdApi.TextEntityTypeSpoiler.CONSTRUCTOR ? entity : null; - TdApi.TextEntity emojiEntity = entity.type.getConstructor() == TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR ? entity : null; + TdApi.TextEntity spoilerEntity = Td.isSpoiler(entity.type) ? entity : null; + TdApi.TextEntity emojiEntity = Td.isCustomEmoji(entity.type) ? entity : null; int flags = addFlags(entity.type); if (parentEntities != null) { for (int i = parentEntities.size() - 1; i >= 0; i--) { @@ -123,7 +124,7 @@ private TextEntityMessage (@Nullable Tdlib tdlib, boolean needFakeBold, int offs flags |= addFlags(parentEntity.type); if (clickableEntity == null && isClickable(parentEntity.type)) { clickableEntity = parentEntity; - } else if (spoilerEntity == null && parentEntity.type.getConstructor() == TdApi.TextEntityTypeSpoiler.CONSTRUCTOR) { + } else if (spoilerEntity == null && Td.isSpoiler(parentEntity.type)) { spoilerEntity = parentEntity; } } @@ -249,7 +250,8 @@ public static boolean isClickable (TdApi.TextEntityType type) { case TdApi.TextEntityTypeCode.CONSTRUCTOR: case TdApi.TextEntityTypePreCode.CONSTRUCTOR: - case TdApi.TextEntityTypePre.CONSTRUCTOR: { + case TdApi.TextEntityTypePre.CONSTRUCTOR: + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: { return true; } case TdApi.TextEntityTypeMediaTimestamp.CONSTRUCTOR: // TODO @@ -262,11 +264,14 @@ public static boolean isClickable (TdApi.TextEntityType type) { case TdApi.TextEntityTypeUnderline.CONSTRUCTOR: { return false; } + default: + Td.assertTextEntityType_91234a79(); + throw Td.unsupported(type); } - return false; } private static boolean isEssential (TdApi.TextEntityType type) { + //noinspection SwitchIntDef switch (type.getConstructor()) { // case TdApi.TextEntityTypeBotCommand.CONSTRUCTOR: // case TdApi.TextEntityTypeHashtag.CONSTRUCTOR: @@ -283,6 +288,7 @@ public boolean isFullWidth () { } private static boolean isMonospace (TdApi.TextEntityType type) { + //noinspection SwitchIntDef switch (type.getConstructor()) { case TdApi.TextEntityTypeCode.CONSTRUCTOR: case TdApi.TextEntityTypePre.CONSTRUCTOR: @@ -293,9 +299,11 @@ private static boolean isMonospace (TdApi.TextEntityType type) { } private static boolean isFullWidth (TdApi.TextEntityType type) { + //noinspection SwitchIntDef switch (type.getConstructor()) { case TdApi.TextEntityTypePre.CONSTRUCTOR: case TdApi.TextEntityTypePreCode.CONSTRUCTOR: + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: return true; } return false; @@ -392,7 +400,7 @@ public boolean hasAnchor (String anchor) { } @Override - public void performClick (View view, Text text, TextPart part, @Nullable Text.ClickCallback callback) { + public void performClick (View view, Text text, TextPart part, @Nullable Text.ClickCallback callback, boolean isFromLongPressMenu) { final ViewController context = findRoot(view); if (context == null) { Log.v("performClick ignored, because ancestor not found"); @@ -406,7 +414,7 @@ public void performClick (View view, Text text, TextPart part, @Nullable Text.Cl case TdApi.TextEntityTypeUrl.CONSTRUCTOR: { String link = Td.substring(text.getText(), clickableEntity); - TdlibUi.UrlOpenParameters openParameters = this.openParameters(view, text, part); + TdlibUi.UrlOpenParameters openParameters = this.openParameters(view, text, part, isFromLongPressMenu); if (callback == null || !callback.onUrlClick(view, link, false, openParameters)) { if (tdlib != null) { tdlib.ui().openUrl(context, link, modifyUrlOpenParameters(openParameters, callback, link)); @@ -416,7 +424,7 @@ public void performClick (View view, Text text, TextPart part, @Nullable Text.Cl } case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: { String link = ((TdApi.TextEntityTypeTextUrl) clickableEntity.type).url; - TdlibUi.UrlOpenParameters openParameters = this.openParameters(view, text, part); + TdlibUi.UrlOpenParameters openParameters = this.openParameters(view, text, part, isFromLongPressMenu); if (callback == null || !callback.onUrlClick(view, link, true, openParameters)) { context.openLinkAlert(link, modifyUrlOpenParameters(openParameters, callback, link)); } @@ -433,7 +441,7 @@ public void performClick (View view, Text text, TextPart part, @Nullable Text.Cl String username = Td.substring(text.getText(), clickableEntity); if (callback == null || !callback.onUsernameClick(username)) { if (tdlib != null) { - tdlib.ui().openPublicChat(context, username, this.openParameters(view, text, part)); + tdlib.ui().openPublicChat(context, username, this.openParameters(view, text, part, isFromLongPressMenu)); } } break; @@ -442,7 +450,7 @@ public void performClick (View view, Text text, TextPart part, @Nullable Text.Cl TdApi.TextEntityTypeMentionName mentionEntity = (TdApi.TextEntityTypeMentionName) clickableEntity.type; if (callback == null || !callback.onUserClick(mentionEntity.userId)) { if (tdlib != null) { - tdlib.ui().openPrivateProfile(context, mentionEntity.userId, this.openParameters(view, text, part)); + tdlib.ui().openPrivateProfile(context, mentionEntity.userId, this.openParameters(view, text, part, isFromLongPressMenu)); } } break; @@ -510,11 +518,15 @@ public void performClick (View view, Text text, TextPart part, @Nullable Text.Cl case TdApi.TextEntityTypeMediaTimestamp.CONSTRUCTOR: case TdApi.TextEntityTypePre.CONSTRUCTOR: case TdApi.TextEntityTypePreCode.CONSTRUCTOR: + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: case TdApi.TextEntityTypeSpoiler.CONSTRUCTOR: case TdApi.TextEntityTypeStrikethrough.CONSTRUCTOR: case TdApi.TextEntityTypeUnderline.CONSTRUCTOR: // Non-clickable break; + default: + Td.assertTextEntityType_91234a79(); + throw Td.unsupported(clickableEntity.type); } } @@ -529,19 +541,19 @@ public boolean performLongPress (final View view, final Text text, final TextPar return false; } - if (clickableEntity.type.getConstructor() == TdApi.TextEntityTypeBotCommand.CONSTRUCTOR) { + if (Td.isBotCommand(clickableEntity.type)) { String command = Td.substring(text.getText(), clickableEntity); return clickCallback != null && clickCallback.onCommandClick(view, text, part, command, true); } final String copyText; - if (clickableEntity.type.getConstructor() == TdApi.TextEntityTypeTextUrl.CONSTRUCTOR) { + if (Td.isTextUrl(clickableEntity.type)) { copyText = ((TdApi.TextEntityTypeTextUrl) clickableEntity.type).url; } else { copyText = Td.substring(text.getText(), clickableEntity); } - final boolean canShare = clickableEntity.type.getConstructor() == TdApi.TextEntityTypeUrl.CONSTRUCTOR || clickableEntity.type.getConstructor() == TdApi.TextEntityTypeTextUrl.CONSTRUCTOR; + final boolean canShare = Td.isUrl(clickableEntity.type) || Td.isTextUrl(clickableEntity.type); final int size = canShare ? 3 : 2; IntList ids = new IntList(size); StringList strings = new StringList(size); @@ -558,7 +570,7 @@ public boolean performLongPress (final View view, final Text text, final TextPar case TdApi.TextEntityTypeMention.CONSTRUCTOR: case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: { ids.append(R.id.btn_openLink); - strings.append(clickableEntity.type.getConstructor() == TdApi.TextEntityTypeBankCardNumber.CONSTRUCTOR ? R.string.OpenInExternalApp : R.string.Open); + strings.append(Td.isBankCardNumber(clickableEntity.type) ? R.string.OpenInExternalApp : R.string.Open); icons.append(R.drawable.baseline_open_in_browser_24); break; } @@ -566,7 +578,8 @@ public boolean performLongPress (final View view, final Text text, final TextPar case TdApi.TextEntityTypePhoneNumber.CONSTRUCTOR: case TdApi.TextEntityTypePreCode.CONSTRUCTOR: case TdApi.TextEntityTypeCode.CONSTRUCTOR: - case TdApi.TextEntityTypePre.CONSTRUCTOR: { + case TdApi.TextEntityTypePre.CONSTRUCTOR: + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: { break; } case TdApi.TextEntityTypeBotCommand.CONSTRUCTOR: // Unreachable because of the condition above @@ -577,26 +590,30 @@ public boolean performLongPress (final View view, final Text text, final TextPar case TdApi.TextEntityTypeItalic.CONSTRUCTOR: case TdApi.TextEntityTypeSpoiler.CONSTRUCTOR: case TdApi.TextEntityTypeStrikethrough.CONSTRUCTOR: - case TdApi.TextEntityTypeUnderline.CONSTRUCTOR: - default: { + case TdApi.TextEntityTypeUnderline.CONSTRUCTOR: { Log.i("Long press is unsupported for entity: %s", clickableEntity); return false; } + + default: { + Td.assertTextEntityType_91234a79(); + throw Td.unsupported(clickableEntity.type); + } } - if (clickableEntity.type.getConstructor() != TdApi.TextEntityTypeMentionName.CONSTRUCTOR) { + if (!Td.isMentionName(clickableEntity.type)) { ids.append(R.id.btn_copyText); - strings.append(clickableEntity.type.getConstructor() == TdApi.TextEntityTypeMention.CONSTRUCTOR ? R.string.CopyUsername : R.string.Copy); + strings.append(Td.isMention(clickableEntity.type) ? R.string.CopyUsername : R.string.Copy); icons.append(R.drawable.baseline_content_copy_24); } final String copyLink; - if (clickableEntity.type.getConstructor() == TdApi.TextEntityTypeMention.CONSTRUCTOR && copyText != null) { + if (Td.isMention(clickableEntity.type) && copyText != null) { ids.append(R.id.btn_copyLink); strings.append(R.string.CopyLink); icons.append(R.drawable.baseline_link_24); - copyLink = TD.getLink(copyText.substring(1)); + copyLink = tdlib.tMeUrl(copyText.substring(1)); } else { copyLink = null; } @@ -625,6 +642,7 @@ public boolean performLongPress (final View view, final Text text, final TextPar case TdApi.TextEntityTypeCashtag.CONSTRUCTOR: message = R.string.CopiedCashtag; break; + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: case TdApi.TextEntityTypePreCode.CONSTRUCTOR: case TdApi.TextEntityTypeCode.CONSTRUCTOR: case TdApi.TextEntityTypePre.CONSTRUCTOR: { @@ -632,6 +650,7 @@ public boolean performLongPress (final View view, final Text text, final TextPar break; } default: { + Td.assertTextEntityType_91234a79(); message = R.string.CopiedLink; break; } @@ -643,7 +662,7 @@ public boolean performLongPress (final View view, final Text text, final TextPar TD.shareLink(new TdlibContext(context.context(), tdlib), copyText); } } else if (id == R.id.btn_openLink) { - performClick(itemView, text, part, clickCallback); + performClick(itemView, text, part, clickCallback, true); } return true; }, clickCallback != null ? clickCallback.getForcedTheme(view, text) : null); diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/TextMedia.java b/app/src/main/java/org/thunderdog/challegram/util/text/TextMedia.java index 90f5ff9cc3..6454dcc0d1 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/TextMedia.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/TextMedia.java @@ -32,6 +32,7 @@ import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibEmojiManager; import org.thunderdog.challegram.telegram.TdlibThread; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; @@ -48,11 +49,11 @@ public class TextMedia implements Destroyable, TdlibEmojiManager.Watcher { private final Text source; final List attachedToParts = new ArrayList<>(); - private int displayMediaKeyOffset = -1; + private long displayMediaKeyOffset = -1; private final Tdlib tdlib; public final String keyId; - public final int id; + public final long id; private final int width, height; private boolean isDestroyed; @@ -65,7 +66,7 @@ public class TextMedia implements Destroyable, TdlibEmojiManager.Watcher { private ImageFile imageFile; private GifFile gifFile; - public TextMedia (Text source, Tdlib tdlib, String keyId, int id, int size, long customEmojiId) { + public TextMedia (Text source, Tdlib tdlib, String keyId, long id, int size, long customEmojiId) { if (tdlib == null) throw new IllegalArgumentException(); this.source = source; @@ -81,7 +82,7 @@ public TextMedia (Text source, Tdlib tdlib, String keyId, int id, int size, long } } - public TextMedia (Text source, Tdlib tdlib, String keyId, int id, TdApi.RichTextIcon icon) { + public TextMedia (Text source, Tdlib tdlib, String keyId, long id, TdApi.RichTextIcon icon) { if (tdlib == null) throw new IllegalArgumentException(); this.source = source; @@ -231,7 +232,7 @@ public static float getScale (TdApi.Sticker sticker, int size) { } public boolean needsRepainting () { - return isCustomEmoji() && customEmoji != null && TD.needRepainting(customEmoji.value); + return isCustomEmoji() && customEmoji != null && TD.needThemedColorFilter(customEmoji.value); } public boolean isCustomEmoji () { @@ -249,11 +250,11 @@ public void performDestroy () { } } - void setDisplayMediaKeyOffset (int keyOffset) { + void setDisplayMediaKeyOffset (long keyOffset) { this.displayMediaKeyOffset = keyOffset; } - int getDisplayMediaKey () { + long getDisplayMediaKey () { if (displayMediaKeyOffset != -1) { return displayMediaKeyOffset + id; } @@ -261,7 +262,7 @@ int getDisplayMediaKey () { } public void requestFiles (ComplexReceiver receiver) { - int displayMediaKey = getDisplayMediaKey(); + long displayMediaKey = getDisplayMediaKey(); if (displayMediaKey == -1) throw new IllegalStateException(); if (isCustomEmoji() && customEmoji == null && !customEmojiRequested) { @@ -277,7 +278,7 @@ public void requestFiles (ComplexReceiver receiver) { } } - public void draw (Canvas c, ComplexReceiver receiver, int left, int top, int right, int bottom, float alpha, int displayMediaKey) { + public void draw (Canvas c, ComplexReceiver receiver, int left, int top, int right, int bottom, float alpha, long displayMediaKey) { if (isCustomEmoji() && customEmoji == null) { if (BuildConfig.DEBUG) { c.drawCircle(left + (right - left) / 2f, top + (bottom - top) / 2f, height / 2f, Paints.fillingPaint(ColorUtils.alphaColor(alpha, 0xffff0000))); @@ -286,9 +287,6 @@ public void draw (Canvas c, ComplexReceiver receiver, int left, int top, int rig } final boolean needRepainting = needsRepainting(); - if (needRepainting) { - c.saveLayerAlpha(left - width / 4f, top - height / 4f, right + width / 4f, bottom + height / 4f, 255, Canvas.ALL_SAVE_FLAG); - } //noinspection ConstantConditions float scale = customEmoji != null && !customEmoji.isNotFound() ? getScale(customEmoji.value, (right - left)) : 1f; @@ -316,6 +314,12 @@ public void draw (Canvas c, ComplexReceiver receiver, int left, int top, int rig } DoubleImageReceiver preview = content == null || content.needPlaceholder() ? receiver.getPreviewReceiver(displayMediaKey) : null; if (preview != null) { + if (needRepainting) { + long complexColor = source.getMediaTextComplexColor(); + Theme.applyComplexColor(preview, complexColor); + } else { + preview.disablePorterDuffColorFilter(); + } preview.setBounds(left, top, right, bottom); preview.setPaintAlpha(alpha); if (outline != null && preview.needPlaceholder()) { @@ -325,6 +329,12 @@ public void draw (Canvas c, ComplexReceiver receiver, int left, int top, int rig preview.restorePaintAlpha(); } if (content != null) { + if (needRepainting) { + long complexColor = source.getMediaTextComplexColor(); + Theme.applyComplexColor(content, complexColor); + } else { + content.disablePorterDuffColorFilter(); + } if (preview == null && outline != null && content.needPlaceholder()) { content.drawPlaceholderContour(c, outline, alpha); } @@ -338,9 +348,5 @@ public void draw (Canvas c, ComplexReceiver receiver, int left, int top, int rig if (needScaleUp) { Views.restore(c, restoreToCount); } - if (needRepainting) { - c.drawRect(left - width / 4f, top - height / 4f, right + width / 4f, bottom + height / 4f, Paints.getSrcInPaint(source.getEmojiStatusColor())); - c.restore(); - } } } diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/TextPart.java b/app/src/main/java/org/thunderdog/challegram/util/text/TextPart.java index 49065025b4..a3862970d4 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/TextPart.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/TextPart.java @@ -29,6 +29,7 @@ import org.thunderdog.challegram.emoji.Emoji; import org.thunderdog.challegram.emoji.EmojiInfo; import org.thunderdog.challegram.loader.ComplexReceiver; +import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.navigation.TooltipOverlayView; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; @@ -355,6 +356,43 @@ private void drawEmoji (Canvas c, final int x, final int y, TextPaint textPaint, Emoji.instance().draw(c, emojiInfo, rect, (int) (alpha * textPaint.getAlpha())); } + private static final int DRAW_BATCH_LEVEL_LOCAL = 1; + + public @Nullable GifReceiver findTargetReceiver (@Nullable ComplexReceiver receiver) { + if (receiver != null && media != null && !media.isNotFoundCustomEmoji() && media.isAnimated()) { + final long displayMediaKey = media.getDisplayMediaKey(); + return receiver.getGifReceiver(displayMediaKey); + } + return null; + } + + public void beginDrawBatch (@Nullable ComplexReceiver receiver, int externalBatchId) { + if (externalBatchId <= 0) + throw new IllegalArgumentException(Integer.toString(externalBatchId)); + GifReceiver gifReceiver = findTargetReceiver(receiver); + if (gifReceiver != null) { + gifReceiver.beginDrawBatch(DRAW_BATCH_LEVEL_LOCAL + externalBatchId); + } + } + + public void finishAllDrawBatches (@Nullable ComplexReceiver receiver) { + finishDrawBatch(receiver, 0); + } + + public void finishDrawBatch (@Nullable ComplexReceiver receiver, int externalBatchId) { + if (externalBatchId < 0) { + throw new IllegalArgumentException(Integer.toString(externalBatchId)); + } + GifReceiver gifReceiver = findTargetReceiver(receiver); + if (gifReceiver != null) { + if (externalBatchId == 0) { + gifReceiver.finishAllDrawBatches(); + } else { + gifReceiver.finishDrawBatch(DRAW_BATCH_LEVEL_LOCAL + externalBatchId); + } + } + } + public void draw (int partIndex, Canvas c, int startX, int endX, int endXBottomPadding, int startY, float alpha, @Nullable TextColorSet colorProvider, @Nullable ComplexReceiver receiver) { final int y = startY + this.y; final int x = makeX(startX, endX, endXBottomPadding); @@ -369,7 +407,7 @@ public void draw (int partIndex, Canvas c, int startX, int endX, int endXBottomP } return; } - final int displayMediaKey = media.getDisplayMediaKey(); + final long displayMediaKey = media.getDisplayMediaKey(); final int iconY = y + textPaint.baselineShift - (isCustomEmoji() ? Screen.dp(1.5f) : 0); final int height = this.height == -1 ? (int) width : this.height; if (receiver != null && displayMediaKey != -1) { @@ -384,7 +422,7 @@ public void draw (int partIndex, Canvas c, int startX, int endX, int endXBottomP restoreToCount = Views.save(c); c.translate(x, iconY); if (isFirst && media.isAnimated()) { - receiver.getGifReceiver(displayMediaKey).beginDrawBatch(); + receiver.getGifReceiver(displayMediaKey).beginDrawBatch(DRAW_BATCH_LEVEL_LOCAL); } } else { left = x; @@ -396,7 +434,7 @@ public void draw (int partIndex, Canvas c, int startX, int endX, int endXBottomP media.draw(c, receiver, left, top, right, bottom, alpha, displayMediaKey); if (needTranslate) { if (isLast && media.isAnimated()) { - receiver.getGifReceiver(displayMediaKey).finishDrawBatch(); + receiver.getGifReceiver(displayMediaKey).finishDrawBatch(DRAW_BATCH_LEVEL_LOCAL); } Views.restore(c, restoreToCount); } diff --git a/app/src/main/java/org/thunderdog/challegram/util/text/TextWrapper.java b/app/src/main/java/org/thunderdog/challegram/util/text/TextWrapper.java index a2bafbd9b8..d4164f966b 100644 --- a/app/src/main/java/org/thunderdog/challegram/util/text/TextWrapper.java +++ b/app/src/main/java/org/thunderdog/challegram/util/text/TextWrapper.java @@ -179,6 +179,14 @@ public boolean hasMedia () { return false; } + public boolean hasBuiltInEmoji () { + for (Text text : texts) { + if (text != null && text.hasBuiltInEmoji()) + return true; + } + return false; + } + public int getMaxMediaCount () { int count = 0; for (Text text : texts) { @@ -197,7 +205,7 @@ public void requestMedia (ComplexReceiver receiver) { requestMedia(receiver, -1, -1); } - public void requestMedia (ComplexReceiver receiver, int startKey, int maxMediaCount) { + public void requestMedia (ComplexReceiver receiver, long startKey, long maxMediaCount) { Text text = getCurrent(); if (text != null) { text.requestMedia(receiver, startKey, maxMediaCount); @@ -231,6 +239,7 @@ private Text getInternal (int index, int maxWidth) { Text.Builder b = new Text.Builder(this.text, maxWidth, textStyleProvider, colorTheme) .maxLineCount(maxLines) .entities(entities, this) + .viewProvider(viewProvider) .highlight(highlightText) .lineWidthProvider(lineWidthProvider) .textFlags(BitwiseUtils.setFlag(textFlags, Text.FLAG_BIG_EMOJI, false)); @@ -349,6 +358,28 @@ public void detachFromView (View view) { } } + public final void beginDrawBatch (@Nullable ComplexReceiver receiver, int externalBatchId) { + if (receiver != null) { + final Text text = getCurrent(); + if (text != null) { + text.beginDrawBatch(receiver, externalBatchId); + } + } + } + + public final void finishAllDrawBatches (@Nullable ComplexReceiver receiver) { + finishDrawBatch(receiver, 0); + } + + public final void finishDrawBatch (@Nullable ComplexReceiver receiver, int externalBatchId) { + if (receiver != null) { + final Text text = getCurrent(); + if (text != null) { + text.finishDrawBatch(receiver, externalBatchId); + } + } + } + public final void draw (Canvas c, int startX, int startY) { draw(c, startX, startY, null, 1f); } diff --git a/app/src/main/java/org/thunderdog/challegram/v/ChatsRecyclerView.java b/app/src/main/java/org/thunderdog/challegram/v/ChatsRecyclerView.java index 34660727fa..b029f45f02 100644 --- a/app/src/main/java/org/thunderdog/challegram/v/ChatsRecyclerView.java +++ b/app/src/main/java/org/thunderdog/challegram/v/ChatsRecyclerView.java @@ -191,6 +191,10 @@ public void updateChatHasScheduledMessages (long chatId, boolean hasScheduledMes } } + public void updateRelativeDate () { + adapter.updateRelativeDate(); + invalidateAll(); + } public void updateMessageSendSucceeded (TdApi.Message message, long oldMessageId) { int updated = adapter.updateMessageSendSucceeded(message, oldMessageId); if (updated != -1) { diff --git a/app/src/main/java/org/thunderdog/challegram/v/CustomRecyclerView.java b/app/src/main/java/org/thunderdog/challegram/v/CustomRecyclerView.java index 52e3e63e92..71ba1bc47c 100644 --- a/app/src/main/java/org/thunderdog/challegram/v/CustomRecyclerView.java +++ b/app/src/main/java/org/thunderdog/challegram/v/CustomRecyclerView.java @@ -167,6 +167,10 @@ public void setScrollDisabled (boolean isDisabled) { this.interceptEvents = true; } + public boolean isScrollDisabled () { + return scrollDisabled; + } + public void setScrollDisabled (boolean isDisabled, boolean intercept) { this.scrollDisabled = isDisabled; this.interceptEvents = intercept; diff --git a/app/src/main/java/org/thunderdog/challegram/v/RtlGridLayoutManager.java b/app/src/main/java/org/thunderdog/challegram/v/RtlGridLayoutManager.java index 73e033bb80..ed56c94310 100644 --- a/app/src/main/java/org/thunderdog/challegram/v/RtlGridLayoutManager.java +++ b/app/src/main/java/org/thunderdog/challegram/v/RtlGridLayoutManager.java @@ -45,4 +45,25 @@ public RtlGridLayoutManager setAlignOnly (boolean alignOnly) { protected final boolean isLayoutRTL () { return !alignOnly && Lang.rtl(); } + + private boolean isCanScrollVertically = true; + private boolean isCanScrollHorizontally = true; + + public void setCanScrollVertically (boolean canScrollVertically) { + isCanScrollVertically = canScrollVertically; + } + + public void setCanScrollHorizontally (boolean canScrollHorizontally) { + isCanScrollHorizontally = canScrollHorizontally; + } + + @Override + public boolean canScrollVertically () { + return isCanScrollVertically && super.canScrollVertically(); + } + + @Override + public boolean canScrollHorizontally () { + return isCanScrollHorizontally && super.canScrollHorizontally(); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/voip/TgCallsController.java b/app/src/main/java/org/thunderdog/challegram/voip/TgCallsController.java index 987b2fcbea..f762a00d51 100644 --- a/app/src/main/java/org/thunderdog/challegram/voip/TgCallsController.java +++ b/app/src/main/java/org/thunderdog/challegram/voip/TgCallsController.java @@ -18,6 +18,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.voip.annotation.AudioState; import org.thunderdog.challegram.voip.annotation.CallNetworkType; import org.thunderdog.challegram.voip.annotation.VideoState; @@ -26,8 +28,8 @@ public class TgCallsController extends VoIPInstance { private final String version; private long nativePtr; - public TgCallsController (@NonNull CallConfiguration configuration, @NonNull CallOptions options, @NonNull ConnectionStateListener stateListener, String version) { - super(configuration, options, stateListener); + public TgCallsController (@NonNull Tdlib tdlib, @NonNull TdApi.Call call, @NonNull CallConfiguration configuration, @NonNull CallOptions options, @NonNull ConnectionStateListener stateListener, String version) { + super(tdlib, call, configuration, options, stateListener); if (configuration.state.encryptionKey.length != 256) throw new IllegalArgumentException(Integer.toString(configuration.state.encryptionKey.length)); this.version = version; diff --git a/app/src/main/java/org/thunderdog/challegram/voip/VoIP.java b/app/src/main/java/org/thunderdog/challegram/voip/VoIP.java index f7474a7d40..23d94b1b82 100644 --- a/app/src/main/java/org/thunderdog/challegram/voip/VoIP.java +++ b/app/src/main/java/org/thunderdog/challegram/voip/VoIP.java @@ -232,6 +232,8 @@ public static VoIPInstance instantiateAndConnect ( } if (version.equals(libtgvoipVersion) && (Config.FORCE_DIRECT_TGVOIP || !ArrayUtils.contains(tgCallsVersions, version) || isForceDisabled(version))) { tgcalls = new VoIPController( + tdlib, + call, configuration, options, connectionStateListener @@ -239,6 +241,8 @@ public static VoIPInstance instantiateAndConnect ( } else if (ArrayUtils.contains(tgCallsVersions, version)) { try { tgcalls = new TgCallsController( + tdlib, + call, configuration, options, connectionStateListener, diff --git a/app/src/main/java/org/thunderdog/challegram/voip/VoIPController.java b/app/src/main/java/org/thunderdog/challegram/voip/VoIPController.java index 2782f144c8..bc8f83ff3b 100755 --- a/app/src/main/java/org/thunderdog/challegram/voip/VoIPController.java +++ b/app/src/main/java/org/thunderdog/challegram/voip/VoIPController.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.voip.annotation.CallNetworkType; import java.util.ArrayList; @@ -21,11 +22,13 @@ public final class VoIPController extends VoIPInstance { private long nativeInst; public VoIPController ( + @NonNull Tdlib tdlib, + @NonNull TdApi.Call call, @NonNull CallConfiguration configuration, @NonNull CallOptions options, @NonNull ConnectionStateListener stateListener ) { - super(configuration, options, stateListener); + super(tdlib, call, configuration, options, stateListener); nativeInst = nativeInit(configuration.persistentStateFilePath); } diff --git a/app/src/main/java/org/thunderdog/challegram/voip/VoIPInstance.java b/app/src/main/java/org/thunderdog/challegram/voip/VoIPInstance.java index 8b1fb9fae0..a7102ce937 100644 --- a/app/src/main/java/org/thunderdog/challegram/voip/VoIPInstance.java +++ b/app/src/main/java/org/thunderdog/challegram/voip/VoIPInstance.java @@ -19,19 +19,27 @@ import androidx.annotation.Keep; import androidx.annotation.NonNull; +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.voip.annotation.CallNetworkType; import org.thunderdog.challegram.voip.annotation.CallState; import me.vkryl.core.lambda.Destroyable; public abstract class VoIPInstance implements Destroyable { + protected final Tdlib tdlib; + protected final TdApi.Call call; protected final CallConfiguration configuration; protected final CallOptions options; protected final @NonNull ConnectionStateListener connectionStateListener; - public VoIPInstance (@NonNull CallConfiguration configuration, + public VoIPInstance (@NonNull Tdlib tdlib, + @NonNull TdApi.Call call, + @NonNull CallConfiguration configuration, @NonNull CallOptions options, @NonNull ConnectionStateListener stateListener) { + this.tdlib = tdlib; + this.call = call; this.configuration = configuration; this.options = options; this.connectionStateListener = stateListener; @@ -41,6 +49,14 @@ public VoIPInstance (@NonNull CallConfiguration configuration, // Getters + public final Tdlib tdlib () { + return tdlib; + } + + public final @NonNull TdApi.Call getCall () { + return call; + } + public final @NonNull CallConfiguration getConfiguration () { return configuration; } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/AvatarView.java b/app/src/main/java/org/thunderdog/challegram/widget/AvatarView.java index 25960cba3b..84c5df42e6 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/AvatarView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/AvatarView.java @@ -20,6 +20,7 @@ import android.view.MotionEvent; import android.view.View; +import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import org.drinkless.tdlib.TdApi; @@ -368,13 +369,13 @@ private boolean needRounds () { return (flags & FLAG_NO_ROUND) == 0; } - private void drawPlaceholder (Canvas c, @ColorId int colorId) { + private void drawPlaceholder (Canvas c, @ColorInt int color) { if (needRounds()) { c.drawCircle(receiver.centerX(), receiver.centerY(), receiver.getRadius(), Paints.fillingPaint( - Theme.getColor(colorId) + color )); } else { - c.drawRect(receiver.getLeft(), receiver.getTop(), receiver.getRight(), receiver.getBottom(), Paints.fillingPaint(Theme.placeholderColor())); + c.drawRect(receiver.getLeft(), receiver.getTop(), receiver.getRight(), receiver.getBottom(), Paints.fillingPaint(color)); } } @@ -383,7 +384,7 @@ protected void onDraw (Canvas c) { if (account != null || getUserId() != 0 || getChatId() != 0) { if (hasPhoto) { if (receiver.needPlaceholder() && (preview == null || preview.needPlaceholder())) { - drawPlaceholder(c, ColorId.placeholder); + drawPlaceholder(c, Theme.placeholderColor()); } if (preview != null && receiver.needPlaceholder()) { preview.draw(c); @@ -393,20 +394,20 @@ protected void onDraw (Canvas c) { if ((flags & FLAG_NEED_OVERLAY) == 0) { if (avatarPlaceholderMetadata != null) { if (avatarPlaceholder == null) - avatarPlaceholder = new AvatarPlaceholder(Screen.px(receiver.getWidth() / 2), avatarPlaceholderMetadata, null); + avatarPlaceholder = new AvatarPlaceholder(Screen.px(receiver.getWidth() / 2f), avatarPlaceholderMetadata, null); avatarPlaceholder.draw(c, receiver.centerX(), receiver.centerY()); } } else { - drawPlaceholder(c, avatarPlaceholderMetadata != null ? avatarPlaceholderMetadata.colorId : ColorId.placeholder); + drawPlaceholder(c, avatarPlaceholderMetadata != null ? avatarPlaceholderMetadata.accentColor.getPrimaryColor() : Theme.placeholderColor()); } } } if ((flags & FLAG_NEED_OVERLAY) != 0) { if (hasPhoto) { - drawPlaceholder(c, ColorId.statusBar); + drawPlaceholder(c, Theme.getColor(ColorId.statusBar)); } if (overlayIcon != null) - Drawables.draw(c, overlayIcon, receiver.centerX() - overlayIcon.getMinimumWidth() / 2, receiver.centerY() - overlayIcon.getMinimumHeight() / 2, Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, overlayIcon, receiver.centerX() - overlayIcon.getMinimumWidth() / 2f, receiver.centerY() - overlayIcon.getMinimumHeight() / 2f, Paints.whitePorterDuffPaint()); } } } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/BaseView.java b/app/src/main/java/org/thunderdog/challegram/widget/BaseView.java index c27c3f0ea6..03f1169d90 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/BaseView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/BaseView.java @@ -347,6 +347,7 @@ public boolean onLongPressRequestedAt (View view, float x, float y) { if (customControllerProvider != null) { ViewController controller = customControllerProvider.createForceTouchPreview(this, x, y); if (controller != null) { + controller.setInForceTouchMode(true); if (controller.needAsynchronousAnimation()) { openPreviewAsync(controller, x, y); } else { @@ -439,6 +440,10 @@ public final void setPreviewChatId (TdApi.ChatList chatList, long chatId, long c this.allowMaximizePreview = allowMaximize; } + public void setAllowMaximizePreview (boolean allowMaximizePreview) { + this.allowMaximizePreview = allowMaximizePreview; + } + public final TdApi.ChatList getPreviewChatList () { return chatList; } @@ -468,6 +473,7 @@ private void openChatPreviewAsync (TdApi.ChatList chatList, TdApi.Chat chat, @Nu } cancelAsyncPreview(); final MessagesController controller = new MessagesController(getContext(), tdlib); + controller.setInForceTouchMode(true); controller.setArguments(createChatPreviewArguments(chatList, chat, messageThread, filter)); openPreviewAsync(controller, x, y); } @@ -623,10 +629,12 @@ public void onAfterForceTouchAction (ForceTouchView.ForceTouchContext context, i } private void closePreview () { - if (currentOpenPreview != null) { - UI.getContext(getContext()).closeForceTouch(); - currentOpenPreview = null; - } + UI.post(() -> { + if (currentOpenPreview != null) { + UI.getContext(getContext()).closeForceTouch(); + currentOpenPreview = null; + } + }); } // Utils diff --git a/app/src/main/java/org/thunderdog/challegram/widget/BetterChatView.java b/app/src/main/java/org/thunderdog/challegram/widget/BetterChatView.java index 7f71ab4699..79648cb0c0 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/BetterChatView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/BetterChatView.java @@ -47,8 +47,11 @@ import org.thunderdog.challegram.telegram.NotificationSettingsListener; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibCache; +import org.thunderdog.challegram.telegram.TdlibMessageViewer; +import org.thunderdog.challegram.telegram.TdlibUi; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.DrawAlgorithms; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Icons; import org.thunderdog.challegram.tool.Paints; @@ -74,17 +77,19 @@ import me.vkryl.td.MessageId; import me.vkryl.td.Td; -public class BetterChatView extends BaseView implements Destroyable, RemoveHelper.RemoveDelegate, ChatListener, TdlibCache.UserDataChangeListener, TdlibCache.SupergroupDataChangeListener, TdlibCache.BasicGroupDataChangeListener, NotificationSettingsListener, TdlibCache.UserStatusChangeListener, DrawableProvider, TooltipOverlayView.LocationProvider { +public class BetterChatView extends BaseView implements Destroyable, RemoveHelper.RemoveDelegate, ChatListener, TdlibCache.UserDataChangeListener, TdlibCache.SupergroupDataChangeListener, TdlibCache.BasicGroupDataChangeListener, NotificationSettingsListener, TdlibCache.UserStatusChangeListener, DrawableProvider, TooltipOverlayView.LocationProvider, TdlibUi.MessageProvider { private static final int FLAG_FAKE_TITLE = 1; private static final int FLAG_SECRET = 1 << 1; private static final int FLAG_ONLINE = 1 << 2; private static final int FLAG_SELF_CHAT = 1 << 3; + private static final int FLAG_NO_SUBTITLE = 1 << 4; private int flags; private final AvatarReceiver avatarReceiver; private final ComplexReceiver subtitleMediaReceiver; private final EmojiStatusHelper emojiStatusHelper; + private @Nullable SimplestCheckBoxHelper checkBoxHelper; private FormattedText title; private Highlight titleHighlight; @@ -139,6 +144,15 @@ public void performDestroy () { setMessageImpl(null); } + public void setIsChecked (boolean isChecked, boolean animated) { + if (isChecked != (checkBoxHelper != null && checkBoxHelper.isChecked())) { + if (checkBoxHelper == null) { + checkBoxHelper = new SimplestCheckBoxHelper(this, avatarReceiver); + } + checkBoxHelper.setIsChecked(isChecked, animated); + } + } + @SuppressWarnings("WrongConstant") public void setCallItem (CallItem item) { long userId = item.getUserId(); @@ -229,6 +243,14 @@ public void setIsSecret (boolean isSecret) { } } + public void setNoSubtitle (boolean noSubtitle) { + int flags = BitwiseUtils.setFlag(this.flags, FLAG_NO_SUBTITLE, noSubtitle); + if (this.flags != flags) { + this.flags = flags; + invalidate(); + } + } + public void setAvatar (ImageFile avatar, AvatarPlaceholder.Metadata avatarPlaceholderMetadata) { if (avatar != null) { avatarReceiver.requestSpecific(tdlib, avatar, AvatarReceiver.Options.NONE); @@ -240,8 +262,8 @@ public void setAvatar (ImageFile avatar, AvatarPlaceholder.Metadata avatarPlaceh public void setEmojiStatus (@Nullable TdApi.User user) { emojiStatusHelper.updateEmoji(user, new TextColorSetOverride(TextColorSets.Regular.NORMAL) { @Override - public int emojiStatusColor () { - return Theme.getColor(ColorId.iconActive); + public long mediaTextComplexColor () { + return Theme.newComplexColor(true, ColorId.iconActive); } }); setTrimmedTitle(); @@ -268,7 +290,7 @@ private void setTrimmedTitle () { int width = getMeasuredWidth(); float avail = width - Screen.dp(72f) - ChatView.getTimePaddingRight(); if (timeWidth != 0) { - avail -= timeWidth + ChatView.getTimePaddingLeft(); + avail -= timeWidth + ChatView.getTimePaddingLeft(); } if ((flags & FLAG_SECRET) != 0) { avail -= Screen.dp(15f); @@ -381,14 +403,23 @@ protected void onDraw (Canvas c) { avatarReceiver.drawPlaceholder(c); } avatarReceiver.draw(c); - - int titleTop = Screen.dp(12f) + Screen.dp(1f); + final float checkFactor = checkBoxHelper != null ? checkBoxHelper.getCheckFactor() : 0f; + if (checkFactor > 0f) { + DrawAlgorithms.drawSimplestCheckBox(c, avatarReceiver, checkFactor); + } + boolean noSubtitle = BitwiseUtils.hasFlag(flags, FLAG_NO_SUBTITLE); int titleLeft = Screen.dp(72f); + int titleTop; + if (noSubtitle) { + titleTop = (getHeight() - displayTitle.getHeight()) / 2; + } else { + titleTop = Screen.dp(12f) + Screen.dp(1f); + } if (displayTitle != null) { boolean isSecret = (flags & FLAG_SECRET) != 0; Paint paint = ChatView.getTitlePaint((flags & FLAG_FAKE_TITLE) != 0); if (isSecret) { - Drawables.drawRtl(c, Icons.getSecureDrawable(), titleLeft - Screen.dp(6f), Screen.dp(12f), Paints.getGreenPorterDuffPaint(), width, rtl); + Drawables.drawRtl(c, Icons.getSecureDrawable(), titleLeft - Screen.dp(6f), titleTop - Screen.dp(1f), Paints.getGreenPorterDuffPaint(), width, rtl); titleLeft += Screen.dp(15f); paint.setColor(Theme.getColor(ColorId.textSecure)); } @@ -396,18 +427,20 @@ protected void onDraw (Canvas c) { titleLeft += displayTitle.getWidth(); } emojiStatusHelper.draw(c, titleLeft + Screen.dp(6), titleTop); - int subtitleOffset = -Screen.dp(1f); - if (displaySubtitle != null) { - int subtitleLeft = Screen.dp(72f); + if (!noSubtitle) { + int subtitleOffset = -Screen.dp(1f); + if (displaySubtitle != null) { + int subtitleLeft = Screen.dp(72f); + if (subtitleIcon != 0) { + subtitleLeft += Screen.dp(20f); + } + int subtitleTop = Screen.dp(39f) + subtitleOffset; + TextColorSet colorSet = BitwiseUtils.hasFlag(flags, FLAG_ONLINE) ? TextColorSets.Regular.NEUTRAL : null; + displaySubtitle.draw(c, subtitleLeft, subtitleTop, colorSet, 1f, subtitleMediaReceiver); + } if (subtitleIcon != 0) { - subtitleLeft += Screen.dp(20f); + Drawables.drawRtl(c, subtitleIconDrawable, Screen.dp(72f), Screen.dp(subtitleIcon == R.drawable.baseline_call_missed_18 ? 40f : 39f) + subtitleOffset, PorterDuffPaint.get(subtitleIconColorId), width, rtl); } - int subtitleTop = Screen.dp(39f) + subtitleOffset; - TextColorSet colorSet = BitwiseUtils.hasFlag(flags, FLAG_ONLINE) ? TextColorSets.Regular.NEUTRAL : null; - displaySubtitle.draw(c, subtitleLeft, subtitleTop, colorSet, 1f, subtitleMediaReceiver); - } - if (subtitleIcon != 0) { - Drawables.drawRtl(c, subtitleIconDrawable, Screen.dp(72f), Screen.dp(subtitleIcon == R.drawable.baseline_call_missed_18 ? 40f : 39f) + subtitleOffset, PorterDuffPaint.get(subtitleIconColorId), width, rtl); } if (time != null) { c.drawText(time, rtl ? ChatView.getTimePaddingRight() : width - ChatView.getTimePaddingRight() - timeWidth, Screen.dp(28f), ChatView.getTimePaint()); @@ -527,7 +560,7 @@ private void updateChat (boolean update) { lastChat.updateChat(); } setTitle(lastChat.getTitle(), lastChat.getTitleHighlight()); - setEmojiStatus(lastChat.getChat() != null ? tdlib.chatUser(lastChat.getChat()): (lastChat.getUserId() != 0 ? tdlib.cache().user(lastChat.getUserId()): null)); + setEmojiStatus(lastChat.getChat() != null ? tdlib.chatUser(lastChat.getChat()) : (lastChat.getUserId() != 0 ? tdlib.cache().user(lastChat.getUserId()) : null)); updateSubtitle(); lastChat.requestAvatar(avatarReceiver, AvatarReceiver.Options.SHOW_ONLINE); setTime(null); @@ -580,7 +613,7 @@ public void onChatPhotoChanged (long chatId, @Nullable TdApi.ChatPhotoInfo photo } @Override - public void onChatReadInbox(long chatId, long lastReadInboxMessageId, int unreadCount, boolean availabilityChanged) { + public void onChatReadInbox (long chatId, long lastReadInboxMessageId, int unreadCount, boolean availabilityChanged) { updateChat(chatId); } @@ -704,4 +737,14 @@ public void setMessage (@Nullable TGFoundMessage foundMessage) { invalidate(); } } + + @Override + public TdApi.Message getVisibleMessage () { + return lastMessage != null ? lastMessage.getMessage() : null; + } + + @Override + public int getVisibleMessageFlags () { + return TdlibMessageViewer.Flags.NO_SENSITIVE_SCREENSHOT_NOTIFICATION; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/BubbleLayout.java b/app/src/main/java/org/thunderdog/challegram/widget/BubbleLayout.java index f1bca867dc..296f6bda6f 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/BubbleLayout.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/BubbleLayout.java @@ -45,7 +45,7 @@ public class BubbleLayout extends AnimatedLinearLayout implements FactorAnimator private float maxAllowedVisibility = 1f; private final @Nullable ViewController themeProvider; - private final boolean top; + private boolean top; public BubbleLayout (@NonNull Context context, @Nullable ViewController themeProvider, boolean top) { super(context); @@ -61,13 +61,7 @@ public BubbleLayout (@NonNull Context context, @Nullable ViewController theme if (themeProvider != null) { themeProvider.addThemeInvalidateListener(this); } - int paddingTop = Screen.dp(2); - int paddingBottom = Screen.dp(4f) + Screen.dp(8f) + Screen.dp(1f); - if (top) { - setPadding(Screen.dp(1f), paddingBottom - Screen.dp(4f) - Screen.dp(2f), Screen.dp(1), paddingTop + Screen.dp(2f)); - } else { - setPadding(Screen.dp(1f), paddingTop, Screen.dp(1), paddingBottom); - } + setDefaultPadding(); ViewUtils.setBackground(this, new Drawable() { @Override public void draw (@NonNull Canvas c) { @@ -75,7 +69,7 @@ public void draw (@NonNull Canvas c) { int viewHeight = getMeasuredHeight(); int cornerWidth = Screen.dp(18f); int cornerHeight = Screen.dp(8f); - if (top) { + if (BubbleLayout.this.top) { backgroundDrawable.setBounds(0, cornerHeight - Screen.dp(2f), viewWidth, viewHeight); backgroundDrawable.draw(c); @@ -218,4 +212,22 @@ private void updateStyles () { setAlpha(MathUtils.clamp(factor)); } + public boolean setTop (boolean top) { + if (this.top != top) { + this.top = top; + requestLayout(); + return true; + } + return false; + } + + public void setDefaultPadding () { + int paddingTop = Screen.dp(2); + int paddingBottom = Screen.dp(4f) + Screen.dp(8f) + Screen.dp(1f); + if (top) { + setPadding(Screen.dp(1f), paddingBottom - Screen.dp(4f) - Screen.dp(2f), Screen.dp(1), paddingTop + Screen.dp(2f)); + } else { + setPadding(Screen.dp(1f), paddingTop, Screen.dp(1), paddingBottom); + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/ChartLayout.java b/app/src/main/java/org/thunderdog/challegram/widget/ChartLayout.java index 150c2b2b02..46002d71da 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/ChartLayout.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/ChartLayout.java @@ -34,7 +34,7 @@ import org.thunderdog.challegram.charts.data.ChartDataUtil; import org.thunderdog.challegram.charts.data.DoubleLinearChartData; import org.thunderdog.challegram.charts.data.StackBarChartData; -import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.ContentPreview; import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.navigation.ViewController; @@ -200,7 +200,7 @@ public void initWithType (Tdlib tdlib, int type, Delegate delegate, @Nullable Vi this.chartType = type; ViewSupport.setThemedBackground(this, ColorId.filling, themeProvider); - tdlib.client().send(new TdApi.GetAnimatedEmoji(TD.EMOJI_ABACUS.textRepresentation), result -> { + tdlib.client().send(new TdApi.GetAnimatedEmoji(ContentPreview.EMOJI_ABACUS.textRepresentation), result -> { if (result.getConstructor() == TdApi.AnimatedEmoji.CONSTRUCTOR) { TdApi.AnimatedEmoji emoji = (TdApi.AnimatedEmoji) result; tdlib.runOnUiThread(() -> { diff --git a/app/src/main/java/org/thunderdog/challegram/widget/CircleButton.java b/app/src/main/java/org/thunderdog/challegram/widget/CircleButton.java index 9d7acafde0..b61a22e8c9 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/CircleButton.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/CircleButton.java @@ -22,11 +22,21 @@ import android.view.View; import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.U; +import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGReaction; +import org.thunderdog.challegram.loader.ImageReceiver; +import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.support.RippleSupport; -import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.telegram.ReactionLoadListener; +import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.DrawAlgorithms; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; @@ -39,8 +49,9 @@ import me.vkryl.core.ColorUtils; import me.vkryl.core.MathUtils; import me.vkryl.core.StringUtils; +import me.vkryl.core.lambda.Destroyable; -public class CircleButton extends View implements FactorAnimator.Target { +public class CircleButton extends View implements FactorAnimator.Target, ReactionLoadListener, Destroyable { private Drawable icon; private int offsetLeft; @@ -50,15 +61,38 @@ public class CircleButton extends View implements FactorAnimator.Target { private int crossBackgroundColorId, crossIconColorId; + private final @Nullable Tdlib tdlib; + private final @Nullable ImageReceiver imageReceiver; + private final @Nullable GifReceiver gifReceiver; + public CircleButton (Context context) { super(context); Views.setClickable(this); + + this.tdlib = null; + this.imageReceiver = null; + this.gifReceiver = null; + } + + public CircleButton (Context context, @NonNull Tdlib tdlib) { + super(context); + Views.setClickable(this); + + this.tdlib = tdlib; + this.imageReceiver = new ImageReceiver(this, 0); + this.gifReceiver = new GifReceiver(this); } public void setIconColorId (int colorId) { if (!this.iconColorIsId || this.iconColor != colorId) { this.iconColorIsId = true; this.iconColor = colorId; + if (this.imageReceiver != null) { + this.imageReceiver.setThemedPorterDuffColorId(colorId); + } + if (this.gifReceiver != null) { + this.gifReceiver.setThemedPorterDuffColorId(colorId); + } invalidate(); } } @@ -67,6 +101,12 @@ public void setCustomIconColor (int color) { if (this.iconColorIsId || this.iconColor != color) { this.iconColorIsId = false; this.iconColor = color; + if (this.imageReceiver != null) { + this.imageReceiver.setPorterDuffColorFilter(color); + } + if (this.gifReceiver != null) { + this.gifReceiver.setPorterDuffColorFilter(color); + } invalidate(); } } @@ -377,15 +417,46 @@ public void setIconAlpha (float iconAlpha) { @Override protected void onDraw (Canvas c) { - float width = getMeasuredWidth(); - float height = getMeasuredHeight(); + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); int cx = (int) (width * .5f) + offsetLeft; int cy = (int) (height * .5f); boolean hasText = !StringUtils.isEmpty(bottomText); - if (icon != null) { + if (sticker != null && imageReceiver != null && gifReceiver != null) { + imageReceiver.setBounds(cx - width / 3, cy - height / 3, cx + width / 3, cy + height / 3); + gifReceiver.setBounds(cx - width / 3, cy - height / 3, cx + width / 3, cy + height / 3); + + final float MIN_SCALE = .82f; + final boolean isAnimation = sticker.isAnimated(); + float originalScale = sticker.getDisplayScale(); + boolean saved = originalScale != 1f || factor != 0f; + int restoreToCount = -1; + if (saved) { + restoreToCount = Views.save(c); + float scale = originalScale * (MIN_SCALE + (1f - MIN_SCALE) * (1f - factor)); + c.scale(scale, scale, cx, cy); + } + if (isAnimation) { + if (gifReceiver.needPlaceholder()) { + //if (imageReceiver.needPlaceholder()) { + // imageReceiver.drawPlaceholderContour(c, contour); + //} + imageReceiver.draw(c); + } + gifReceiver.draw(c); + } else { + //if (imageReceiver.needPlaceholder()) { + // imageReceiver.drawPlaceholderContour(c, contour); + //} + imageReceiver.draw(c); + } + if (saved) { + Views.restore(c, restoreToCount); + } + } else if (icon != null) { final int iconColor = iconColorIsId ? Theme.getColor(this.iconColor) : this.iconColor; final int crossIconColor = ColorUtils.fromToArgb(iconColor, crossIconColorId != 0 ? Theme.getColor(crossIconColorId) : iconColor, factor); final Paint bitmapPaint = getIconPaint(iconColor); @@ -414,7 +485,7 @@ protected void onDraw (Canvas c) { // TODO scale text down if it does not fit c.drawText(bottomText, cx - bottomTextWidth / 2, cy + Screen.dp(17f), Paints.getRegularTextPaint(14f, ColorUtils.alphaColor(scaleFactor, iconColor))); } - Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2 - (hasText ? Screen.dp(8f) : 0), bitmapPaint); + Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2f, cy - icon.getMinimumHeight() / 2f - (hasText ? Screen.dp(8f) : 0), bitmapPaint); if (savedRotation) { c.restore(); } @@ -444,7 +515,7 @@ protected void onDraw (Canvas c) { final float factor = this.factor / STEP_FACTOR; bitmapPaint.setAlpha((int) ((float) sourceAlpha * (1f - factor))); - Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2, bitmapPaint); + Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2f, cy - icon.getMinimumHeight() / 2f, bitmapPaint); bitmapPaint.setAlpha(sourceAlpha); } @@ -469,4 +540,92 @@ protected void onDraw (Canvas c) { } } } + + @Nullable + private TdApi.UnreadReaction unreadReactionRaw; + private TGReaction reaction; + private TGStickerObj sticker; + + public void setUnreadReaction (TdApi.UnreadReaction unreadReaction) { + if (tdlib == null || imageReceiver == null || gifReceiver == null || reaction != null && unreadReaction != null && unreadReactionRaw != null && StringUtils.equalsOrBothEmpty(TD.makeReactionKey(unreadReaction.type), TD.makeReactionKey(unreadReactionRaw.type))) { + return; + } + + if (unreadReactionRaw != null) { + tdlib.listeners().removeReactionLoadListener(TD.makeReactionKey(unreadReactionRaw.type), this); + } + + unreadReactionRaw = unreadReaction; + reaction = unreadReaction != null ? tdlib.getReaction(unreadReaction.type) : null; + sticker = reaction != null ? reaction.newCenterAnimationSicker() : null; + if (unreadReactionRaw != null && reaction == null) { + tdlib.listeners().addReactionLoadListener(TD.makeReactionKey(unreadReactionRaw.type), this); + } + if (sticker != null) { + if (sticker.getPreviewAnimation() != null && sticker.isEmojiReaction()) { + sticker.getPreviewAnimation().setPlayOnce(true); + sticker.getPreviewAnimation().setLooped(true); + } + gifReceiver.requestFile(sticker.getPreviewAnimation()); + imageReceiver.requestFile(sticker.getImage()); + if (sticker.needThemedColorFilter()) { + this.gifReceiver.setThemedPorterDuffColorId(ColorId.iconActive); + this.imageReceiver.setThemedPorterDuffColorId(ColorId.iconActive); + } else { + this.gifReceiver.disablePorterDuffColorFilter(); + this.imageReceiver.disablePorterDuffColorFilter(); + } + } else { + gifReceiver.clear(); + imageReceiver.clear(); + } + invalidate(); + } + + @Override + public void onReactionLoaded (String reactionKey) { + UI.execute(() -> { + if (unreadReactionRaw != null && StringUtils.equalsOrBothEmpty(reactionKey, TD.makeReactionKey(unreadReactionRaw.type))) { + setUnreadReaction(unreadReactionRaw); + } + }); + } + + @Override + protected void onAttachedToWindow () { + super.onAttachedToWindow(); + if (gifReceiver != null) { + gifReceiver.attach(); + } + if (imageReceiver != null) { + imageReceiver.attach(); + } + } + + @Override + protected void onDetachedFromWindow () { + super.onDetachedFromWindow(); + if (gifReceiver != null) { + gifReceiver.detach(); + } + if (imageReceiver != null) { + imageReceiver.detach(); + } + } + + @Override + public void performDestroy () { + if (imageReceiver != null) { + imageReceiver.destroy(); + } + if (gifReceiver != null) { + gifReceiver.destroy(); + } + if (unreadReactionRaw != null && tdlib != null) { + tdlib.listeners().removeReactionLoadListener(TD.makeReactionKey(unreadReactionRaw.type), this); + } + unreadReactionRaw = null; + reaction = null; + sticker = null; + } } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/CustomTextView.java b/app/src/main/java/org/thunderdog/challegram/widget/CustomTextView.java index 2f4ca7b57f..af0d5fbf6e 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/CustomTextView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/CustomTextView.java @@ -22,8 +22,8 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; +import androidx.core.util.ObjectsCompat; -import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Background; import org.thunderdog.challegram.core.Lang; @@ -127,8 +127,8 @@ public void performDestroy () { private final Tdlib tdlib; - private String rawText; - private TextEntity[] entities; + private @Nullable String rawText; + private @Nullable TextEntity[] entities; private final ReplaceAnimator text = new ReplaceAnimator<>(animator -> { if (getMeasuredHeight() != getCurrentHeight()) @@ -223,7 +223,7 @@ public void setText (CharSequence sequence, TextEntity[] entities, boolean anima if (sequence instanceof Spannable && (entities == null || entities.length == 0)) { entities = TD.collectAllEntities(null, tdlib, sequence, false, null); } - if ((rawText == null && text != null) || (rawText != null && !rawText.equals(text))) { + if (!ObjectsCompat.equals(rawText, text)) { this.rawText = text; this.entities = entities; cancelAsyncLayout(); @@ -478,9 +478,12 @@ public void detach () { @Override public void performDestroy () { + TGLegacyManager.instance().removeEmojiListener(this); for (ListAnimator.Entry entry : text) { entry.item.performDestroy(); } text.clear(false); + rawText = null; + entities = null; } } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/DoubleTextView.java b/app/src/main/java/org/thunderdog/challegram/widget/DoubleTextView.java index d3b7eb329d..885c46e28c 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/DoubleTextView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/DoubleTextView.java @@ -49,6 +49,7 @@ import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.util.EmojiStatusHelper; +import org.thunderdog.challegram.util.text.Highlight; import org.thunderdog.challegram.util.text.TextColorSets; import me.vkryl.core.lambda.Destroyable; @@ -198,7 +199,7 @@ public void setIsRounded (boolean isRounded) { private void checkButton () { if (button == null) { RelativeLayout.LayoutParams params; - params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(28f)); + params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, Screen.dp(28f)); params.addRule(Lang.rtl() ? RelativeLayout.ALIGN_PARENT_LEFT : RelativeLayout.ALIGN_PARENT_RIGHT); params.addRule(RelativeLayout.CENTER_VERTICAL); params.rightMargin = params.leftMargin = Screen.dp(19f); @@ -248,10 +249,10 @@ public void detach () { private @Nullable Path stickerSetContour; private boolean useAvatarReceiver; - public void setStickerSet (@NonNull TGStickerSetInfo stickerSet) { + public void setStickerSet (@NonNull TGStickerSetInfo stickerSet, String highlight) { needPlaceholder = false; - titleView.setText(stickerSet.getTitle()); - subtitleView.setText(Lang.plural(stickerSet.isMasks() ? R.string.xMasks : R.string.xStickers, stickerSet.getSize())); + titleView.setText(Highlight.toSpannable(stickerSet.getTitle(), highlight)); + subtitleView.setText(Lang.plural(stickerSet.isMasks() ? R.string.xMasks : stickerSet.isEmoji() ? R.string.xEmoji : R.string.xStickers, stickerSet.getSize())); receiver.getImageReceiver(0).requestFile(stickerSet.getPreviewImage()); receiver.getGifReceiver(0).requestFile(stickerSet.getPreviewAnimation()); receiver.getAvatarReceiver(0).clear(); @@ -300,6 +301,8 @@ public void setChatAvatar (Tdlib tdlib, long chatId) { @Override protected void onDraw (Canvas c) { + final boolean needThemedColorFilter = stickerSetInfo != null && stickerSetInfo.needThemedColorFilter(); + if (useAvatarReceiver) { AvatarReceiver avatarReceiver = receiver.getAvatarReceiver(0); if (avatarReceiver.needPlaceholder()) { @@ -308,12 +311,22 @@ protected void onDraw (Canvas c) { avatarReceiver.draw(c); } else if (stickerSetInfo != null && stickerSetInfo.isAnimated()) { GifReceiver gifReceiver = receiver.getGifReceiver(0); + if (needThemedColorFilter) { + gifReceiver.setThemedPorterDuffColorId(ColorId.iconActive); + } else { + gifReceiver.disablePorterDuffColorFilter(); + } if (gifReceiver.needPlaceholder()) { gifReceiver.drawPlaceholderContour(c, stickerSetContour); } gifReceiver.draw(c); } else { ImageReceiver imageReceiver = receiver.getImageReceiver(0); + if (needThemedColorFilter) { + imageReceiver.setThemedPorterDuffColorId(ColorId.iconActive); + } else { + imageReceiver.disablePorterDuffColorFilter(); + } if (imageReceiver.needPlaceholder()) { if (stickerSetContour != null) { imageReceiver.drawPlaceholderContour(c, stickerSetContour); diff --git a/app/src/main/java/org/thunderdog/challegram/widget/EmojiLayout.java b/app/src/main/java/org/thunderdog/challegram/widget/EmojiLayout.java index 0dc8afc786..6d4efcf7e9 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/EmojiLayout.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/EmojiLayout.java @@ -15,11 +15,6 @@ package org.thunderdog.challegram.widget; import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.drawable.Drawable; import android.os.Build; import android.view.Gravity; import android.view.MotionEvent; @@ -29,41 +24,42 @@ import android.widget.LinearLayout; import androidx.annotation.DrawableRes; +import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SparseArrayCompat; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.R; -import org.thunderdog.challegram.component.attach.CustomItemAnimator; import org.thunderdog.challegram.component.chat.EmojiToneHelper; +import org.thunderdog.challegram.component.sticker.StickerSmallView; import org.thunderdog.challegram.component.sticker.TGStickerObj; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; -import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGStickerSetInfo; import org.thunderdog.challegram.emoji.Emoji; -import org.thunderdog.challegram.loader.ImageReceiver; -import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.EmojiMediaType; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ThemeId; -import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Keyboard; -import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.UI; -import org.thunderdog.challegram.ui.EmojiStatusListController; import org.thunderdog.challegram.ui.EmojiListController; import org.thunderdog.challegram.ui.EmojiMediaListController; +import org.thunderdog.challegram.ui.EmojiStatusListController; import org.thunderdog.challegram.unsorted.Settings; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutRecyclerController; +import org.thunderdog.challegram.widget.emoji.header.EmojiHeaderView; +import org.thunderdog.challegram.widget.emoji.header.MediaHeaderView; +import org.thunderdog.challegram.widget.emoji.section.EmojiSection; +import org.thunderdog.challegram.widget.emoji.section.EmojiSectionView; +import org.thunderdog.challegram.widget.emoji.section.StickerSectionView; import org.thunderdog.challegram.widget.rtl.RtlViewPager; import java.util.ArrayList; @@ -71,24 +67,28 @@ import me.vkryl.android.AnimatorUtils; import me.vkryl.android.animator.FactorAnimator; import me.vkryl.android.widget.FrameLayoutFix; -import me.vkryl.core.ColorUtils; -import me.vkryl.core.lambda.Destroyable; -public class EmojiLayout extends FrameLayoutFix implements ViewTreeObserver.OnPreDrawListener, ViewPager.OnPageChangeListener, FactorAnimator.Target, View.OnClickListener, View.OnLongClickListener, Lang.Listener { +public class EmojiLayout extends FrameLayoutFix implements ViewTreeObserver.OnPreDrawListener, ViewPager.OnPageChangeListener, FactorAnimator.Target, View.OnClickListener, Lang.Listener, EmojiLayoutRecyclerController.Callback { public interface Listener { - default void onEnterEmoji (String emoji) {}; + default void onEnterEmoji (String emoji) {} + default void onEnterCustomEmoji (TGStickerObj sticker) {} + default boolean onSendSticker (@Nullable View view, TGStickerObj sticker, TdApi.MessageSendOptions sendOptions) { return false; } default boolean onSendGIF (@Nullable View view, TdApi.Animation animation) { return false; } - default boolean onSetEmojiStatus (@Nullable View view, TGStickerObj sticker, int duration) { + default boolean onSetEmojiStatus (@Nullable View view, TGStickerObj sticker, TdApi.EmojiStatus emojiStatus) { return false; - }; - default boolean isEmojiInputEmpty () { return true; }; - default void onDeleteEmoji () {}; - default void onSearchRequested (EmojiLayout layout, boolean areStickers) {}; + } + + default boolean isEmojiInputEmpty () { return true; } + + default void onDeleteEmoji () {} + + default void onSearchRequested (EmojiLayout layout, boolean areStickers) {} + default long getOutputChatId () { return 0; } default void onSectionSwitched (EmojiLayout layout, @EmojiMediaType int section, @EmojiMediaType int prevSection) { } @@ -111,40 +111,20 @@ public static int getHeaderPadding () { return Screen.dp(6f); } - public static int getHeaderImagePadding () { - return Screen.dp(10f); - } - public static int getHorizontalPadding () { return Screen.dp(2.5f); } private ShadowView shadowView; - private @Nullable FrameLayoutFix emojiSectionsView; + private @Nullable EmojiHeaderView emojiHeaderView; + private @Nullable MediaHeaderView mediaSectionsView; - private @Nullable RecyclerView mediaSectionsView; + private int emojiSectionsSize = 0; - private ArrayList emojiSections; - private int currentEmojiSection; - - public void setCurrentEmojiSection (int section) { - if (this.currentEmojiSection != section && section != -1) { - emojiSections.get(currentEmojiSection).setFactor(0f, headerHideFactor != 1f && currentPageFactor != 1f); - this.currentEmojiSection = section; - emojiSections.get(currentEmojiSection).setFactor(1f, headerHideFactor != 1f && currentPageFactor != 1f); - } - } - - private static final int OFFSET = 2; - - public void removeStickerSection (int section) { - mediaAdapter.removeStickerSet(section - mediaAdapter.getAddItemCount(true)); - } - - private void clearRecentStickers () { - if (themeProvider != null && mediaAdapter.hasRecents) { + public void clearRecentStickers () { + if (themeProvider != null && mediaSectionsView.hasRecents()) { themeProvider.showOptions(null, new int[] {R.id.btn_done, R.id.btn_cancel}, new String[] { - Lang.getString(animatedEmojiOnly ? R.string.ClearRecentEmojiStatuses: R.string.ClearRecentStickers), + Lang.getString(animatedEmojiOnly ? R.string.ClearRecentEmojiStatuses : R.string.ClearRecentStickers), Lang.getString(R.string.Cancel) }, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_auto_delete_24, R.drawable.baseline_cancel_24}, (itemView, id) -> { if (id == R.id.btn_done) { @@ -173,994 +153,122 @@ private void clearRecentEmoji () { themeProvider.showOptions(null, new int[] {R.id.btn_delete, R.id.btn_cancel}, new String[] {Lang.getString(R.string.ClearRecentEmojiAction), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_auto_delete_24, R.drawable.baseline_cancel_24}, (itemView, id) -> { if (id == R.id.btn_delete) { Emoji.instance().clearRecents(); - ViewController c = adapter.getCachedItem(0); - if (c != null && !animatedEmojiOnly) { - ((EmojiListController) c).resetRecentEmoji(); - } - } - return true; - }); - } - } - - private void openEmojiSetOptions (final TGStickerSetInfo info) { - if (themeProvider == null) return; - - boolean isTrending = info.isTrendingEmoji(); - themeProvider.showOptions(null, new int[] { - R.id.btn_copyLink, - isTrending ? R.id.btn_addStickerSet: R.id.more_btn_delete - }, new String[] { - Lang.getString(R.string.CopyLink), - Lang.getString(isTrending ? R.string.AddPack: R.string.DeletePack) - }, new int[] { - ViewController.OPTION_COLOR_NORMAL, - isTrending ? ViewController.OPTION_COLOR_NORMAL: ViewController.OPTION_COLOR_RED - }, new int[] { - R.drawable.baseline_link_24, - isTrending ? R.drawable.deproko_baseline_insert_sticker_24: R.drawable.baseline_delete_24 - }, (itemView, id) -> { - if (id == R.id.more_btn_delete) { - if (themeProvider != null) { - themeProvider.showOptions(Lang.getStringBold(R.string.RemoveEmojiSet, info.getTitle()), new int[] {R.id.btn_delete, R.id.btn_cancel}, new String[] {Lang.getString(R.string.RemoveStickerSetAction), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_24, R.drawable.baseline_cancel_24}, (resultItemView, resultId) -> { - if (resultId == R.id.btn_delete) { - ViewController c = adapter.getCachedItem(0); - if (c != null) { - ((EmojiStatusListController) c).removeStickerSet(info); - } - parentController.tdlib().client().send(new TdApi.ChangeStickerSet(info.getId(), false, false), parentController.tdlib().okHandler()); - } - return true; - }); - } - } else if (id == R.id.btn_addStickerSet) { - info.unsetIsTrendingEmoji(); - parentController.tdlib().client().send(new TdApi.ChangeStickerSet(info.getId(), true, false), parentController.tdlib().okHandler()); - } else if (id == R.id.btn_copyLink) { - UI.copyText(TD.getEmojiPackLink(info.getName()), R.string.CopiedLink); - } - return true; - }); - } - - private void removeStickerSet (final TGStickerSetInfo info) { - if (animatedEmojiOnly) return; - - if (themeProvider != null) { - themeProvider.showOptions(null, new int[] {R.id.btn_copyLink, R.id.btn_archive, R.id.more_btn_delete}, new String[] {Lang.getString(R.string.CopyLink), Lang.getString(R.string.ArchivePack), Lang.getString(R.string.DeletePack)}, new int[] {ViewController.OPTION_COLOR_NORMAL, ViewController.OPTION_COLOR_NORMAL, ViewController.OPTION_COLOR_RED}, new int[] {R.drawable.baseline_link_24, R.drawable.baseline_archive_24, R.drawable.baseline_delete_24}, (itemView, id) -> { - if (id == R.id.more_btn_delete) { - if (themeProvider != null) { - themeProvider.showOptions(Lang.getStringBold(R.string.RemoveStickerSet, info.getTitle()), new int[] {R.id.btn_delete, R.id.btn_cancel}, new String[] {Lang.getString(R.string.RemoveStickerSetAction), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_24, R.drawable.baseline_cancel_24}, (resultItemView, resultId) -> { - if (resultId == R.id.btn_delete) { - parentController.tdlib().client().send(new TdApi.ChangeStickerSet(info.getId(), false, false), parentController.tdlib().okHandler()); - } - return true; - }); - } - } else if (id == R.id.btn_archive) { - if (themeProvider != null) { - themeProvider.showOptions(Lang.getStringBold(R.string.ArchiveStickerSet, info.getTitle()), new int[] {R.id.btn_delete, R.id.btn_cancel}, new String[] {Lang.getString(R.string.ArchiveStickerSetAction), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_archive_24, R.drawable.baseline_cancel_24}, (resultItemView, resultId) -> { - if (resultId == R.id.btn_delete) { - parentController.tdlib().client().send(new TdApi.ChangeStickerSet(info.getId(), false, true), parentController.tdlib().okHandler()); - } - return true; - }); - } - } else if (id == R.id.btn_copyLink) { - UI.copyText(TD.getStickerPackLink(info.getName()), R.string.CopiedLink); - } - return true; - }); - } - } - - public void addStickerSection (int section, TGStickerSetInfo info) { - mediaAdapter.addStickerSet(section - mediaAdapter.getAddItemCount(true), info); - } - - public void moveStickerSection (int fromSection, int toSection) { - int addItems = mediaAdapter.getAddItemCount(true); - mediaAdapter.moveStickerSet(fromSection - addItems, toSection - addItems); - } - - public void setCurrentStickerSectionByPosition (int i, boolean isStickerSection, boolean animated) { - if (mediaAdapter.hasRecents && mediaAdapter.hasFavorite && isStickerSection && i >= 1) { - i--; - } - if (isStickerSection) { - i += mediaAdapter.headerItems.size() - mediaAdapter.getAddItemCount(false); - } - setCurrentStickerSection(mediaAdapter.getObject(i), animated); - } - - private void setCurrentStickerSection (Object obj, boolean animated) { - if (mediaAdapter.setSelectedObject(obj, animated, mediaSectionsView.getLayoutManager())) { - int section = mediaAdapter.indexOfObject(obj); - int first = ((LinearLayoutManager) mediaSectionsView.getLayoutManager()).findFirstVisibleItemPosition(); - int last = ((LinearLayoutManager) mediaSectionsView.getLayoutManager()).findLastVisibleItemPosition(); - int itemWidth = (Screen.currentWidth() - getHorizontalPadding() * 2) / emojiSections.size(); - - if (first != -1) { - int scrollX = first * itemWidth; - View v = mediaSectionsView.getLayoutManager().findViewByPosition(first); - if (v != null) { - scrollX += -v.getLeft(); - } - - if (section - OFFSET < first) { - int desiredScrollX = section * itemWidth - itemWidth / 2 - itemWidth * (OFFSET - 1); - if (animated && headerHideFactor != 1f) { - mediaSectionsView.smoothScrollBy(desiredScrollX - scrollX, 0); - } else { - mediaSectionsView.scrollBy(desiredScrollX - scrollX, 0); - } - } else if (section + OFFSET > last) { - int desiredScrollX = Math.max(0, (section - emojiSections.size()) * itemWidth + itemWidth * OFFSET + (animatedEmojiOnly ? -itemWidth: itemWidth / 2)); - if (animated && headerHideFactor != 1f) { - mediaSectionsView.smoothScrollBy(desiredScrollX - scrollX, 0); - } else { - mediaSectionsView.scrollBy(desiredScrollX - scrollX, 0); - } - } - } - } - } - - private CircleButton circleButton; - - public void onEnterEmoji (String emoji) { - if (listener != null) { - listener.onEnterEmoji(emoji); - } - } - - public static class EmojiSection implements FactorAnimator.Target { - public final int index; - public float selectionFactor; - - private int iconRes; - public Drawable icon; - public @Nullable Drawable activeIcon; - - private boolean activeDisabled; - private boolean isTrending; - - private @Nullable View view; - private EmojiLayout parent; - - private int activeIconRes; - - public EmojiSection (EmojiLayout parent, int sectionIndex, @DrawableRes int iconRes, @DrawableRes int activeIconRes) { - this.parent = parent; - this.index = sectionIndex; - this.activeIconRes = activeIconRes; - this.activeIcon = Drawables.get(parent.getResources(), activeIconRes); - changeIcon(iconRes); - } - - public void setIsTrending () { - this.isTrending = true; - } - - public boolean isTrending () { - return isTrending; - } - - public EmojiSection setActiveDisabled () { - activeDisabled = true; - return this; - } - - public void changeIcon (final int iconRes, final int activeIconRes) { - changeIcon(iconRes); - if (this.activeIconRes != activeIconRes) { - this.activeIcon = Drawables.get(parent.getResources(), this.activeIconRes = activeIconRes); - if (view != null) { - view.invalidate(); - } - } - } - - public void changeIcon (final int iconRes) { - if (this.iconRes != iconRes) { - this.icon = Drawables.get(parent.getResources(), this.iconRes = iconRes); - if (view != null) { - view.invalidate(); - } - } - } - - @Override - public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { - setFactor(factor); - } - - @Override - public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { } - - private @Nullable FactorAnimator animator; - - public EmojiSection setFactor (float toFactor, boolean animated) { - if (selectionFactor != toFactor && animated && view != null) { - if (animator == null) { - animator = new FactorAnimator(0, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180, selectionFactor); - } - animator.animateTo(toFactor); - } else { - if (animator != null) { - animator.forceFactor(toFactor); - } - setFactor(toFactor); - } - return this; - } - - private void setFactor (float factor) { - if (this.selectionFactor != factor) { - this.selectionFactor = factor; - - if (isPanda) { - if (factor == 1f) { - startPandaTimer(); - } else { - cancelPandaTimer(); - } - } - - if (view != null) { - view.invalidate(); - } - } - } - - public void setCurrentView (View view) { - this.view = view; - } - - private boolean makeFirstTransparent; - - public EmojiSection setMakeFirstTransparent () { - this.makeFirstTransparent = true; - return this; - } - - private int offsetHalf; - - public EmojiSection setOffsetHalf (boolean fromRight) { - this.offsetHalf = fromRight ? 1 : -1; - return this; - } - - private boolean isPanda, doesPandaBlink, isPandaBlinking; - private Runnable pandaBlink; - - public EmojiSection setIsPanda (boolean isPanda) { - this.isPanda = isPanda; - return this; - } - - private void setPandaBlink (boolean inBlink) { - if (this.doesPandaBlink != inBlink) { - this.doesPandaBlink = inBlink; - this.activeIcon = Drawables.get(parent.getResources(), inBlink ? R.drawable.deproko_baseline_animals_filled_blink_24 : activeIconRes); - if (view != null) { - view.invalidate(); - } - } - } - - private void startPandaTimer () { - if (!isPandaBlinking) { - this.isPandaBlinking = true; - if (pandaBlink == null) { - this.pandaBlink = () -> { - if (isPandaBlinking || doesPandaBlink) { - setPandaBlink(!doesPandaBlink); - if (isPandaBlinking) { - scheduleBlink(false); - } - } - }; - } - blinkNum = 0; - scheduleBlink(true); - } - } - - private int blinkNum; - - private void scheduleBlink (boolean firstTime) { - if (view != null) { - long delay; - switch (blinkNum++) { - case 0: { - setPandaBlink(false); - delay = firstTime ? 6000 : 1000; - break; - } - case 1: case 3: case 5: { - delay = 140; - break; - } - case 2: - case 4: { - delay = 4000; - break; - } - case 6: { - delay = 370; - break; - } - case 7: { - delay = 130; - break; - } - case 8: { - delay = 4000; - blinkNum = 0; - break; - } - default: { - delay = 1000; - blinkNum = 0; - break; - } - } - view.postDelayed(pandaBlink, delay); - } - - } - - private void cancelPandaTimer () { - if (isPandaBlinking) { - isPandaBlinking = false; - setPandaBlink(false); - if (view != null) { - view.removeCallbacks(pandaBlink); - } - } - } - - public void draw (Canvas c, int cx, int cy) { - if (selectionFactor == 0f || activeDisabled) { - Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2, parent.useDarkMode ? Paints.getPorterDuffPaint(Theme.getColor(ColorId.icon, ThemeId.NIGHT_BLACK)) : Paints.getIconGrayPorterDuffPaint()); - } else if (selectionFactor == 1f) { - final Drawable icon = this.activeIcon != null ? activeIcon : this.icon; - Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2, parent.useDarkMode ? Paints.getPorterDuffPaint(Theme.getColor(ColorId.iconActive, ThemeId.NIGHT_BLACK)) : Paints.getActiveKeyboardPaint()); - } else { - final Paint grayPaint = parent.useDarkMode ? Paints.getPorterDuffPaint(Theme.getColor(ColorId.icon, ThemeId.NIGHT_BLACK)) : Paints.getIconGrayPorterDuffPaint(); - final int grayAlpha = grayPaint.getAlpha(); - - if (makeFirstTransparent) { - int newAlpha = (int) ((float) grayAlpha * (1f - selectionFactor)); - grayPaint.setAlpha(newAlpha); - } else if (isPanda) { - int newAlpha = (int) ((float) grayAlpha * (1f - (1f - AnimatorUtils.DECELERATE_INTERPOLATOR.getInterpolation(1f - selectionFactor)))); - grayPaint.setAlpha(newAlpha); - } - - Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2, grayPaint); - grayPaint.setAlpha(grayAlpha); - - final Drawable icon = this.activeIcon != null ? activeIcon : this.icon; - final Paint iconPaint = Paints.getActiveKeyboardPaint(); - final int sourceIconAlpha = iconPaint.getAlpha(); - int alpha = (int) ((float) sourceIconAlpha * selectionFactor); - iconPaint.setAlpha(alpha); - Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2, iconPaint); - iconPaint.setAlpha(sourceIconAlpha); - } - } - } - - public static class EmojiSectionView extends View { - public EmojiSectionView (Context context) { - super(context); - } - - private int itemCount; - - public void setItemCount (int count) { - this.itemCount = count; - } - - private EmojiSection section; - - public void setSection (EmojiSection section) { - if (this.section != null) { - this.section.setCurrentView(null); - } - this.section = section; - if (section != null) { - section.setCurrentView(this); - } - } - - public EmojiSection getSection () { - return section; - } - - private boolean needTranslate; - - public void setNeedTranslate () { - this.needTranslate = true; - } - - @Override - protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { - int itemsSize = Screen.currentWidth(); - int itemWidth = (itemsSize - getHorizontalPadding() * 2) / itemCount; // FIXME MeasureSpec.getSize() - setMeasuredDimension(MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); - if (section != null && needTranslate) { - setTranslationX(Lang.rtl() ? itemsSize - itemWidth * (section.index + 1) : section.index * itemWidth); - } - } - - @Override - protected void onDraw (Canvas c) { - if (section != null) { - section.draw(c, getMeasuredWidth() / 2, getMeasuredHeight() / 2); - } - } - } - - private MediaAdapter mediaAdapter; - - private static class MediaHolder extends RecyclerView.ViewHolder { - public static final int TYPE_EMOJI_SECTION = 0; - public static final int TYPE_STICKER_SECTION = 1; - - public MediaHolder (View itemView) { - super(itemView); - } - - public static MediaHolder create (Context context, int viewType, View.OnClickListener onClickListener, View.OnLongClickListener onLongClickListener, int emojiSectionCount, @Nullable ViewController themeProvider) { - switch (viewType) { - case TYPE_EMOJI_SECTION: { - EmojiSectionView sectionView = new EmojiSectionView(context); - if (themeProvider != null) { - themeProvider.addThemeInvalidateListener(sectionView); - } - sectionView.setId(R.id.btn_section); - sectionView.setOnClickListener(onClickListener); - sectionView.setItemCount(emojiSectionCount); - sectionView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); - return new MediaHolder(sectionView); - } - case TYPE_STICKER_SECTION: { - StickerSectionView sectionView = new StickerSectionView(context); - if (themeProvider != null) { - themeProvider.addThemeInvalidateListener(sectionView); - } - sectionView.setOnLongClickListener(onLongClickListener); - sectionView.setId(R.id.btn_stickerSet); - sectionView.setOnClickListener(onClickListener); - sectionView.setItemCount(emojiSectionCount); - sectionView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); - return new MediaHolder(sectionView); - } - } - throw new RuntimeException("viewType == " + viewType); - } - } - - private static class StickerSectionView extends View implements Destroyable, FactorAnimator.Target { - private final ImageReceiver receiver; - private final GifReceiver gifReceiver; - - private int itemCount; - - private float selectionFactor; - - public StickerSectionView (Context context) { - super(context); - receiver = new ImageReceiver(this, 0); - gifReceiver = new GifReceiver(this); - } - - public void setItemCount (int itemCount) { - this.itemCount = itemCount; - } - - public void attach () { - receiver.attach(); - gifReceiver.attach(); - } - - public void detach () { - receiver.detach(); - gifReceiver.detach(); - } - - @Override - public void performDestroy () { - receiver.destroy(); - gifReceiver.destroy(); - } - - private TGStickerSetInfo info; - private Path contour; - - public void setStickerSet (@NonNull TGStickerSetInfo info) { - this.info = info; - this.contour = info.getPreviewContour(Math.min(receiver.getWidth(), receiver.getHeight())); - receiver.requestFile(info.getPreviewImage()); - gifReceiver.requestFile(info.getPreviewAnimation()); - } - - private FactorAnimator animator; - - public void setSelectionFactor (float factor, boolean animated) { - if (animated && this.selectionFactor != factor) { - if (animator == null) { - animator = new FactorAnimator(0, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180l, selectionFactor); - } - animator.animateTo(factor); - } else { - if (animator != null) { - animator.forceFactor(factor); - } - setSelectionFactor(factor); - } - } - - @Override - public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { - switch (id) { - case 0: { - setSelectionFactor(factor); - break; - } - } - } - - @Override - public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { - - } - - private void setSelectionFactor (float factor) { - if (this.selectionFactor != factor) { - this.selectionFactor = factor; - invalidate(); - } - } - - public @Nullable TGStickerSetInfo getStickerSet () { - return info; - } - - @Override - protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { - int itemWidth = (Screen.currentWidth() - getHorizontalPadding() * 2) / itemCount; // FIXME MeasureSpec.getSize() - setMeasuredDimension(MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); - setBounds(); - } - - private void setBounds () { - int padding = getHeaderImagePadding(); - int width = receiver.getWidth(), height = receiver.getHeight(); - receiver.setBounds(padding, padding, getMeasuredWidth() - padding, getMeasuredHeight() - padding); - gifReceiver.setBounds(padding, padding, getMeasuredWidth() - padding, getMeasuredHeight() - padding); - if (info != null && (width != receiver.getWidth() || height != receiver.getHeight())) { - this.contour = info.getPreviewContour(Math.min(receiver.getWidth(), receiver.getHeight())); - } - } - - @Override - protected void onDraw (Canvas c) { - int cx = getMeasuredWidth() / 2; - int cy = getMeasuredHeight() / 2; - final boolean saved = selectionFactor != 0f; - if (saved) { - final int selectionColor = Theme.chatSelectionColor(); - final int selectionAlpha = Color.alpha(selectionColor); - int color = ColorUtils.color((int) ((float) selectionAlpha * selectionFactor), selectionColor); - int radius = Screen.dp(18f) - (int) ((float) Screen.dp(4f) * (1f - selectionFactor)); - - c.drawCircle(cx, cy, radius, Paints.fillingPaint(color)); - c.save(); - float scale = .85f + .15f * (1f - selectionFactor); - c.scale(scale, scale, cx, cy); - } - - if (info != null && info.isAnimated()) { - if (gifReceiver.needPlaceholder()) { - if (receiver.needPlaceholder()) { - receiver.drawPlaceholderContour(c, contour); - } - receiver.draw(c); - } - gifReceiver.draw(c); - } else { - if (receiver.needPlaceholder()) { - receiver.drawPlaceholderContour(c, contour); - } - receiver.draw(c); - } - if (Config.DEBUG_STICKER_OUTLINES) { - receiver.drawPlaceholderContour(c, contour); - } - if (saved) { - c.restore(); - } - } - } - - private static class MediaAdapter extends RecyclerView.Adapter implements View.OnLongClickListener { - private final Context context; - private final View.OnClickListener onClickListener; - private final ArrayList headerItems; - private final int sectionItemCount; - private final EmojiLayout parent; - - private final @Nullable ViewController themeProvider; - - private Object selectedObject; - private boolean hasRecents, hasFavorite; - - public MediaAdapter (Context context, EmojiLayout parent, OnClickListener onClickListener, int sectionItemCount, boolean selectedIsGifs, @Nullable ViewController themeProvider, boolean hideSectionsExceptRecent) { - this.context = context; - this.parent = parent; - this.onClickListener = onClickListener; - this.themeProvider = themeProvider; - this.headerItems = new ArrayList<>(); - if (!hideSectionsExceptRecent) { - this.headerItems.add(new EmojiSection(parent, -1, R.drawable.baseline_emoticon_outline_24, 0).setActiveDisabled()); - this.headerItems.add(new EmojiSection(parent, -2, R.drawable.deproko_baseline_gif_24, R.drawable.deproko_baseline_gif_filled_24)); - this.headerItems.add(new EmojiSection(parent, -3, R.drawable.outline_whatshot_24, R.drawable.baseline_whatshot_24).setMakeFirstTransparent()); - } - // this.favoriteSection = new EmojiSection(parent, -4, R.drawable.baseline_star_border_24, R.drawable.baseline_star_24).setMakeFirstTransparent(); - this.recentSection = new EmojiSection(parent, -4, R.drawable.baseline_access_time_24, R.drawable.baseline_watch_later_24).setMakeFirstTransparent(); - this.trendingSection = new EmojiSection(parent, -5, R.drawable.outline_whatshot_24, R.drawable.baseline_whatshot_24).setMakeFirstTransparent(); - this.trendingSection.setIsTrending(); - - this.selectedObject = selectedIsGifs ? headerItems.get(1) : recentSection; - if (selectedIsGifs) { - this.headerItems.get(1).setFactor(1f, false); - } else { - this.recentSection.setFactor(1f, false); - } - - this.sectionItemCount = sectionItemCount; - this.stickerSets = new ArrayList<>(); - } - - public void addHeaderItem (EmojiSection emojiSection) { - this.headerItems.add(emojiSection); - } - - public void setHasRecents (boolean hasRecents) { - if (this.hasRecents != hasRecents) { - this.hasRecents = hasRecents; - checkRecent(); - } - } - - public void setShowRecentsAsFound (boolean showRecentAsFound) { - recentSection.changeIcon( - showRecentAsFound ? R.drawable.baseline_emoticon_outline_24: R.drawable.baseline_access_time_24, - showRecentAsFound ? 0: R.drawable.baseline_watch_later_24); - } - - public int getAddItemCount (boolean allowHidden) { - int i = 0; - if (allowHidden) { - if (hasFavorite) { - i++; - } - if (hasRecents) { - i++; - } - if (hasTrending) { - i++; - } - } else { - if (showingRecentSection) { - i++; - } - if (showingTrendingSection) { - i++; - } - } - return i; - } - - private boolean showingRecentSection; - - private void checkRecent () { - boolean showRecent = hasFavorite || hasRecents; - if (this.showingRecentSection != showRecent) { - this.showingRecentSection = showRecent; - if (showRecent) { - headerItems.add(recentSection); - notifyItemInserted(headerItems.size() - 1); - } else { - int i = headerItems.indexOf(recentSection); - if (i != -1) { - headerItems.remove(i); - notifyItemRemoved(i); - } - } - } else if (selectedObject != null) { - int i = indexOfObject(selectedObject); - if (i != -1) { - notifyItemRangeChanged(i, 2); - } - } - } - - private boolean showingTrendingSection; - - private void checkTrending () { - boolean showTrending = hasTrending; - if (this.showingTrendingSection != showTrending) { - this.showingTrendingSection = showTrending; - if (showTrending) { - headerItems.add(trendingSection); - notifyItemInserted(headerItems.size() - 1); - } else { - int i = headerItems.indexOf(trendingSection); - if (i != -1) { - headerItems.remove(i); - notifyItemRemoved(i); - } - } - } else if (selectedObject != null) { - int i = indexOfObject(selectedObject); - if (i != -1) { - notifyItemRangeChanged(i, 2); - } - } - } - - public void setHasFavorite (boolean hasFavorite) { - if (this.hasFavorite != hasFavorite) { - this.hasFavorite = hasFavorite; - checkRecent(); - } - /*if (this.showFavorite != showFavorite) { - this.showFavorite = showFavorite; - if (showFavorite) { - int i = showRecents ? headerItems.size() - 1 : headerItems.size(); - headerItems.add(i, favoriteSection); - notifyItemInserted(i); - } else { - int i = headerItems.indexOf(favoriteSection); - if (i != -1) { - headerItems.remove(i); - notifyItemRemoved(i); - } - } - }*/ - } - - private boolean hasNewHots; - - public void setHasNewHots (boolean hasHots) { - if (this.hasNewHots != hasHots) { - this.hasNewHots = hasHots; - // TODO - } - } - - private boolean hasTrending; - - public void setHasTrending (boolean hasTrending) { - if (this.hasTrending != hasTrending) { - this.hasTrending = hasTrending; - checkTrending(); - } - } - - public boolean setSelectedObject (Object obj, boolean animated, RecyclerView.LayoutManager manager) { - if (this.selectedObject != obj) { - setSelected(this.selectedObject, false, animated, manager); - this.selectedObject = obj; - setSelected(obj, true, animated, manager); - return true; - } - return false; - } - - private Object getObject (int i) { - if (i < 0) return null; - if (i < headerItems.size()) { - return headerItems.get(i); - } else { - int index = i - headerItems.size(); - return index >= 0 && index < stickerSets.size() ? stickerSets.get(index) : null; - } - } - - private int indexOfObject (Object obj) { - int itemCount = getItemCount(); - for (int i = 0; i < itemCount; i++) { - if (getObject(i) == obj) { - return i; - } - } - return -1; - } - - private void setSelected (Object obj, boolean selected, boolean animated, RecyclerView.LayoutManager manager) { - int index = indexOfObject(obj); - if (index != -1) { - switch (getItemViewType(index)) { - case MediaHolder.TYPE_EMOJI_SECTION: { - if (index >= 0 && index < headerItems.size()) { - headerItems.get(index).setFactor(selected ? 1f : 0f, animated); - } - break; - } - case MediaHolder.TYPE_STICKER_SECTION: { - View view = manager.findViewByPosition(index); - if (view != null && view instanceof StickerSectionView) { - ((StickerSectionView) view).setSelectionFactor(selected ? 1f : 0f, animated); - } else { - notifyItemChanged(index); - } - break; - } - } - } - } - - private final ArrayList stickerSets; - private final EmojiSection recentSection; // favoriteSection - private final EmojiSection trendingSection; // favoriteSection - - public void removeStickerSet (int index) { - if (index >= 0 && index < stickerSets.size()) { - stickerSets.remove(index); - notifyItemRemoved(index + headerItems.size()); - } - } - - public void addStickerSet (int index, TGStickerSetInfo info) { - stickerSets.add(index, info); - notifyItemInserted(index + headerItems.size()); - } - - public void moveStickerSet (int fromIndex, int toIndex) { - TGStickerSetInfo info = stickerSets.remove(fromIndex); - stickerSets.add(toIndex, info); - fromIndex += headerItems.size(); - toIndex += headerItems.size(); - notifyItemMoved(fromIndex, toIndex); - } - - public void setStickerSets (ArrayList stickers) { - if (!stickerSets.isEmpty()) { - int removedCount = stickerSets.size(); - stickerSets.clear(); - notifyItemRangeRemoved(headerItems.size(), removedCount); - } - if (stickers != null && !stickers.isEmpty()) { - int addedCount; - if (!stickers.get(0).isSystem()) { - stickerSets.addAll(stickers); - addedCount = stickers.size(); - } else { - addedCount = 0; - for (int i = 0; i < stickers.size(); i++) { - TGStickerSetInfo stickerSet = stickers.get(i); - if (stickerSet.isSystem()) { - continue; - } - stickerSets.add(stickerSet); - addedCount++; - } - } - notifyItemRangeInserted(headerItems.size(), addedCount); - } - } - - @Override - public MediaHolder onCreateViewHolder (ViewGroup parent, int viewType) { - return MediaHolder.create(context, viewType, onClickListener, this, sectionItemCount, themeProvider); - } - - @Override - public boolean onLongClick (View v) { - // if (parent != null && parent.animatedEmojiOnly) return false; - if (v instanceof StickerSectionView) { - StickerSectionView sectionView = (StickerSectionView) v; - TGStickerSetInfo info = sectionView.getStickerSet(); - if (parent != null) { - if (parent.animatedEmojiOnly) { - parent.openEmojiSetOptions(info); - } else { - parent.removeStickerSet(info); - } - return true; - } - return false; - } - if ((v instanceof EmojiSectionView)) { - EmojiSectionView sectionView = (EmojiSectionView) v; - EmojiSection section = sectionView.getSection(); - - if (parent != null) { - if (section == recentSection) { - parent.clearRecentStickers(); - return true; + ViewController c = adapter.getCachedItem(0); + if (c != null && !animatedEmojiOnly) { + ((EmojiListController) c).resetRecentEmoji(); } } - } - - return false; + return true; + }); } + } - @Override - public void onBindViewHolder (MediaHolder holder, int position) { - switch (holder.getItemViewType()) { - case MediaHolder.TYPE_EMOJI_SECTION: { - EmojiSection section = headerItems.get(position); - ((EmojiSectionView) holder.itemView).setSection(section); - holder.itemView.setOnLongClickListener(section == recentSection ? this : null); - break; + public void openEmojiSetOptions (final TGStickerSetInfo info) { + if (themeProvider == null) return; + + boolean isTrending = info.isTrendingEmoji(); + themeProvider.showOptions(null, new int[] { + R.id.btn_copyLink, + isTrending ? R.id.btn_addStickerSet : R.id.more_btn_delete + }, new String[] { + Lang.getString(R.string.CopyLink), + Lang.getString(isTrending ? R.string.AddPack : R.string.DeletePack) + }, new int[] { + ViewController.OPTION_COLOR_NORMAL, + isTrending ? ViewController.OPTION_COLOR_NORMAL : ViewController.OPTION_COLOR_RED + }, new int[] { + R.drawable.baseline_link_24, + isTrending ? R.drawable.deproko_baseline_insert_sticker_24 : R.drawable.baseline_delete_24 + }, (itemView, id) -> { + if (id == R.id.more_btn_delete) { + if (themeProvider != null) { + themeProvider.showOptions(Lang.getStringBold(R.string.RemoveEmojiSet, info.getTitle()), new int[] {R.id.btn_delete, R.id.btn_cancel}, new String[] {Lang.getString(R.string.RemoveStickerSetAction), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_24, R.drawable.baseline_cancel_24}, (resultItemView, resultId) -> { + if (resultId == R.id.btn_delete) { + ViewController c = adapter.getCachedItem(0); + if (c != null) { + ((EmojiStatusListController) c).removeStickerSet(info); + } + parentController.tdlib().client().send(new TdApi.ChangeStickerSet(info.getId(), false, false), parentController.tdlib().okHandler()); + } + return true; + }); } - case MediaHolder.TYPE_STICKER_SECTION: { - Object obj = getObject(position); - ((StickerSectionView) holder.itemView).setSelectionFactor(selectedObject == obj ? 1f : 0f, false); - ((StickerSectionView) holder.itemView).setStickerSet((TGStickerSetInfo) obj); - break; + } else if (id == R.id.btn_addStickerSet) { + info.unsetIsTrendingEmoji(); + parentController.tdlib().client().send(new TdApi.ChangeStickerSet(info.getId(), true, false), parentController.tdlib().okHandler()); + } else if (id == R.id.btn_copyLink) { + TdApi.StickerSetInfo stickerSetInfo = info.getInfo(); + if (stickerSetInfo != null) { + String url = parentController.tdlib().tMeStickerSetUrl(stickerSetInfo); + UI.copyText(url, R.string.CopiedLink); } } - } - - @Override - public int getItemViewType (int position) { - if (position < headerItems.size()) { - return MediaHolder.TYPE_EMOJI_SECTION; - } else { - return MediaHolder.TYPE_STICKER_SECTION; - } - } + return true; + }); + } - @Override - public int getItemCount () { - return headerItems.size() + (stickerSets != null ? stickerSets.size() : 0); - } + public void removeStickerSet (final TGStickerSetInfo info) { + if (animatedEmojiOnly) return; - @Override - public void onViewAttachedToWindow (MediaHolder holder) { - switch (holder.getItemViewType()) { - case MediaHolder.TYPE_STICKER_SECTION: { - ((StickerSectionView) holder.itemView).attach(); - break; + if (themeProvider != null) { + themeProvider.showOptions(null, new int[] {R.id.btn_copyLink, R.id.btn_archive, R.id.more_btn_delete}, new String[] {Lang.getString(R.string.CopyLink), Lang.getString(R.string.ArchivePack), Lang.getString(R.string.DeletePack)}, new int[] {ViewController.OPTION_COLOR_NORMAL, ViewController.OPTION_COLOR_NORMAL, ViewController.OPTION_COLOR_RED}, new int[] {R.drawable.baseline_link_24, R.drawable.baseline_archive_24, R.drawable.baseline_delete_24}, (itemView, id) -> { + if (id == R.id.more_btn_delete) { + if (themeProvider != null) { + themeProvider.showOptions(Lang.getStringBold(R.string.RemoveStickerSet, info.getTitle()), new int[] {R.id.btn_delete, R.id.btn_cancel}, new String[] {Lang.getString(R.string.RemoveStickerSetAction), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_delete_24, R.drawable.baseline_cancel_24}, (resultItemView, resultId) -> { + if (resultId == R.id.btn_delete) { + parentController.tdlib().client().send(new TdApi.ChangeStickerSet(info.getId(), false, false), parentController.tdlib().okHandler()); + } + return true; + }); + } + } else if (id == R.id.btn_archive) { + if (themeProvider != null) { + themeProvider.showOptions(Lang.getStringBold(R.string.ArchiveStickerSet, info.getTitle()), new int[] {R.id.btn_delete, R.id.btn_cancel}, new String[] {Lang.getString(R.string.ArchiveStickerSetAction), Lang.getString(R.string.Cancel)}, new int[] {ViewController.OPTION_COLOR_RED, ViewController.OPTION_COLOR_NORMAL}, new int[] {R.drawable.baseline_archive_24, R.drawable.baseline_cancel_24}, (resultItemView, resultId) -> { + if (resultId == R.id.btn_delete) { + parentController.tdlib().client().send(new TdApi.ChangeStickerSet(info.getId(), false, true), parentController.tdlib().okHandler()); + } + return true; + }); + } + } else if (id == R.id.btn_copyLink) { + TdApi.StickerSetInfo stickerSetInfo = info.getInfo(); + if (stickerSetInfo != null) { + String url = parentController.tdlib().tMeStickerSetUrl(stickerSetInfo); + UI.copyText(url, R.string.CopiedLink); + } } - } + return true; + }); } + } - @Override - public void onViewDetachedFromWindow (MediaHolder holder) { - switch (holder.getItemViewType()) { - case MediaHolder.TYPE_STICKER_SECTION: { - ((StickerSectionView) holder.itemView).detach(); - break; - } - } - } + public boolean isUseDarkMode () { + return useDarkMode; + } - @Override - public void onViewRecycled (MediaHolder holder) { - if (holder.getItemViewType() == MediaHolder.TYPE_STICKER_SECTION) { - ((StickerSectionView) holder.itemView).performDestroy(); - } + private CircleButton circleButton; + + public void onEnterEmoji (String emoji) { + if (listener != null) { + listener.onEnterEmoji(emoji); } } private @Nullable ViewController themeProvider; private boolean allowMedia; private boolean animatedEmojiOnly; + private boolean classicEmojiOnly; + private boolean allowPremiumFeatures; private boolean useDarkMode; public EmojiToneHelper.Delegate getToneDelegate () { return parentController != null && parentController instanceof EmojiToneHelper.Delegate ? (EmojiToneHelper.Delegate) parentController : null; } + public boolean isAnimatedEmojiOnly () { + return animatedEmojiOnly; + } + public boolean useDarkMode () { return useDarkMode; } @@ -1174,21 +282,27 @@ public FrameLayoutFix getHeaderView () { } public void initWithEmojiStatus (ViewController context, @NonNull Listener listener, @Nullable ViewController themeProvider) { - initWithMediasEnabled(context, false, true, listener, themeProvider, false); + initWithMediasEnabled(context, false, true, listener, themeProvider, false, false); } public void initWithMediasEnabled (ViewController context, boolean allowMedia, @NonNull Listener listener, @Nullable ViewController themeProvider, boolean useDarkMode) { - initWithMediasEnabled(context, allowMedia, false, listener, themeProvider, useDarkMode); + initWithMediasEnabled(context, allowMedia, false, listener, themeProvider, useDarkMode, false); } - public void initWithMediasEnabled (ViewController context, boolean allowMedia, boolean animatedEmojiOnly, @NonNull Listener listener, @Nullable ViewController themeProvider, boolean useDarkMode) { + public int getEmojiSectionsSize () { + return emojiSectionsSize; + } + + public void initWithMediasEnabled (ViewController context, boolean allowMedia, boolean animatedEmojiOnly, @NonNull Listener listener, @Nullable ViewController themeProvider, boolean useDarkMode, boolean classicEmojiOnly) { this.parentController = context; this.listener = listener; this.themeProvider = themeProvider; this.allowMedia = allowMedia && !animatedEmojiOnly; this.animatedEmojiOnly = animatedEmojiOnly; + this.classicEmojiOnly = classicEmojiOnly; this.useDarkMode = useDarkMode; + /* this.emojiSections = new ArrayList<>(); this.emojiSections.add(new EmojiSection(this, 0, R.drawable.baseline_access_time_24, R.drawable.baseline_watch_later_24).setFactor(1f, false).setMakeFirstTransparent().setOffsetHalf(false)); this.emojiSections.add(new EmojiSection(this, 1, R.drawable.baseline_emoticon_outline_24, R.drawable.baseline_emoticon_24).setMakeFirstTransparent()); @@ -1199,10 +313,13 @@ public void initWithMediasEnabled (ViewController context, boolean allowMedia this.emojiSections.add(new EmojiSection(this, 6, R.drawable.deproko_baseline_flag_outline_24, R.drawable.deproko_baseline_flag_filled_24).setMakeFirstTransparent()); if (allowMedia) { - this.emojiSections.add(new EmojiSection(this, 7, R.drawable.deproko_baseline_stickers_24, /*R.drawable.ic_gif*/ 0).setActiveDisabled().setOffsetHalf(true)); + this.emojiSections.add(new EmojiSection(this, 7, R.drawable.deproko_baseline_stickers_24, 0).setActiveDisabled().setOffsetHalf(true)); } else { this.emojiSections.get(this.emojiSections.size() - 1).setOffsetHalf(true); } + */ + + emojiSectionsSize = 7 + (allowMedia ? 1 : 0); adapter = new Adapter(context, this, allowMedia, themeProvider); pager = new RtlViewPager(getContext()); @@ -1229,46 +346,34 @@ public boolean onTouchEvent (MotionEvent event) { // Emoji sections if (!animatedEmojiOnly) { - emojiSectionsView = new FrameLayoutFix(getContext()); - emojiSectionsView.setPadding(getHorizontalPadding(), 0, getHorizontalPadding(), 0); - emojiSectionsView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, headerSize)); + ArrayList emojiSections = new ArrayList<>(2); + emojiSections.add(new EmojiSection(this, EmojiSection.SECTION_EMOJI_TRENDING, R.drawable.outline_whatshot_24, R.drawable.baseline_whatshot_24).setMakeFirstTransparent()); + emojiSections.add(new EmojiSection(this, EmojiSection.SECTION_EMOJI_RECENT, R.drawable.baseline_access_time_24, R.drawable.baseline_watch_later_24)/*.setFactor(1f, false)*/.setMakeFirstTransparent().setOffsetHalf(false)); - for (EmojiSection section : emojiSections) { - EmojiSectionView sectionView = new EmojiSectionView(getContext()); - if (themeProvider != null) { - themeProvider.addThemeInvalidateListener(sectionView); - } - sectionView.setId(R.id.btn_section); - sectionView.setNeedTranslate(); - sectionView.setOnClickListener(this); - sectionView.setOnLongClickListener(this); - sectionView.setSection(section); - sectionView.setItemCount(emojiSections.size()); - sectionView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); - emojiSectionsView.addView(sectionView); - } + ArrayList expandableSections = new ArrayList<>(6); + expandableSections.add(new EmojiSection(this, EmojiSection.SECTION_EMOJI_SMILEYS, R.drawable.baseline_emoticon_outline_24, R.drawable.baseline_emoticon_24).setMakeFirstTransparent()); + expandableSections.add(new EmojiSection(this, EmojiSection.SECTION_EMOJI_ANIMALS, R.drawable.deproko_baseline_animals_outline_24, R.drawable.deproko_baseline_animals_24));/*.setIsPanda(!useDarkMode)*/ + expandableSections.add(new EmojiSection(this, EmojiSection.SECTION_EMOJI_FOOD, R.drawable.baseline_restaurant_menu_24, R.drawable.baseline_restaurant_menu_24)); + expandableSections.add(new EmojiSection(this, EmojiSection.SECTION_EMOJI_TRAVEL, R.drawable.baseline_directions_car_24, R.drawable.baseline_directions_car_24)); + expandableSections.add(new EmojiSection(this, EmojiSection.SECTION_EMOJI_SYMBOLS, R.drawable.deproko_baseline_lamp_24, R.drawable.deproko_baseline_lamp_filled_24)); + expandableSections.add(new EmojiSection(this, EmojiSection.SECTION_EMOJI_FLAGS, R.drawable.deproko_baseline_flag_outline_24, R.drawable.deproko_baseline_flag_filled_24).setMakeFirstTransparent()); - headerView.addView(emojiSectionsView); + emojiHeaderView = new EmojiHeaderView(getContext(), this, themeProvider, emojiSections, expandableSections, allowMedia); + emojiHeaderView.setSectionsOnClickListener(this); + emojiHeaderView.setSectionsOnLongClickListener(this::onEmojiHeaderLongClick); + checkAllowPremiumFeatures(); + headerView.addView(emojiHeaderView); } // Media sections if (allowMedia || animatedEmojiOnly) { - mediaSectionsView = new RecyclerView(getContext()); - mediaSectionsView.setHasFixedSize(true); - mediaSectionsView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180)); - mediaSectionsView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? OVER_SCROLL_IF_CONTENT_SCROLLS :OVER_SCROLL_NEVER); - mediaSectionsView.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, Lang.rtl())); - mediaSectionsView.setPadding(getHorizontalPadding(), 0, getHorizontalPadding(), 0); - mediaSectionsView.setClipToPadding(false); - mediaSectionsView.setAdapter(mediaAdapter = new MediaAdapter(getContext(), this, this, animatedEmojiOnly ? 8: emojiSections.size(), !animatedEmojiOnly && Settings.instance().getEmojiMediaSection() == EmojiMediaType.GIF, themeProvider, animatedEmojiOnly)); - mediaSectionsView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, headerSize)); - + mediaSectionsView = new MediaHeaderView(getContext()); + mediaSectionsView.init(this, themeProvider, this); headerView.addView(mediaSectionsView); } else { mediaSectionsView = null; - mediaAdapter = null; } // Shadow and etc @@ -1317,14 +422,29 @@ public boolean onTouchEvent (MotionEvent event) { addView(shadowView); addView(circleButton); + checkBackground(); + // NewEmoji.instance().loadAllEmoji(); + + setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + private void checkBackground () { if (useDarkMode) { setBackgroundColor(Theme.getColor(ColorId.chatKeyboard, ThemeId.NIGHT_BLACK)); } else { - ViewSupport.setThemedBackground(this, ColorId.chatKeyboard, themeProvider); + ViewSupport.setThemedBackground(this, isOptimizedForDisplayMessageOptionsWindow ? ColorId.filling : ColorId.chatKeyboard, themeProvider); } - // NewEmoji.instance().loadAllEmoji(); + } - setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + public void setAllowPremiumFeatures (boolean allowPremiumFeatures) { + this.allowPremiumFeatures = allowPremiumFeatures; + checkAllowPremiumFeatures(); + } + + private void checkAllowPremiumFeatures () { + if (emojiHeaderView != null && parentController != null) { + emojiHeaderView.setIsPremium((allowPremiumFeatures || parentController.tdlib().hasPremium()) && !classicEmojiOnly, false); + } } public void onTextChanged (CharSequence charSequence) { @@ -1357,17 +477,24 @@ public void setCircleVisible (boolean isVisible, boolean isSearch) { } } + private boolean isOptimizedForDisplayMessageOptionsWindow; + + public void optimizeForDisplayMessageOptionsWindow (boolean needOptimize) { + isOptimizedForDisplayMessageOptionsWindow = needOptimize; + optimizeForDisplayTextFormattingLayout(needOptimize); + checkBackground(); + } + public void optimizeForDisplayTextFormattingLayout (boolean needOptimize) { - int visibility = needOptimize ? VISIBLE : GONE; - // if (headerView != null) headerView.setVisibility(visibility); + int visibility = needOptimize ? GONE : VISIBLE; + if (headerView != null) headerView.setVisibility(needOptimize ? INVISIBLE : VISIBLE); if (shadowView != null) shadowView.setVisibility(visibility); if (pager != null) pager.setVisibility(visibility); if (circleButton != null) circleButton.setVisibility(visibility); } public int getCurrentItem () { - int p = pager.getCurrentItem(); - return p; + return pager.getCurrentItem(); } private void setCircleFactor (float toFactor, boolean animated) { @@ -1378,10 +505,10 @@ private void setCircleFactor (float toFactor, boolean animated) { if (toFactor == 1f && circleFactor == 0f) { circleAnimator.setInterpolator(AnimatorUtils.OVERSHOOT_INTERPOLATOR); - circleAnimator.setDuration(210l); + circleAnimator.setDuration(210L); } else { circleAnimator.setInterpolator(AnimatorUtils.DECELERATE_INTERPOLATOR); - circleAnimator.setDuration(100l); + circleAnimator.setDuration(100L); } circleAnimator.animateTo(toFactor); @@ -1414,15 +541,11 @@ private void updateCircleStyles () { } public void setShowRecents (boolean showRecents) { - mediaAdapter.setHasRecents(showRecents); + mediaSectionsView.setShowRecents(showRecents); } public void setShowFavorite (boolean showFavorite) { - mediaAdapter.setHasFavorite(showFavorite); - } - - public void setHasNewHots (boolean hasHots) { - mediaAdapter.setHasNewHots(hasHots); + mediaSectionsView.setShowFavorite(showFavorite); } public void setStickerSets (ArrayList stickers, boolean showFavorite, boolean showRecents) { @@ -1430,15 +553,17 @@ public void setStickerSets (ArrayList stickers, boolean showFa } public void setStickerSets (ArrayList stickers, boolean showFavorite, boolean showRecents, boolean showTrending, boolean isFound) { - mediaAdapter.setHasFavorite(showFavorite); - mediaAdapter.setHasRecents(showRecents); - mediaAdapter.setShowRecentsAsFound(isFound); - mediaAdapter.setHasTrending(showTrending); - mediaAdapter.setStickerSets(stickers); + mediaSectionsView.setStickerSets(stickers, showFavorite, showRecents, showTrending, isFound); + } + + public void setEmojiPacks (ArrayList stickers) { + if (emojiHeaderView != null) { + emojiHeaderView.setStickerSets(stickers); + } } public void invalidateStickerSets () { - mediaAdapter.notifyDataSetChanged(); + mediaSectionsView.invalidateStickerSets(); } private void scrollToStickerSet (@NonNull TGStickerSetInfo stickerSet) { @@ -1449,6 +574,16 @@ private void scrollToStickerSet (@NonNull TGStickerSetInfo stickerSet) { } return; } + + if (stickerSet.isEmoji()) { + ViewController c = adapter.getCachedItem(0); + if (c != null) { + ((EmojiListController) c).showStickerSet(stickerSet); + } + return; + + } + ViewController c = adapter.getCachedItem(1); if (c != null) { ((EmojiMediaListController) c).showStickerSet(stickerSet); @@ -1462,14 +597,23 @@ private void scrollToEmojiSection (int sectionIndex) { } } - public boolean setEmojiStatus (View view, TGStickerObj sticker, int duration) { - return listener != null && listener.onSetEmojiStatus(view, sticker, duration); + public boolean setEmojiStatus (View view, TGStickerObj sticker, long expirationDate) { + return listener != null && listener.onSetEmojiStatus(view, sticker, new TdApi.EmojiStatus(sticker.getCustomEmojiId(), (int) expirationDate)); } public boolean sendSticker (View view, TGStickerObj sticker, TdApi.MessageSendOptions sendOptions) { return listener != null && listener.onSendSticker(view, sticker, sendOptions); } + public void onEnterCustomEmoji (TGStickerObj sticker) { + if (!sticker.isRecent()) { + Emoji.instance().saveRecentCustomEmoji(sticker.getCustomEmojiId()); + } + if (listener != null) { + listener.onEnterCustomEmoji(sticker); + } + } + public long findOutputChatId () { return listener != null ? listener.getOutputChatId() : 0; } @@ -1478,35 +622,18 @@ public boolean sendGif (View view, TdApi.Animation animation) { return listener != null && listener.onSendGIF(view, animation); } - public void resetScrollState () { - resetScrollState(false); - } - - public void resetScrollState (boolean silent) { - switch (pager.getCurrentItem()) { - case 0: { - ViewController c = adapter.getCachedItem(0); - if (c != null && !animatedEmojiOnly) { - resetScrollingCache(((EmojiListController) c).getCurrentScrollY(), silent); - } - break; - } - case 1: { - ViewController c = adapter.getCachedItem(1); - if (c != null) { - resetScrollingCache(((EmojiMediaListController) c).getCurrentScrollY(), silent); - } - break; - } - } - } - - @Override - public boolean onLongClick (View v) { + public boolean onEmojiHeaderLongClick (View v) { int viewId = v.getId(); - if (viewId == R.id.btn_section) { + + if (v instanceof StickerSectionView) { + StickerSectionView sectionView = (StickerSectionView) v; + TGStickerSetInfo info = sectionView.getStickerSet(); + removeStickerSet(info); + return true; + } else if (viewId == R.id.btn_section) { EmojiSection section = ((EmojiSectionView) v).getSection(); - if (emojiSections.get(0) == section && Emoji.instance().canClearRecents()) { + + if (section.index == 0 && Emoji.instance().canClearRecents()) { clearRecentEmoji(); return true; } @@ -1551,23 +678,26 @@ public void onClick (View v) { if (animatedEmojiOnly) { ViewController c = adapter.getCachedItem(0); if (c != null) { - if (section.isTrending) { + if (section.isTrending()) { ((EmojiStatusListController) c).scrollToTrendingStickers(true); } else { ((EmojiStatusListController) c).scrollToSystemStickers(true); } } } else if (section.index >= 0) { - if (allowMedia && section.index == emojiSections.size() - 1) { + scrollToEmojiSection(section.index); + newSection = EmojiMediaType.EMOJI; + } else { + if (section.index == EmojiSection.SECTION_EMOJI_TRENDING) { + ViewController c = adapter.getCachedItem(0); + if (c instanceof EmojiListController) { + ((EmojiListController) c).showTrending(); + } + } else if (section.index == EmojiSection.SECTION_SWITCH_TO_MEDIA) { pager.setCurrentItem(1, true); newSection = getCurrentMediaEmojiSection(); - } else { - scrollToEmojiSection(section.index); - newSection = EmojiMediaType.EMOJI; } - } else { int index = -(section.index) - 1; - switch (index) { case 0: { pager.setCurrentItem(0, true); @@ -1634,8 +764,8 @@ public void setHeaderHideFactor (float factor, float offset) { headerView.setTranslationY(y + offset); shadowView.setTranslationY(y + offset); float alpha = 1f - AnimatorUtils.DECELERATE_INTERPOLATOR.getInterpolation(Math.max(0f, Math.min(1f, factor / .5f))); - if (emojiSectionsView != null) { - emojiSectionsView.setAlpha(alpha); + if (emojiHeaderView != null) { + emojiHeaderView.setAlpha(alpha); } if (mediaSectionsView != null) { mediaSectionsView.setAlpha(alpha); @@ -1650,11 +780,11 @@ public int getHeaderBottom () { private void showOrHideHeader () { if (headerHideFactor != 0f && headerHideFactor != 1f) { float hideFactor = headerHideFactor > .25f && lastY - getHeaderSize() > 0 ? 1f : 0f; - moveHeader(hideFactor, true); + moveHeaderImpl(hideFactor, true); } } - private void moveHeader (float factor, boolean animated) { + private void moveHeaderImpl (float factor, boolean animated) { if (factor == 1f) { lastHeaderVisibleY = Math.max(0, lastY - getHeaderSize()); } else { @@ -1669,7 +799,7 @@ private void moveHeader (float factor, boolean animated) { public void setHeaderHideFactor (float factor, boolean animated) { if (animated) { if (hideAnimator == null) { - hideAnimator = new FactorAnimator(HIDE_ANIMATOR, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 210l, headerHideFactor); + hideAnimator = new FactorAnimator(HIDE_ANIMATOR, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 210L, headerHideFactor); } hideAnimator.animateTo(factor); } else { @@ -1724,14 +854,14 @@ public void setIgnoreMovement (boolean ignoreMovement) { if (ignoreMovement) { ignoreFirstScrollEvent = true; } else { - resetScrollState(); + resetScrollState(false); } } } public void moveHeaderFull (int y) { if (ignoreFirstScrollEvent) { - resetScrollState(); + resetScrollState(false); ignoreFirstScrollEvent = false; return; } @@ -1744,10 +874,10 @@ public void moveHeaderFull (int y) { setCircleVisible(headerHideFactor == 0f, true); } - private void moveHeader (int y) { + private void moveHeaderImpl (int y) { lastY = y; if (ignoreFirstScrollEvent) { - resetScrollState(); + resetScrollState(false); ignoreFirstScrollEvent = false; return; } @@ -1782,22 +912,6 @@ public void setIsScrolling (boolean isScrolling) { } } - public void onScroll (int totalDy) { - moveHeader(totalDy); - } - - public void onSectionInteracted (@EmojiMediaType int mediaType, boolean interactionFinished) { - if (listener != null) { - listener.onSectionInteracted(this, mediaType, interactionFinished); - } - } - - public void onSectionScroll (@EmojiMediaType int mediaType, boolean moved) { - if (moved) { - onSectionInteracted(mediaType, false); - } - } - public void putCachedItem (ViewController c, int position) { adapter.cachedItems.put(position, c); } @@ -1833,7 +947,7 @@ public void updateCachedItemsSpanCounts () { ((EmojiStatusListController) c).checkSpanCount(); } } - parent.resetScrollState(); + parent.resetScrollState(false); } public void invalidateCachedItems () { @@ -1873,7 +987,7 @@ public Object instantiateItem (@NonNull ViewGroup container, int position) { mediaListController.setArguments(parent); c = mediaListController; } else { - EmojiListController emojiListController = new EmojiListController(context.context(), context.tdlib()); + EmojiListController emojiListController = new EmojiListController(context.context(), context.tdlib(), parent.classicEmojiOnly); emojiListController.setArguments(parent); c = emojiListController; } @@ -1925,9 +1039,9 @@ private boolean hasRightButton () { } private void updatePositions () { - float currentPageFactor = animatedEmojiOnly ? 1f: this.currentPageFactor; - if (emojiSectionsView != null) { - emojiSectionsView.setTranslationX((float) (emojiSectionsView.getMeasuredWidth()) * currentPageFactor * (Lang.rtl() ? 1f : -1f)); + float currentPageFactor = animatedEmojiOnly ? 1f : this.currentPageFactor; + if (emojiHeaderView != null) { + emojiHeaderView.setTranslationX((float) (emojiHeaderView.getMeasuredWidth()) * currentPageFactor * (Lang.rtl() ? 1f : -1f)); } if (mediaSectionsView != null) { mediaSectionsView.setTranslationX(mediaSectionsView.getMeasuredWidth() * (1f - currentPageFactor) * (Lang.rtl() ? -1f : 1f)); @@ -1940,7 +1054,7 @@ public void onPageScrolled (int position, float positionOffset, int positionOffs if (affectHeight) { float factor = fromHeightHideFactor + Math.abs(fromPageFactor - currentPageFactor) * heightFactorDiff; - moveHeader(factor, false); + moveHeaderImpl(factor, false); } } @@ -1958,7 +1072,7 @@ public void onPageSelected (int position) { } else if (hasLeft || hasRight) { setCircleVisible((hasLeft && position == 0) || (hasRight && position == 1), true, position == 0 ? R.drawable.baseline_backspace_24 : R.drawable.baseline_search_24, position == 0 ? -Screen.dp(BACKSPACE_OFFSET) : 0); } - resetScrollState(); + resetScrollState(false); } private boolean affectHeight; @@ -2007,7 +1121,7 @@ public void setListener (@Nullable Listener listener) { @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(forceHeight > 0 ? forceHeight: Keyboard.getSize(), MeasureSpec.EXACTLY)); + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(forceHeight > 0 ? forceHeight : Keyboard.getSize(), MeasureSpec.EXACTLY)); checkWidth(getMeasuredWidth()); } @@ -2057,8 +1171,8 @@ public Listener getListener () { } public void setMediaSection (boolean isGif) { - if (emojiSections.size() > 7) { - emojiSections.get(7).changeIcon(isGif ? R.drawable.deproko_baseline_gif_24 : R.drawable.deproko_baseline_stickers_24); + if (emojiHeaderView != null) { + emojiHeaderView.setMediaSection(isGif); } } @@ -2092,6 +1206,10 @@ public void reset () { if (c != null) { ((EmojiMediaListController) c).applyScheduledChanges(); } + ViewController c2 = adapter.getCachedItem(0); + if (c2 instanceof EmojiListController) { + ((EmojiListController) c2).applyScheduledChanges(); + } } public void destroy () { @@ -2163,4 +1281,120 @@ public void onLanguagePackEvent (int event, int arg1) { } } } + + + /* Interface */ + + public static final @IdRes int STICKERS_INSTALLED_CONTROLLER_ID = R.id.controller_emojiLayoutStickers; + public static final @IdRes int STICKERS_TRENDING_CONTROLLER_ID = R.id.controller_emojiLayoutStickersTrending; + public static final @IdRes int EMOJI_INSTALLED_CONTROLLER_ID = R.id.controller_emojiLayoutEmoji; + public static final @IdRes int EMOJI_TRENDING_CONTROLLER_ID = R.id.controller_emojiLayoutEmojiTrending; + + public static @EmojiMediaType int getEmojiMediaType (int controllerId) { + return controllerId == R.id.controller_emojiLayoutEmojiTrending + || controllerId == R.id.controller_emojiLayoutEmoji ? EmojiMediaType.EMOJI : EmojiMediaType.STICKER; + } + + @Override + public void onAddStickerSection (@IdRes int controllerId, int section, TGStickerSetInfo info) { + if (controllerId == EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID && mediaSectionsView != null) { + mediaSectionsView.addStickerSection(section, info); + } else if (controllerId == EmojiLayout.EMOJI_INSTALLED_CONTROLLER_ID && emojiHeaderView != null) { + emojiHeaderView.addStickerSection(section, info); + } + } + + @Override + public void onMoveStickerSection (@IdRes int controllerId, int fromSection, int toSection) { + if (controllerId == EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID && mediaSectionsView != null) { + mediaSectionsView.moveStickerSection(fromSection, toSection); + } else if (controllerId == EmojiLayout.EMOJI_INSTALLED_CONTROLLER_ID && emojiHeaderView != null) { + emojiHeaderView.moveStickerSection(fromSection, toSection); + } + } + + @Override + public void onRemoveStickerSection (@IdRes int controllerId, int section) { + if (controllerId == EmojiLayout.STICKERS_INSTALLED_CONTROLLER_ID && mediaSectionsView != null) { + mediaSectionsView.removeStickerSection(section); + } else if (controllerId == EmojiLayout.EMOJI_INSTALLED_CONTROLLER_ID && emojiHeaderView != null) { + emojiHeaderView.removeStickerSection(section); + } + } + + @Override + public void setCurrentStickerSectionByPosition (@IdRes int controllerId, int i, boolean isStickerSection, boolean animated) { + if (controllerId == R.id.controller_emojiLayoutStickers && mediaSectionsView != null) { + mediaSectionsView.setCurrentStickerSectionByPosition(i, isStickerSection, animated); + } else if (controllerId == EMOJI_INSTALLED_CONTROLLER_ID && emojiHeaderView != null) { + emojiHeaderView.setCurrentStickerSectionByPosition(i + (isStickerSection ? 1: 0), animated); + } + } + + @Override + public boolean onStickerClick (@IdRes int controllerId, StickerSmallView view, View clickView, TGStickerSetInfo stickerSet, TGStickerObj sticker, boolean isMenuClick, TdApi.MessageSendOptions sendOptions) { + if (sticker.isTrending() && !isMenuClick) { + if (stickerSet != null) { + stickerSet.show(parentController); + return true; + } + return false; + } else if (sticker.isCustomEmoji()) { + onEnterCustomEmoji(sticker); + return true; + } else { + return sendSticker(clickView, sticker, sendOptions); + } + } + + @Override + public boolean canFindChildViewUnder (int controllerId, StickerSmallView view, int recyclerX, int recyclerY) { + return recyclerY > getHeaderBottom(); + } + + public void setHasNewHots (@IdRes int controllerId, boolean hasHots) { + if (controllerId == STICKERS_TRENDING_CONTROLLER_ID && mediaSectionsView != null) { + mediaSectionsView.setHasNewHots(hasHots); + } + } + + @Override + public void onSectionInteracted (@EmojiMediaType int mediaType, boolean interactionFinished) { + if (listener != null) { + listener.onSectionInteracted(this, mediaType, interactionFinished); + } + } + + @Override + public void onSectionInteractedScroll (@EmojiMediaType int mediaType, boolean moved) { + if (moved) { + onSectionInteracted(mediaType, false); + } + } + + @Override + public void moveHeader (int totalDy) { + moveHeaderImpl(totalDy); + } + + @Override + public void resetScrollState (boolean silent) { + switch (pager.getCurrentItem()) { + case 0: { + ViewController c = adapter.getCachedItem(0); + if (c != null && !animatedEmojiOnly) { + resetScrollingCache(((EmojiListController) c).getCurrentScrollY(), silent); + } + break; + } + case 1: { + ViewController c = adapter.getCachedItem(1); + if (c != null) { + resetScrollingCache(((EmojiMediaListController) c).getCurrentScrollY(), silent); + } + break; + } + } + } + } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/EmojiPacksInfoView.java b/app/src/main/java/org/thunderdog/challegram/widget/EmojiPacksInfoView.java new file mode 100644 index 0000000000..6be4d33ac3 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/EmojiPacksInfoView.java @@ -0,0 +1,129 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 18/08/2023 + */ +package org.thunderdog.challegram.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.style.ClickableSpan; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.Log; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.util.text.FormattedText; +import org.thunderdog.challegram.util.text.TextEntity; + +import me.vkryl.td.Td; + +@SuppressLint("ViewConstructor") +public class EmojiPacksInfoView extends CustomTextView { + private final ViewController parent; + private @Nullable TdApi.StickerSetInfo lastInfo; + private int key = 0; + private long[] emojiPacksIds; + + public EmojiPacksInfoView (Context context, ViewController parent, Tdlib tdlib) { + super(context, tdlib); + this.parent = parent; + + setTextColorId(ColorId.textLight); + setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(40f))); + setPadding(Screen.dp(16f), Screen.dp(14f), Screen.dp(16f), Screen.dp(6f)); + } + + public void update (long firstEmojiId, long[] emojiPacksIds, ClickableSpan onClickListener, boolean animated) { + this.emojiPacksIds = emojiPacksIds; + boolean isSingle = emojiPacksIds.length == 1; + + if (isSingle && (lastInfo == null || lastInfo.id != emojiPacksIds[0])) { + lastInfo = null; + this.key += 1; + + final int key = this.key; + parent.tdlib().client().send(new TdApi.GetStickerSet(emojiPacksIds[0]), (obj) -> { + if (obj.getConstructor() != TdApi.StickerSet.CONSTRUCTOR) return; + UI.post(() -> { + if (this.key != key) return; + this.lastInfo = Td.toStickerSetInfo((TdApi.StickerSet) obj); + updateImpl(firstEmojiId, emojiPacksIds.length, onClickListener, lastInfo, false); + }); + }); + } + + updateImpl(firstEmojiId, emojiPacksIds.length, onClickListener, lastInfo, animated); + } + + public long[] getEmojiPacksIds () { + return emojiPacksIds; + } + + private void updateImpl (long firstEmojiId, int emojiPacksCount, ClickableSpan onClickListener, @Nullable TdApi.StickerSetInfo info, boolean animated) { + boolean isSingle = emojiPacksCount == 1; + + String link; + if (isSingle) { + link = Lang.getString(R.string.xEmojiPacksEmojiSingle, + info != null ? info.title : Lang.getString(R.string.LoadingMessageEmojiPack) + ); + } else { + link = Lang.plural(R.string.xEmojiPacks, emojiPacksCount);; + } + String text = Lang.getString(isSingle ? R.string.EmojiUsedFromSingle : R.string.EmojiUsedFromX, link); + + // FIXME: do not rely on cloud strings here + final int linkStart = text.indexOf(link); + final int emojiStart = text.indexOf("*"); + + try { + final TdApi.FormattedText formattedTextRaw; + if (!isSingle || emojiStart == -1) { + formattedTextRaw = new TdApi.FormattedText(text, new TdApi.TextEntity[]{ + new TdApi.TextEntity(linkStart, link.length(), new TdApi.TextEntityTypeUrl()) + }); + } else { + formattedTextRaw = new TdApi.FormattedText(text, emojiStart >= linkStart ? new TdApi.TextEntity[]{ + new TdApi.TextEntity(linkStart, link.length(), new TdApi.TextEntityTypeUrl()), + new TdApi.TextEntity(emojiStart, 1, new TdApi.TextEntityTypeCustomEmoji(firstEmojiId)) + }: new TdApi.TextEntity[]{ + new TdApi.TextEntity(emojiStart, 1, new TdApi.TextEntityTypeCustomEmoji(firstEmojiId)), + new TdApi.TextEntity(linkStart, link.length(), new TdApi.TextEntityTypeUrl()) + }); + } + + FormattedText formattedText = FormattedText.valueOf(parent, formattedTextRaw, null); + if (formattedText.entities != null) { + for (TextEntity entity : formattedText.entities) { + entity.setOnClickListener(onClickListener); + if (!entity.isCustomEmoji()) { + entity.makeBold(true); + } + } + } + + setText(text, formattedText.entities, animated); + } catch (Throwable t) { + Log.e("Cannot get string", t); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/FileProgressComponent.java b/app/src/main/java/org/thunderdog/challegram/widget/FileProgressComponent.java index edf6baa614..c5cb3d2485 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/FileProgressComponent.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/FileProgressComponent.java @@ -403,7 +403,7 @@ public void setFile (@Nullable TdApi.File file, @Nullable TdApi.Message message) this.file = file; if (file != null && file.local != null) { this.isDownloaded = file.local.isDownloadingCompleted; - this.useGenerationProgress = !file.local.isDownloadingCompleted && !file.remote.isUploadingCompleted && message != null && message.content.getConstructor() != TdApi.MessagePhoto.CONSTRUCTOR; + this.useGenerationProgress = !file.local.isDownloadingCompleted && !file.remote.isUploadingCompleted && message != null && !Td.isPhoto(message.content); } else { this.isDownloaded = this.useGenerationProgress = false; } @@ -1483,7 +1483,7 @@ public void draw (T view, final Canvas c) { drawPlayPause(c, cx, cy, playPauseAlpha, true); } else if (currentBitmapRes != 0 && (currentBitmapRes != downloadedIconRes || !hideDownloadedIcon) && !(isVideoStreaming() && isVideoStreamingCloudNeeded)) { boolean ignoreScale = isVideoStreaming() && !isVideoStreamingSmallUi() && vsOnDownloadedAnimator != null && vsOnDownloadedAnimator.isAnimating(); - Paint bitmapPaint = Paints.getPorterDuffPaint(0xffffffff); + Paint bitmapPaint = Paints.whitePorterDuffPaint(); final float initScaleFactor = bitmapChangeFactor <= .5f ? (bitmapChangeFactor / .5f) : (1f - (bitmapChangeFactor - .5f) / .5f); final float scaleFactor = (ignoreScale) ? 0f : initScaleFactor; diff --git a/app/src/main/java/org/thunderdog/challegram/widget/ForceTouchView.java b/app/src/main/java/org/thunderdog/challegram/widget/ForceTouchView.java index eb7e4209e5..0b8b388d4a 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/ForceTouchView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/ForceTouchView.java @@ -14,8 +14,6 @@ */ package org.thunderdog.challegram.widget; -import static java.lang.annotation.RetentionPolicy.SOURCE; - import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; @@ -63,7 +61,9 @@ import org.thunderdog.challegram.telegram.MessageThreadListener; import org.thunderdog.challegram.telegram.NotificationSettingsListener; import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibAccount; import org.thunderdog.challegram.telegram.TdlibCache; +import org.thunderdog.challegram.telegram.TdlibManager; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.ColorState; import org.thunderdog.challegram.theme.Theme; @@ -80,6 +80,7 @@ import org.thunderdog.challegram.util.text.Text; import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import me.vkryl.android.AnimatorUtils; @@ -314,10 +315,20 @@ public void initWithContext (ForceTouchContext context) { headerView.setIgnoreCustomHeight(); headerView.setInnerMargins(Screen.dp(8f), Screen.dp(8f)); headerView.setTextColors(Theme.textAccentColor(), Theme.textDecentColor()); - if (context.boundDataType == TYPE_CHAT && context.boundDataId != 0) { - setupChat(context.boundDataId, (ThreadInfo) context.boundArg1, headerView); - } else if (context.boundDataType == TYPE_USER && context.boundDataId != 0) { - setupUser((int) context.boundDataId, headerView); + if (context.boundDataType != 0 && context.boundDataId != 0) { + switch (context.boundDataType) { + case DataType.CHAT: + setupChat(context.boundDataId, (ThreadInfo) context.boundArg1, headerView); + break; + case DataType.USER: + setupUser(context.boundDataId, headerView); + break; + case DataType.ACCOUNT: + setupAccount((int) context.boundDataId, headerView); + break; + default: + throw new UnsupportedOperationException(); + } } else { if (context.avatarSender != null) { headerView.getAvatarReceiver().requestMessageSender(tdlib, context.avatarSender, AvatarReceiver.Options.NONE); @@ -439,7 +450,7 @@ public int getOpacity () { View offsetView; - final int offsetWeight = context.shrunkenFooter ? 4: 1; + final int offsetWeight = context.shrunkenFooter ? 4 : 1; if (context.actionItems.size() > 1) { offsetView = new View(getContext()); @@ -472,10 +483,10 @@ public int getOpacity () { view = new ImageView(getContext()) { @Override protected void onDraw (Canvas c) { - c.save(); + final int restoreToCount = Views.save(c); c.scale(-1f, 1f, getMeasuredWidth() / 2f, getMeasuredHeight() / 2f); super.onDraw(c); - c.restore(); + Views.restore(c, restoreToCount); } }; } else if (actionItem.messageSender != null && actionItem.iconRes == 0) { @@ -487,10 +498,10 @@ protected void onDraw (Canvas c) { @Override protected void onDraw (Canvas c) { super.onDraw(c); - c.save(); + final int restoreToCount = Views.save(c); c.translate((getMeasuredWidth() - receiver.getWidth()) / 2f, (getMeasuredHeight() - receiver.getHeight()) / 2f); receiver.draw(c); - c.restore(); + Views.restore(c, restoreToCount); } }; receiver.setUpdateListener(r -> view.invalidate()); @@ -999,14 +1010,18 @@ public static int getMatchParentHorizontalMargin () { private static final float MAXIMIZE_FACTOR = 1.3f; - private static final int TYPE_NONE = 0; - private static final int TYPE_CHAT = 1; - private static final int TYPE_USER = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DataType.CHAT, DataType.USER, DataType.ACCOUNT + }) + private @interface DataType { + int CHAT = 1, USER = 2, ACCOUNT = 3; + } // Context public static class ForceTouchContext { - @Retention(SOURCE) + @Retention(RetentionPolicy.SOURCE) @IntDef({ANIMATION_TYPE_SCALE, ANIMATION_TYPE_EXPAND_VERTICALLY}) public @interface AnimationType {} @@ -1031,7 +1046,7 @@ public static class ForceTouchContext { private String title, subtitle; - private int boundDataType; + private @DataType int boundDataType; private long boundDataId; private @Nullable Object boundArg1; @@ -1166,7 +1181,7 @@ public void setHeader (String title, String subtitle) { public void setBoundChatId (long chatId, @Nullable ThreadInfo messageThread) { this.needHeader = true; this.needHeaderAvatar = true; - this.boundDataType = TYPE_CHAT; + this.boundDataType = DataType.CHAT; this.boundDataId = chatId; this.boundArg1 = messageThread; } @@ -1174,11 +1189,19 @@ public void setBoundChatId (long chatId, @Nullable ThreadInfo messageThread) { public void setBoundUserId (long userId) { this.needHeader = userId != 0; this.needHeaderAvatar = true; - this.boundDataType = TYPE_USER; + this.boundDataType = DataType.USER; this.boundDataId = userId; this.boundArg1 = 0; } + public void setBoundAccountId (int accountId) { + this.needHeader = accountId != TdlibAccount.NO_ID; + this.needHeaderAvatar = true; + this.boundDataType = DataType.ACCOUNT; + this.boundDataId = accountId; + this.boundArg1 = 0; + } + public void setHeaderAvatar (TdApi.MessageSender avatarSender, AvatarPlaceholder.Metadata avatarPlaceholder) { this.needHeaderAvatar = true; this.avatarSender = avatarSender; @@ -1216,18 +1239,35 @@ public boolean needHideKeyboard () { // Header - private int boundDataType; + private @DataType int boundDataType; private TdApi.User boundUser; private TdApi.Chat boundChat; + private TdlibAccount boundAccount; private ThreadInfo boundMessageThread; - private void setupUser (int userId, ComplexHeaderView headerView) { + private void setupUser (long userId, ComplexHeaderView headerView) { TdApi.User user = tdlib.cache().user(userId); if (user == null) { throw new NullPointerException(); } - this.boundDataType = TYPE_USER; + this.boundDataType = DataType.USER; + this.boundUser = user; + addUserListeners(user, true); + + setHeaderUser(user); + } + + private void setupAccount (int accountId, ComplexHeaderView headerView) { + TdlibAccount account = TdlibManager.instanceForAccountId(accountId).account(accountId); + TdApi.User user = account.getUser(); + if (user == null) { + // TODO: it's possible to support, but there's no need, + // as it's possible to just wait for myUser to load before opening the preview + throw new UnsupportedOperationException(); + } + + this.boundDataType = DataType.USER; this.boundUser = user; addUserListeners(user, true); @@ -1249,7 +1289,7 @@ private void setupChat (long chatId, @Nullable ThreadInfo messageThread, Complex throw new NullPointerException(); } - this.boundDataType = TYPE_CHAT; + this.boundDataType = DataType.CHAT; this.boundChat = chat; this.boundMessageThread = messageThread; addChatListeners(chat, messageThread, true); @@ -1271,18 +1311,24 @@ private void setupChat (long chatId, @Nullable ThreadInfo messageThread, Complex private void setChatAvatar () { if (!isDestroyed) { switch (boundDataType) { - case TYPE_CHAT: { + case DataType.CHAT: { if (boundChat != null) { headerView.getAvatarReceiver().requestChat(tdlib, boundChat.id, AvatarReceiver.Options.NONE); } break; } - case TYPE_USER: { + case DataType.USER: { if (boundUser != null) { headerView.getAvatarReceiver().requestUser(tdlib, boundUser.id, AvatarReceiver.Options.NONE); } break; } + case DataType.ACCOUNT: { + if (boundAccount != null) { + headerView.getAvatarReceiver().requestAccount(tdlib, boundAccount.id, AvatarReceiver.Options.NONE); + } + break; + } } } } @@ -1389,9 +1435,10 @@ private void addChatListeners (TdApi.Chat chat, @Nullable ThreadInfo messageThre @Override public void onUserUpdated (TdApi.User user) { switch (boundDataType) { - case TYPE_CHAT: + case DataType.CHAT: break; - case TYPE_USER: + case DataType.USER: + case DataType.ACCOUNT: setHeaderUser(user); break; } @@ -1406,8 +1453,9 @@ public boolean needUserStatusUiUpdates () { @Override public void onUserStatusChanged (long userId, TdApi.UserStatus status, boolean uiOnly) { switch (boundDataType) { - case TYPE_CHAT: - case TYPE_USER: + case DataType.CHAT: + case DataType.USER: + case DataType.ACCOUNT: setChatSubtitle(); break; } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/JoinedUsersView.java b/app/src/main/java/org/thunderdog/challegram/widget/JoinedUsersView.java index 37d3cfc1e2..fc90eaee1a 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/JoinedUsersView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/JoinedUsersView.java @@ -31,8 +31,8 @@ import org.thunderdog.challegram.loader.ImageReceiver; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.telegram.Tdlib; -import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; @@ -349,7 +349,7 @@ public void onClickAt (View view, float x, float y) { // Drawing private void drawPlaceholder (Canvas c, AvatarInfo info, int cx, int cy, float factor) { - c.drawCircle(cx, cy, Screen.dp(AVATAR_RADIUS), Paints.fillingPaint(ColorUtils.alphaColor(factor, Theme.getColor(info.avatarColorId)))); + c.drawCircle(cx, cy, Screen.dp(AVATAR_RADIUS), Paints.fillingPaint(ColorUtils.alphaColor(factor, info.accentColor.getPrimaryColor()))); Paint paint = Paints.whiteMediumPaint(15f, info.letters.needFakeBold, false); paint.setAlpha((int) (255f * factor)); c.drawText(info.letters.text, cx - info.lettersWidth15dp / 2, cy + Screen.dp(5.5f), paint); diff --git a/app/src/main/java/org/thunderdog/challegram/widget/LinkPreviewToggleView.java b/app/src/main/java/org/thunderdog/challegram/widget/LinkPreviewToggleView.java new file mode 100644 index 0000000000..575df97eb8 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/LinkPreviewToggleView.java @@ -0,0 +1,351 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 29/11/2023 + */ +package org.thunderdog.challegram.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.view.View; + +import androidx.annotation.IntDef; +import androidx.appcompat.widget.AppCompatImageView; + +import org.thunderdog.challegram.BuildConfig; +import org.thunderdog.challegram.Log; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.U; +import org.thunderdog.challegram.navigation.TooltipOverlayView; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.DrawAlgorithms; +import org.thunderdog.challegram.tool.Drawables; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Views; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.BoolAnimator; +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.core.MathUtils; +import me.vkryl.core.lambda.RunnableData; + +public class LinkPreviewToggleView extends AppCompatImageView implements TooltipOverlayView.LocationProvider { + private final BoolAnimator showAboveText = new BoolAnimator(this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); + private final BoolAnimator hasMedia = new BoolAnimator(this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); + private final FactorAnimator mediaStateFactor = new FactorAnimator(0, (id, factor, fraction, callee) -> invalidate(), AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); + + private int rectWidth, horizontalInset, verticalInset, verticalLineSpacing, horizontalLineSpacing, lineSize; + + private final Drawable topDrawable; + private boolean needFallback; + + public LinkPreviewToggleView (Context context) { + super(context); + setImageResource(R.drawable.baseline_link_preview_bg_24); + setScaleType(ScaleType.CENTER); + + setColorFilter(Theme.iconColor()); + + topDrawable = Drawables.get(R.drawable.baseline_link_preview_top_layer_24); + + updateDimensions(); + } + + private void updateDimensions () { + long now = SystemClock.uptimeMillis(); + + Drawable topDrawable = Drawables.get(R.drawable.baseline_link_preview_top_layer_24); + toBitmap(topDrawable, bitmap -> { + int centerX = bitmap.getWidth() / 2; + + int startTopY = -1, endTopY = -1, secondaryTopY = -1; + int prevColor = 0; + for (int y = 0; y < bitmap.getHeight(); y++) { + int color = bitmap.getPixel(centerX, y); + + boolean hadColor = Color.alpha(prevColor) != 0; + boolean hasColor = Color.alpha(color) != 0; + if (hadColor != hasColor) { + if (hasColor) { + if (startTopY == -1) { + startTopY = y; + } else if (secondaryTopY == -1) { + secondaryTopY = y; + break; + } + } else { + if (endTopY == -1) { + endTopY = y; + } + } + } + + prevColor = color; + } + + this.verticalLineSpacing = secondaryTopY - endTopY; + this.lineSize = endTopY - startTopY - 1; + this.verticalInset = startTopY; + + prevColor = 0; + + int startRightX = -1; + int endRightX = -1, secondaryRightX = -1; + + for (int x = bitmap.getWidth() - 1; x >= 0; x--) { + int color = bitmap.getPixel(x, verticalInset); + + boolean hadColor = Color.alpha(prevColor) != 0; + boolean hasColor = Color.alpha(color) != 0; + + if (hadColor != hasColor) { + if (hasColor) { + if (startRightX == -1) { + startRightX = x; + } else if (secondaryRightX == -1) { + secondaryRightX = x; + break; + } + } else { + if (endRightX == -1) { + endRightX = x; + } + } + } + + prevColor = color; + } + + needFallback = false; + if (startTopY == -1 || endTopY == -1 || secondaryTopY == -1) { + if (BuildConfig.DEBUG) { + throw new IllegalStateException(); + } + needFallback = true; + } + if (startRightX == -1 || endRightX == -1 || secondaryRightX == -1) { + if (BuildConfig.DEBUG) { + throw new IllegalStateException(); + } + needFallback = true; + } + + this.horizontalInset = bitmap.getWidth() - startRightX - 1; + this.horizontalLineSpacing = endRightX - secondaryRightX; + this.rectWidth = startRightX - endRightX - 1; + + if (BuildConfig.DEBUG) { + Log.v("Measured icon in %dms", SystemClock.uptimeMillis() - now); + } + }); + } + + public void addThemeListeners (ViewController themeProvider) { + themeProvider.addThemeFilterListener(this, ColorId.icon); + themeProvider.addThemeInvalidateListener(this); + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + MediaVisibility.NONE, + MediaVisibility.SMALL, + MediaVisibility.LARGE + }) + public @interface MediaVisibility { + int NONE = 0, SMALL = 1, LARGE = 2; + } + + public void setShowAboveText (boolean showAboveText, boolean animated) { + this.showAboveText.setValue(showAboveText, animated && Views.isValid(this)); + } + + private @MediaVisibility int mediaVisibility = MediaVisibility.NONE; + + public void setMediaVisibility (@MediaVisibility int newState, boolean animated) { + if (this.mediaVisibility != newState || !animated) { + this.mediaVisibility = newState; + animated = animated && Views.isValid(this); + this.hasMedia.setValue(newState != MediaVisibility.NONE, animated); + if (animated) { + mediaStateFactor.animateTo(newState); + } else { + mediaStateFactor.forceFactor(newState); + } + } + } + + @Override + public void getTargetBounds (View targetView, Rect outRect) { + Drawable drawable = getDrawable(); + if (drawable != null) { + int centerX = getMeasuredWidth() / 2; + int centerY = getMeasuredHeight() / 2; + outRect.set( + centerX - drawable.getMinimumWidth() / 2, + centerY - drawable.getMinimumHeight() / 2, + centerX + drawable.getMinimumWidth() / 2, + centerY + drawable.getMinimumHeight() / 2 + ); + } else { + outRect.setEmpty(); + } + } + + @Override + protected void onDraw (Canvas c) { + super.onDraw(c); + + int width = topDrawable.getMinimumWidth(); + int height = topDrawable.getMinimumHeight(); + + float centerX = getMeasuredWidth() / 2f; + float centerY = getMeasuredHeight() / 2f; + + float minY = centerY - height / 2f + verticalInset + lineSize; + float maxY = centerY + height / 2f - verticalInset - lineSize; + + centerY += MathUtils.clamp(1f - showAboveText.getFloatValue()) * ((int) (height / 2) + (int) (height % 2) + Screen.dp(.5f)); + + final float left = centerX - width / 2f; + final float top = centerY - height / 2f; + + final float right = left + width; + final float bottom = top + height / 2f; + + if (needFallback) { + Drawables.draw(c, topDrawable, left, top, PorterDuffPaint.get(ColorId.icon)); + return; + } + + /*if (BuildConfig.DEBUG) { + Drawables.draw(c, topDrawable, left, top, Paints.getPorterDuffPaint(0xaaff0000)); + }*/ + + float factor = mediaStateFactor.getFactor(); + + float cLeft = left, cTop = top, cRight = right, cBottom = bottom; + cLeft += horizontalInset; + cRight -= horizontalInset; + cTop += verticalInset; + cBottom -= verticalInset; + + int contentColor = Theme.textAccentColor(); + + if (factor > 0f) { + float rectWidth = factor <= 1f ? this.rectWidth * factor : MathUtils.fromTo(this.rectWidth, cRight - (cLeft + lineSize + horizontalLineSpacing), factor - 1f); + c.drawRect( + cRight - rectWidth, + cTop, + cRight, + cTop + lineSize * 2 + verticalLineSpacing, + Paints.fillingPaint(contentColor) + ); + } + + float lineStartX = cLeft + lineSize + horizontalLineSpacing; + float topLineEndX; + if (factor <= 1f) { + topLineEndX = MathUtils.fromTo(cRight, cRight - rectWidth - horizontalLineSpacing, factor); + } else { + topLineEndX = MathUtils.fromTo(cRight - rectWidth - horizontalLineSpacing, lineStartX, factor - 1f); + } + float bottomLineEndX = MathUtils.fromTo(cRight - rectWidth - horizontalLineSpacing, cRight, hasMedia.getFloatValue()); + + c.drawRect( + lineStartX, + cTop, + topLineEndX, + cTop + lineSize, + Paints.fillingPaint(contentColor) + ); + c.drawRect( + lineStartX, + cTop + lineSize + verticalLineSpacing, + topLineEndX, + cTop + lineSize * 2 + verticalLineSpacing, + Paints.fillingPaint(contentColor) + ); + c.drawRect( + lineStartX, + cTop + lineSize * 2 + verticalLineSpacing * 2, + bottomLineEndX, + cTop + lineSize * 3 + verticalLineSpacing * 2, + Paints.fillingPaint(contentColor) + ); + + rebuildPath(cLeft, + cTop, + cLeft + lineSize, + cTop + lineSize * 3 + verticalLineSpacing * 2, + MathUtils.clamp(showAboveText.getFloatValue()), + MathUtils.clamp(1f - showAboveText.getFloatValue()) + ); + c.drawPath(leftLinePath, Paints.fillingPaint(contentColor)); + + /*if (BuildConfig.DEBUG) { + Drawables.drawCentered(c, Drawables.get(R.drawable.baseline_link_preview_bg_24), getMeasuredWidth() / 2f, getMeasuredHeight() / 2f, Paints.getPorterDuffPaint(0xaaff0000)); + }*/ + } + + private final RectF leftLineRect = new RectF(); + private float leftLineTopRadius, leftLineBottomRadius; + + private void rebuildPath (float left, float top, float right, float bottom, float topRadiusFactor, float bottomRadiusFactor) { + if (U.setRect(leftLineRect, left, top, right, bottom) || leftLineTopRadius != topRadiusFactor || leftLineBottomRadius != bottomRadiusFactor) { + leftLinePath.reset(); + leftLineTopRadius = topRadiusFactor; + leftLineBottomRadius = bottomRadiusFactor; + float radius = (right - left); + DrawAlgorithms.buildPath(leftLinePath, leftLineRect, radius * topRadiusFactor, 0, 0, radius * bottomRadiusFactor); + } + } + + private final Path leftLinePath = new Path(); + + private static void toBitmap (Drawable drawable, RunnableData callback) { + if (drawable instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + if (bitmapDrawable.getBitmap() != null) { + callback.runWithData(bitmapDrawable.getBitmap()); + return; + } + } + + Bitmap bitmap; + if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { + bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel + } else { + bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + callback.runWithData(bitmap); + bitmap.recycle(); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/LiveLocationView.java b/app/src/main/java/org/thunderdog/challegram/widget/LiveLocationView.java index ed5f88fc29..999d46b355 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/LiveLocationView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/LiveLocationView.java @@ -54,7 +54,7 @@ protected void onDraw (Canvas c) { } int cx = getMeasuredWidth() / 2; int cy = getMeasuredHeight() / 2; - Drawables.draw(c, liveLocationBmp, cx - liveLocationBmp.getMinimumWidth() / 2, cy - liveLocationBmp.getMinimumHeight() / 2, Paints.getPorterDuffPaint(0xffffffff)); + Drawables.draw(c, liveLocationBmp, cx - liveLocationBmp.getMinimumWidth() / 2, cy - liveLocationBmp.getMinimumHeight() / 2, Paints.whitePorterDuffPaint()); long delay = DrawAlgorithms.drawWaves(c, cx, cy - Screen.dp(4f), 0xffffffff, true, nextScheduleTime); if (delay != -1) { nextScheduleTime = SystemClock.uptimeMillis() + delay; diff --git a/app/src/main/java/org/thunderdog/challegram/widget/NonMaterialButton.java b/app/src/main/java/org/thunderdog/challegram/widget/NonMaterialButton.java index e9a8f21312..3a9024d25e 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/NonMaterialButton.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/NonMaterialButton.java @@ -126,7 +126,14 @@ public void setIcon (@DrawableRes int res) { @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(MeasureSpec.makeMeasureSpec(textWidth + Screen.dp(15f) * 2 + getPaddingLeft() + getPaddingRight(), MeasureSpec.EXACTLY), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); + int measuredWidth; + if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { + measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + } else { + measuredWidth = textWidth + Screen.dp(15f) * 2 + getPaddingLeft() + getPaddingRight(); + } + int measuredHeight = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); + setMeasuredDimension(measuredWidth, measuredHeight); updatePath(); setProgressBounds(); diff --git a/app/src/main/java/org/thunderdog/challegram/widget/PopupLayout.java b/app/src/main/java/org/thunderdog/challegram/widget/PopupLayout.java index 303b003367..07a8fca5c9 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/PopupLayout.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/PopupLayout.java @@ -24,6 +24,7 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.WindowManager; import android.widget.PopupWindow; @@ -321,7 +322,7 @@ private void showSystemWindow (View anchorView) { return; } int state = context.getActivityState(); - if (state == UI.STATE_RESUMED) { + if (state == UI.State.RESUMED) { try { window.showAtLocation(windowAnchorView = anchorView, Gravity.NO_GRAVITY, 0, 0); window.setBackgroundDrawable(new RootDrawable(UI.getContext(getContext()))); @@ -349,7 +350,7 @@ public void onActivityStateChanged (BaseActivity activity, int newState, int pre context.removeSimpleStateListener(this); return; } - if (newState == UI.STATE_RESUMED) { + if (newState == UI.State.RESUMED) { context.removeSimpleStateListener(this); if (!isTemporarilyHidden) { showSystemWindow(anchorView); @@ -910,6 +911,14 @@ public void addStatusBar () { useStatusBar = true; } + public static PopupLayout parentOf (View view) { + ViewParent parent = view.getParent(); + while (parent != null && !(parent instanceof PopupLayout)) { + parent = parent.getParent(); + } + return (PopupLayout) parent; + } + // Drawing diff --git a/app/src/main/java/org/thunderdog/challegram/widget/ProgressComponent.java b/app/src/main/java/org/thunderdog/challegram/widget/ProgressComponent.java index a7b1b318b8..f10c48ac71 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/ProgressComponent.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/ProgressComponent.java @@ -91,7 +91,7 @@ public ProgressComponent (BaseActivity context, int radius) { @Override public void onActivityStateChanged (BaseActivity activity, int newState, int prevState) { - setUiResumed(newState == UI.STATE_RESUMED); + setUiResumed(newState == UI.State.RESUMED); } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/widget/ReactionsSelectorRecyclerView.java b/app/src/main/java/org/thunderdog/challegram/widget/ReactionsSelectorRecyclerView.java index 58f836d2a6..033ae82cfd 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/ReactionsSelectorRecyclerView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/ReactionsSelectorRecyclerView.java @@ -2,7 +2,9 @@ import android.content.Context; import android.graphics.Canvas; +import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.drawable.GradientDrawable; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; @@ -17,7 +19,6 @@ import org.thunderdog.challegram.component.sticker.StickerSmallView; import org.thunderdog.challegram.component.sticker.TGStickerObj; import org.thunderdog.challegram.config.Config; -import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.data.TGReaction; import org.thunderdog.challegram.telegram.Tdlib; @@ -25,21 +26,22 @@ import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.Paints; import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.ui.MessageOptionsPagerController; import org.thunderdog.challegram.util.text.Counter; -import java.util.Set; - import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.MathUtils; public class ReactionsSelectorRecyclerView extends RecyclerView { + private final GradientDrawable gradientDrawableRight; + private final MessageOptionsPagerController.State state; + private boolean needDrawBorderGradient; + private ReactionsAdapter adapter; - public ReactionsSelectorRecyclerView (@NonNull Context context) { + public ReactionsSelectorRecyclerView (@NonNull Context context, MessageOptionsPagerController.State state) { super(context); - } - - public void setMessage (TGMessage message) { - Set chosen = message.getMessageReactions().getChosen(); - TdApi.AvailableReaction[] reactions = message.getMessageAvailableReactions(); + this.state = state; + this.gradientDrawableRight = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[]{ 0, lastColor = Theme.backgroundColor() }); LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false) { @Override @@ -49,30 +51,56 @@ protected boolean isLayoutRTL () { }; setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? OVER_SCROLL_IF_CONTENT_SCROLLS : OVER_SCROLL_NEVER); - setPadding(Screen.dp(9), Screen.dp(8), Screen.dp(9), Screen.dp(8)); + setPadding(Screen.dp(9), Screen.dp(7), Screen.dp(9), Screen.dp(7)); setClipToPadding(false); setHasFixedSize(true); + addItemDecoration(new ItemDecoration() { + @Override + public void getItemOffsets (@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull State state) { + outRect.set(Screen.dp(-1), 0, Screen.dp(-1), 0); + } + }); setLayoutManager(linearLayoutManager); - setAdapter(adapter = new ReactionsAdapter(getContext(), message)); - - int index = -1; - for (int a = 0; a < reactions.length; a++) { - String reactionKey = TD.makeReactionKey(reactions[a].type); - if (chosen.contains(reactionKey)) { - index = a; - break; - } + setAdapter(adapter = new ReactionsAdapter(getContext(), state)); + } + + public void setNeedDrawBorderGradient (boolean needDrawBorderGradient) { + this.needDrawBorderGradient = needDrawBorderGradient; + this.invalidate(); + } + + @Override + protected void dispatchDraw (Canvas canvas) { + super.dispatchDraw(canvas); + if (!needDrawBorderGradient) { + return; } - if (index != -1) { - linearLayoutManager.scrollToPositionWithOffset(index, Screen.currentWidth() / 2 - Screen.dp(38) / 2); + checkGradients(); + float s = computeHorizontalScrollRange() - computeHorizontalScrollOffset() - computeHorizontalScrollExtent(); + int alpha = (int) (MathUtils.clamp(s / Screen.dp(20f)) * 255); + + gradientDrawableRight.setAlpha(alpha); + gradientDrawableRight.setBounds(getMeasuredWidth() - Screen.dp(35), 0, getMeasuredWidth(), getMeasuredHeight()); + gradientDrawableRight.draw(canvas); + } + + @Override + public void onScrolled (int dx, int dy) { + super.onScrolled(dx, dy); + if (needDrawBorderGradient) { + invalidate(); } } - ReactionsAdapter adapter; - public void setDelegate (ReactionSelectDelegate delegate) { - adapter.setDelegate(delegate); + private int lastColor; + + private void checkGradients () { + int color = Theme.backgroundColor(); + if (color != lastColor) { + gradientDrawableRight.setColors(new int[]{ 0, lastColor = Theme.backgroundColor() }); + } } private static class ReactionView extends FrameLayoutFix { @@ -85,13 +113,13 @@ private static class ReactionView extends FrameLayoutFix { public ReactionView (Context context) { super(context); - stickerView = new StickerSmallView(context, Screen.dp(-1)) { + stickerView = new StickerSmallView(context, 0) { @Override public boolean dispatchTouchEvent (MotionEvent event) { return false; } }; - stickerView.setLayoutParams(newParams(Screen.dp(38), Screen.dp(38), Gravity.LEFT | Gravity.CENTER_VERTICAL)); + stickerView.setLayoutParams(newParams(Screen.dp(40), Screen.dp(40), Gravity.LEFT | Gravity.CENTER_VERTICAL)); addView(stickerView); rectF = new RectF(); @@ -125,24 +153,24 @@ public void playAnimation () { @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { - int width = Screen.dp(38f); + int width = Screen.dp(40f); int padding = Screen.dp(1); if (useCounter) { width += counter.getScaledWidth(Screen.dp(6)); } - rectF.set(padding, padding, width - padding, Screen.dp(37)); + rectF.set(0, Screen.dp(1), width, Screen.dp(39)); - super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), heightMeasureSpec); + super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(Screen.dp(40), MeasureSpec.EXACTLY)); } @Override protected void dispatchDraw (Canvas c) { if (chosen) { - c.drawRoundRect(rectF, Screen.dp(18), Screen.dp(18), Paints.fillingPaint(Theme.getColor(ColorId.fillingPositive))); + c.drawRoundRect(rectF, Screen.dp(19), Screen.dp(19), Paints.fillingPaint(Theme.getColor(ColorId.fillingPositive))); } if (useCounter) { - counter.draw(c, Screen.dp(35), getMeasuredHeight() / 2f, Gravity.LEFT, 1f); + counter.draw(c, Screen.dp(36), getMeasuredHeight() / 2f, Gravity.LEFT, 1f); } super.dispatchDraw(c); } @@ -167,19 +195,14 @@ private static class ReactionsAdapter extends RecyclerView.Adapter chosen; - private ReactionSelectDelegate delegate; + private final MessageOptionsPagerController.State state; - ReactionsAdapter (Context context, TGMessage message) { + ReactionsAdapter (Context context, MessageOptionsPagerController.State state) { this.context = context; - this.tdlib = message.tdlib(); - this.reactions = message.getMessageAvailableReactions(); - this.message = message; - this.chosen = message.getMessageReactions().getChosen(); - } - - public void setDelegate (ReactionSelectDelegate delegate) { - this.delegate = delegate; + this.tdlib = state.tdlib; + this.message = state.message; + this.state = state; + this.reactions = state.availableReactions; } @NonNull @@ -199,14 +222,10 @@ public void onBindViewHolder (@NonNull ReactionHolder holder, int position) { final boolean needUseCounter = (message.isChannel() || !message.canGetAddedReactions()) && !message.useReactionBubbles(); view.setReaction(reaction, tdReaction, needUseCounter); view.setOnClickListener((v) -> { - if (delegate != null) { - delegate.onClick(v, reaction); - } + state.onReactionClickListener.onReactionClick(v, reaction, false); }); view.setOnLongClickListener((v) -> { - if (delegate != null) { - delegate.onLongClick(v, reaction); - } + state.onReactionClickListener.onReactionClick(v, reaction, true); return true; }); } @@ -232,9 +251,4 @@ public void onViewRecycled (ReactionHolder holder) { ((ReactionView) holder.itemView).stickerView.performDestroy(); } } - - public interface ReactionSelectDelegate { - void onClick (View v, TGReaction reaction); - void onLongClick (View v, TGReaction reaction); - } } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/RootFrameLayout.java b/app/src/main/java/org/thunderdog/challegram/widget/RootFrameLayout.java index 0bf1b09628..a3608668c1 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/RootFrameLayout.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/RootFrameLayout.java @@ -100,15 +100,19 @@ private void setKeyboardVisible (final boolean isVisible) { lastAction = null; } if (keyboardListener != null) { - getViewTreeObserver().removeOnPreDrawListener(onPreDrawListener); - getViewTreeObserver().addOnPreDrawListener(onPreDrawListener); + ViewTreeObserver observer = getViewTreeObserver(); + observer.removeOnPreDrawListener(onPreDrawListener); + observer.addOnPreDrawListener(onPreDrawListener); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { keyboardListener.onKeyboardStateChanged(isVisible); UI.post(lastAction = new CancellableRunnable() { @Override public void act () { - getViewTreeObserver().removeOnPreDrawListener(onPreDrawListener); + observer.removeOnPreDrawListener(onPreDrawListener); invalidate(); + if (lastAction == this) { + lastAction = null; + } } }.removeOnCancel(UI.getAppHandler()), 20); } else { @@ -116,24 +120,14 @@ public void act () { @Override public void act () { keyboardListener.onKeyboardStateChanged(isVisible); - getViewTreeObserver().removeOnPreDrawListener(onPreDrawListener); + observer.removeOnPreDrawListener(onPreDrawListener); invalidate(); + if (lastAction == this) { + lastAction = null; + } } }.removeOnCancel(UI.getAppHandler()), 2); } - /*if (true || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - - } else { - keyboardListener.onKeyboardStateChanged(isVisible); - }*/ - /*if (isVisible) { - UI.postDelayed(new Runnable() { - @Override - public void run () { - keyboardListener.closeAdditionalKeyboards(); - } - }, 20); - }*/ } } } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/SendButton.java b/app/src/main/java/org/thunderdog/challegram/widget/SendButton.java index 8ca603d211..f856d10871 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/SendButton.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/SendButton.java @@ -18,36 +18,62 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; +import android.view.Gravity; import android.view.MotionEvent; import android.view.View; +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.loader.AvatarReceiver; import org.thunderdog.challegram.navigation.TooltipOverlayView; +import org.thunderdog.challegram.telegram.ChatListener; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibCache; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.theme.ThemeManager; import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.util.RateLimiter; +import org.thunderdog.challegram.util.text.Counter; +import org.thunderdog.challegram.util.text.TextColorSet; + +import java.util.concurrent.TimeUnit; import me.vkryl.android.AnimatorUtils; import me.vkryl.android.animator.BoolAnimator; import me.vkryl.android.animator.FactorAnimator; import me.vkryl.core.ColorUtils; +import me.vkryl.core.MathUtils; +import me.vkryl.core.lambda.CancellableRunnable; +import me.vkryl.core.lambda.Destroyable; +import me.vkryl.core.lambda.RunnableInt; +import me.vkryl.td.ChatId; +import me.vkryl.td.Td; public class SendButton extends View implements FactorAnimator.Target, TooltipOverlayView.LocationProvider { private static Paint strokePaint; private final Drawable sendIcon; + private final Drawable sendIconBg; public SendButton (Context context, int sendIconRes) { super(context); + avatarReceiver = new AvatarReceiver(this); sendIcon = Drawables.get(getResources(), sendIconRes); + sendIconBg = Drawables.get(getResources(), sendIconRes); if (strokePaint == null) { strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); strokePaint.setStyle(Paint.Style.STROKE); @@ -107,6 +133,7 @@ protected void onDraw (Canvas c) { if (sendScale > 0f) { if (editFactor != 1f) { final Paint paint = Paints.getSendButtonPaint(); + final Paint paintBg = PorterDuffPaint.get(ColorId.iconLight); final int sourceAlpha = Color.alpha(Theme.chatSendButtonColor()); final boolean saved = editFactor != 0f || sendScale != 1f; if (saved) { @@ -114,6 +141,7 @@ protected void onDraw (Canvas c) { final float scale = Config.DEFAULT_ICON_SWITCH_SCALE + (1f - Config.DEFAULT_ICON_SWITCH_SCALE) * (1f - editFactor) * sendScale; c.scale(scale, scale, cx, cy); paint.setAlpha((int) ((float) sourceAlpha * (1f - editFactor) * sendScale)); + paintBg.setAlpha((int) ((float) sourceAlpha * (1f - editFactor) * sendScale)); } boolean rtl = Lang.rtl(); if (rtl) { @@ -121,9 +149,30 @@ protected void onDraw (Canvas c) { c.save(); c.scale(-1f, 1f, cx, cy); } - Drawables.draw(c, sendIcon, cx - sendIcon.getMinimumWidth() / 2, cy - sendIcon.getMinimumHeight() / 2, paint); + + final int iconW = sendIcon.getMinimumWidth(); + final int iconX = cx - iconW / 2; + final int iconY = cy - sendIcon.getMinimumHeight() / 2; + + final float slowModeDelayProgress = slowModeCounterController != null ? + slowModeCounterController.getSlowModeDelayProgress() : 1f; + + if (slowModeDelayProgress == 1f) { + Drawables.draw(c, sendIcon, iconX, iconY, paint); + } else { + int s = Views.save(c); + c.clipRect(iconW * slowModeDelayProgress + iconX, 0, getMeasuredWidth(), getMeasuredHeight()); + Drawables.draw(c, sendIconBg, iconX, iconY, paintBg); + Views.restore(c, s); + s = Views.save(c); + c.clipRect(0, 0, iconW * slowModeDelayProgress + iconX, getMeasuredHeight()); + Drawables.draw(c, sendIconBg, iconX, iconY, paint); + Views.restore(c, s); + } + if (saved) { paint.setAlpha(sourceAlpha); + paintBg.setAlpha(sourceAlpha); c.restore(); } else if (rtl) { c.restore(); @@ -289,6 +338,10 @@ protected void onDraw (Canvas c) { c.restore(); } } + + if (slowModeCounterController != null) { + slowModeCounterController.draw(c, avatarReceiver, cx, cy, 1f); + } } public void forceState (boolean inEditMode, boolean isActive) { @@ -359,13 +412,325 @@ public void onFactorChanged (int id, float factor, float fraction, FactorAnimato invalidate(); } + private final RateLimiter inlineProgressLimiter = new RateLimiter(this::animateInlineProgress, 100L, null); + @Override public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { switch (id) { case INLINE_PROGRESS_ANIMATOR: { - animateInlineProgress(); + if (finalFactor == 1f) { + inlineProgressLimiter.run(); + } break; } } } + + private SlowModeCounterController slowModeCounterController; + private final AvatarReceiver avatarReceiver; + private boolean ignoreDrawMessageSender; + + public void setIgnoreDrawMessageSender () { + this.ignoreDrawMessageSender = true; + } + + public void destroySlowModeCounterController () { + if (slowModeCounterController != null) { + slowModeCounterController.performDestroy(); + slowModeCounterController = null; + } + avatarReceiver.destroy(); + } + + @Override + protected void onAttachedToWindow () { + avatarReceiver.attach(); + super.onAttachedToWindow(); + } + + @Override + protected void onDetachedFromWindow () { + avatarReceiver.detach(); + super.onDetachedFromWindow(); + } + + public SlowModeCounterController getSlowModeCounterController (Tdlib tdlib) { + if (slowModeCounterController != null && slowModeCounterController.tdlib != tdlib) { + destroySlowModeCounterController(); + } + + if (slowModeCounterController == null) { + slowModeCounterController = new SlowModeCounterController(tdlib, this, new TextColorSet() { + @Override + public int defaultTextColor () { + return Theme.getColor(ColorId.textLight); + } + + @Override + public int backgroundColor (boolean isPressed) { + return Theme.getColor(ColorId.filling); + } + }, true, ignoreDrawMessageSender, (a, b, c) -> { + avatarReceiver.requestMessageSender(a, b, c); + invalidate(); + }); + } + return slowModeCounterController; + } + + public static class SlowModeCounterController implements TdlibCache.SupergroupDataChangeListener, ChatListener, Destroyable { + public final Counter counter; + public final RectF lastCounterDrawRect = new RectF(); + private final Tdlib tdlib; + private final boolean needBackground; + private final Drawable anonymousDrawable; + private final View view; + private final boolean ignoreDrawMessageSender; + private final Callback callback; + private float lastVisibilityDraw; + + private long chatId; + private @Nullable TdApi.Chat chat; + + public interface Callback { + void requestMessageSender (@Nullable Tdlib tdlib, @Nullable TdApi.MessageSender sender, @AvatarReceiver.Options int options); + } + + public SlowModeCounterController (Tdlib tdlib, View v, TextColorSet textColorSet, boolean needBackground, boolean ignoreDrawMessageSender, Callback callback) { + this.tdlib = tdlib; + this.view = v; + this.needBackground = needBackground; + this.anonymousDrawable = Drawables.get(v.getResources(), R.drawable.infanf_baseline_incognito_11); + this.ignoreDrawMessageSender = ignoreDrawMessageSender; + this.callback = callback; + + Counter.Builder builder = new Counter.Builder() + .callback((c, s) -> view.invalidate()) + .textSize(11f) + .colorSet(textColorSet); + + if (!needBackground) { + builder.noBackground(); + } + + this.counter = builder.build(); + } + + public Tdlib tdlib () { + return tdlib; + } + + public boolean isVisible () { + return counter.getVisibility() > 0f || hasChatDefaultMessageSenderIdToDraw(); + } + + public void draw (Canvas c, @Nullable AvatarReceiver avatarReceiver, float cx, float cy, float visibility) { + final float cxReal = cx + Screen.dp(5); + final float cyReal = cy + Screen.dp(10f); + + final boolean needScale = visibility != 1f; + int scaleSaveTo = -1; + if (needScale) { + scaleSaveTo = Views.save(c); + c.scale(visibility, visibility, cxReal, cyReal); + } + + lastVisibilityDraw = visibility; + counter.draw(c, cxReal, cyReal, Gravity.CENTER, 1f, lastCounterDrawRect); + + if (!ignoreDrawMessageSender) { + final float sendAsFactor = 1f - counter.getVisibility(); + final long sendAsSender = getChatDefaultMessageSenderId(); + + if (sendAsFactor > 0f && sendAsSender != 0 && sendAsSender != tdlib.myUserId()) { + if (needBackground) { + c.drawCircle(cxReal, cyReal, Screen.dp(9.5f * sendAsFactor), Paints.fillingPaint(counter.backgroundColor(false))); + } + final float radius = Screen.dp(7.5f * sendAsFactor); + + if (sendAsSender == chatId) { + c.drawCircle(cxReal, cyReal, radius, Paints.fillingPaint(Theme.iconLightColor())); + Drawables.draw(c, anonymousDrawable, cxReal - Screen.dp(5.5f), cyReal - Screen.dp(5.5f), PorterDuffPaint.get(ColorId.badgeMutedText)); + } else if (avatarReceiver != null) { + avatarReceiver.setBounds( + (int) (cxReal - radius), + (int) (cyReal - radius), + (int) (cxReal + radius), + (int) (cyReal + radius)); + avatarReceiver.draw(c); + } + } + } + if (needScale) { + Views.restore(c, scaleSaveTo); + } + } + + public void setCurrentChat (long chatId) { + if (this.chatId == chatId) { + return; + } + + stopSlowModeTimerUpdates(); + final long oldChatId = this.chatId; + this.chatId = chatId; + this.chat = tdlib.chat(chatId); + + if (oldChatId != 0) { + final long supergroupId = ChatId.toSupergroupId(oldChatId); + if (supergroupId != 0) { + tdlib.cache().unsubscribeFromSupergroupUpdates(supergroupId, this); + } + tdlib.listeners().unsubscribeFromChatUpdates(chatId, this); + } + + if (chatId != 0) { + final long supergroupId = ChatId.toSupergroupId(chatId); + if (supergroupId != 0) { + tdlib.cache().subscribeToSupergroupUpdates(supergroupId, this); + } + tdlib.listeners().subscribeToChatUpdates(chatId, this); + } + + updateChatDefaultMessageSenderId(chat != null ? chat.messageSenderId : null); + updateSlowModeTimer(false); + } + + public float getSlowModeDelayProgress () { + return slowModeDelayProgress; + } + + public void updateSlowModeTimer (boolean animated) { + if (!tdlib.isSupergroup(chatId)) { + setSlowModeTimer(0, 0, animated); + return; + } + + final TdApi.SupergroupFullInfo info = tdlib.cache().supergroupFull(ChatId.toSupergroupId(chatId), false); + if (info == null) { + tdlib.cache().supergroupFull(ChatId.toSupergroupId(chatId)); + setSlowModeTimer(0, 0, animated); + return; + } + + final long slowModeDelayExpiresIn = tdlib.cache().getSlowModeDelayExpiresIn(ChatId.toSupergroupId(chatId), TimeUnit.SECONDS); + setSlowModeTimer(slowModeDelayExpiresIn, info.slowModeDelay, animated); + + if (slowModeDelayExpiresIn > 0) { + startSlowModeTimerUpdates(); + } + } + + private CancellableRunnable slowModeTimerUpdateRunnable; + + private void startSlowModeTimerUpdates () { + stopSlowModeTimerUpdates(); + UI.post(slowModeTimerUpdateRunnable = new CancellableRunnable() { + @Override + public void act () { + updateSlowModeTimer(true); + } + }, 500); + } + + private void stopSlowModeTimerUpdates () { + if (slowModeTimerUpdateRunnable != null) { + slowModeTimerUpdateRunnable.cancel(); + slowModeTimerUpdateRunnable = null; + } + } + + private RunnableInt slowModeCounterUpdateListener; + + public void setSlowModeCounterUpdateListener (RunnableInt slowModeCounterUpdateListener) { + this.slowModeCounterUpdateListener = slowModeCounterUpdateListener; + } + + private float slowModeDelayProgress = 1f; + + private void setSlowModeTimer (long seconds, long slowModeDelaySeconds, boolean animated) { + this.slowModeDelayProgress = slowModeDelaySeconds == 0 ? 1f: + ((float) Math.max(slowModeDelaySeconds - seconds, 0)) / slowModeDelaySeconds; + + this.counter.setCount(seconds, false, formatElapsedTime((int) seconds), animated); + this.view.invalidate(); + if (slowModeCounterUpdateListener != null) { + slowModeCounterUpdateListener.runWithInt((int) seconds); + } + } + + public static String formatElapsedTime (int seconds) { + final int minutes = seconds / 60; + if (minutes > 0) { + return Lang.plural(R.string.SlowModeMinutesShort, minutes); + } else { + return Integer.toString(seconds); + } + } + + @Override + public void onSupergroupFullUpdated (long supergroupId, TdApi.SupergroupFullInfo newSupergroupFull) { + UI.post(() -> { + if (supergroupId == ChatId.toSupergroupId(chatId)) { + updateSlowModeTimer(true); + } + }); + } + + /* Default Sender Id */ + + @Override + public void onChatDefaultMessageSenderIdChanged (long chatId, TdApi.MessageSender senderId) { + UI.post(() -> { + if (chatId == this.chatId) { + updateChatDefaultMessageSenderId(senderId); + } + }); + } + + private void updateChatDefaultMessageSenderId (TdApi.MessageSender sender) { + final boolean isUserSender = Td.getSenderId(sender) == tdlib.myUserId(); + final boolean isGroupSender = Td.getSenderId(sender) == chatId; + updateChatDefaultMessageSenderId(sender, isUserSender, isGroupSender); + } + + private void updateChatDefaultMessageSenderId (TdApi.MessageSender sender, boolean isPersonal, boolean isAnonymous) { + callback.requestMessageSender(tdlib, sender, AvatarReceiver.Options.NONE); + } + + private long getChatDefaultMessageSenderId () { + return chat != null && chat.messageSenderId != null ? Td.getSenderId(chat.messageSenderId) : 0; + } + + private boolean hasChatDefaultMessageSenderIdToDraw () { + return chat != null && chat.messageSenderId != null && Td.getSenderId(chat.messageSenderId) != tdlib.myUserId(); + } + + + + /* * */ + + private final RectF tmpRectF = new RectF(); + + public void buildClipPath (View v, Path clipPath) { + final boolean hasSenderId = hasChatDefaultMessageSenderIdToDraw(); + final float cx = v.getMeasuredWidth() / 2f + Screen.dp(5); + final float cy = v.getMeasuredHeight() / 2f + Screen.dp(10f); + final float width = MathUtils.fromTo(hasSenderId ? Screen.dp(19) : 0, lastCounterDrawRect.width(), counter.getVisibility()) * lastVisibilityDraw; + final float height = MathUtils.fromTo(hasSenderId ? Screen.dp(19) : 0, lastCounterDrawRect.height(), counter.getVisibility()) * lastVisibilityDraw; + final float radius = Math.min(width, height) / 2f; + + tmpRectF.set(cx - width / 2f, cy - height / 2f, cx + width / 2f, cy + height / 2f); + + clipPath.reset(); + clipPath.addRect(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight(), Path.Direction.CW); + clipPath.addRoundRect(tmpRectF, radius, radius, Path.Direction.CCW); + clipPath.close(); + } + + @Override + public void performDestroy () { + setCurrentChat(0); + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/SnackBar.java b/app/src/main/java/org/thunderdog/challegram/widget/SnackBar.java index a5443ad13a..1b2a083d94 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/SnackBar.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/SnackBar.java @@ -156,6 +156,10 @@ private void updateTranslation () { } } + public float getVisibilityFactor () { + return isShowing.getFloatValue(); + } + @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); diff --git a/app/src/main/java/org/thunderdog/challegram/widget/StickersSuggestionsLayout.java b/app/src/main/java/org/thunderdog/challegram/widget/StickersSuggestionsLayout.java new file mode 100644 index 0000000000..4917ab69e7 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/StickersSuggestionsLayout.java @@ -0,0 +1,297 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 19/08/2023 + */ +package org.thunderdog.challegram.widget; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.chat.StickerSuggestionAdapter; +import org.thunderdog.challegram.component.sticker.StickerSmallView; +import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.config.Config; +import org.thunderdog.challegram.navigation.NavigationController; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.EmojiMediaType; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.ui.MessagesController; + +import java.util.ArrayList; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.android.widget.AnimatedFrameLayout; +import me.vkryl.android.widget.FrameLayoutFix; + +public class StickersSuggestionsLayout extends AnimatedFrameLayout implements FactorAnimator.Target { + private static final int ANIMATOR_STICKERS = 1; + + private final ImageView stickerSuggestionArrowView; + private final LinearLayoutManager manager; + private final RecyclerView stickerSuggestionsView; + private StickerSuggestionAdapter stickerSuggestionAdapter; + + private MessagesController parent; + private FactorAnimator stickersAnimator; + private Delegate delegate; + private boolean choosingSuggestionSent; + private boolean areStickersVisible; + private boolean isEmoji; + + public interface Delegate { + void notifyChoosingEmoji (int emojiType, boolean isChoosingEmoji); + } + + public StickersSuggestionsLayout (Context context) { + super(context); + + manager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false); + + stickerSuggestionsView = new RecyclerView(context) { + @Override + public boolean onTouchEvent (MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_DOWN) { + View v = manager.findViewByPosition(0); + if (v != null && v.getLeft() > e.getX()) { + return false; + } + int i = manager.findLastVisibleItemPosition(); + if (i != -1) { + v = manager.findViewByPosition(i); + if (v != null && v.getRight() < e.getX()) { + return false; + } + } + } + return areStickersVisible && getAlpha() == 1f && super.onTouchEvent(e); + } + }; + stickerSuggestionsView.setItemAnimator(null); + stickerSuggestionsView.setPadding(Screen.dp(48), 0, Screen.dp(48), 0); + stickerSuggestionsView.setClipToPadding(false); + stickerSuggestionsView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? View.OVER_SCROLL_IF_CONTENT_SCROLLS : View.OVER_SCROLL_NEVER); + stickerSuggestionsView.setLayoutManager(manager); + + addView(stickerSuggestionsView); + + stickerSuggestionArrowView = new ImageView(context); + stickerSuggestionArrowView.setScaleType(ImageView.ScaleType.CENTER); + stickerSuggestionArrowView.setImageResource(R.drawable.stickers_back_arrow); + stickerSuggestionArrowView.setColorFilter(new PorterDuffColorFilter(Theme.headerFloatBackgroundColor(), PorterDuff.Mode.MULTIPLY)); + addView(stickerSuggestionArrowView); + } + + public void init (@NonNull MessagesController parent, boolean isEmoji) { + this.parent = parent; + this.isEmoji = isEmoji; + this.stickerSuggestionAdapter = new StickerSuggestionAdapter(parent, manager, parent, isEmoji) { + @Override + public void onStickerPreviewOpened (StickerSmallView view, TGStickerObj sticker) { + delegate.notifyChoosingEmoji(isEmoji ? EmojiMediaType.EMOJI : EmojiMediaType.STICKER, true); + if (areStickersVisible) { + choosingSuggestionSent = true; + } + } + + @Override + public void onStickerPreviewChanged (StickerSmallView view, TGStickerObj otherOrThisSticker) { + delegate.notifyChoosingEmoji(isEmoji ? EmojiMediaType.EMOJI : EmojiMediaType.STICKER, true); + if (areStickersVisible) { + choosingSuggestionSent = true; + } + } + + @Override + public void onStickerPreviewClosed (StickerSmallView view, TGStickerObj thisSticker) { + if (!choosingSuggestionSent) { + delegate.notifyChoosingEmoji(isEmoji ? EmojiMediaType.EMOJI : EmojiMediaType.STICKER, false); + } + } + }; + this.stickerSuggestionAdapter.setCallback(parent); + this.stickerSuggestionsView.setAdapter(stickerSuggestionAdapter); + + parent.addThemeSpecialFilterListener(stickerSuggestionArrowView, ColorId.overlayFilling); + + + int stickersListTopHeight = Screen.dp(isEmoji ? 36 : 72) + Screen.dp(2.5f); + int stickersListTotalHeight = stickersListTopHeight + Screen.dp(6.5f); + int stickerArrowHeight = Screen.dp(12f); + + RelativeLayout.LayoutParams params; + params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, stickersListTotalHeight + stickerArrowHeight); + params.addRule(RelativeLayout.ABOVE, R.id.msg_bottom); + params.bottomMargin = -(Screen.dp(8f) + stickerArrowHeight); + setLayoutParams(params); + + stickerSuggestionsView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, stickersListTotalHeight)); + + FrameLayoutFix.LayoutParams fparams; + fparams = FrameLayoutFix.newParams(Screen.dp(27f), stickerArrowHeight); + fparams.topMargin = stickersListTopHeight; + // fparams.leftMargin = Screen.dp(55f) + Screen.dp(2.5f); + + // setPivotX(fparams.leftMargin + Screen.dp(27f) / 2f); + setPivotY(stickersListTopHeight + stickerArrowHeight); + + stickerSuggestionArrowView.setLayoutParams(fparams); + } + + public boolean hasStickers () { + return stickerSuggestionAdapter.hasStickers(); + } + + public void setStickers (@NonNull MessagesController parent, @Nullable ArrayList stickers) { + stickerSuggestionAdapter.setCallback(parent); + stickerSuggestionAdapter.setStickers(stickers); + stickerSuggestionsView.scrollToPosition(0); + } + + public void addStickers (@NonNull MessagesController parent, @Nullable ArrayList stickers) { + stickerSuggestionAdapter.setCallback(parent); + stickerSuggestionAdapter.addStickers(stickers); + } + + public void setOnScrollListener (RecyclerView.OnScrollListener onScrollListener) { + stickerSuggestionsView.setOnScrollListener(onScrollListener); + } + + public void setArrowX (int arrowX) { + int width = stickerSuggestionsView.getMeasuredWidth(); + int paddingLeft = Math.max(arrowX - Screen.dp(24), Screen.dp(48)); + stickerSuggestionsView.setPadding(paddingLeft, 0, Screen.dp(48), 0); + stickerSuggestionArrowView.setTranslationX(arrowX - Screen.dp(27) / 2f); + setPivotX(arrowX); + } + + public void setChoosingDelegate (Delegate delegate) { + this.delegate = delegate; + } + + private float stickersFactor; + + private void setStickersFactor (float factor) { + if (this.stickersFactor != factor) { + this.stickersFactor = factor; + + final float scale = .8f + .2f * factor; + setScaleX(scale /* (isEmoji ? 0.5f : 1f)*/); + setScaleY(scale /* (isEmoji ? 0.5f : 1f)*/); + setAlpha(Math.min(1f, Math.max(0f, factor))); + } + } + + private void animateStickersFactor (float toFactor, boolean onLayout) { + if (stickersAnimator == null) { + stickersAnimator = new FactorAnimator(ANIMATOR_STICKERS, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180L, stickersFactor); + } + if (toFactor == 1f && stickersFactor == 0f) { + stickersAnimator.setInterpolator(AnimatorUtils.OVERSHOOT_INTERPOLATOR); + stickersAnimator.setDuration(210L); + } else { + stickersAnimator.setInterpolator(AnimatorUtils.DECELERATE_INTERPOLATOR); + stickersAnimator.setDuration(100L); + } + stickersAnimator.animateTo(toFactor, onLayout ? this : null); + } + + public void setStickersVisible (boolean areVisible) { + if (this.areStickersVisible != areVisible) { + this.areStickersVisible = areVisible; + if (areVisible) { + updatePosition(true); + stickerSuggestionsView.scrollToPosition(0); + } + if (choosingSuggestionSent) { + if (!areVisible) { + delegate.notifyChoosingEmoji(isEmoji ? EmojiMediaType.EMOJI : EmojiMediaType.STICKER, false); + } + choosingSuggestionSent = false; + } + boolean onLayout = getParent() == null && areVisible; + if (onLayout) { + parent.context().addToRoot(this, false); + } + animateStickersFactor(areVisible ? 1f : 0f, onLayout); + } + } + + public boolean isStickersVisible () { + return areStickersVisible; + } + + private void onStickersDisappeared () { + // stickerSuggestionAdapter.setStickers(null); // todo: clear stickers ?? + parent.context().removeFromRoot(this); + } + + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + if (id == ANIMATOR_STICKERS) { + setStickersFactor(factor); + } + } + + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + if (id == ANIMATOR_STICKERS) { + if (finalFactor == 0f) { + onStickersDisappeared(); + } + } + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + updatePosition(true); + } + + public void updatePosition (boolean needTranslate) { + ViewController c = UI.getCurrentStackItem(getContext()); + float tx = 0; + if (c instanceof MessagesController) { + int[] cords = ((MessagesController) c).getInputCursorOffset(); + setArrowX(cords[0]); + setTranslationY(cords[1]); + tx -= ((MessagesController) c).getPagerScrollOffsetInPixels(); + } else { + // setTranslationY(0); + } + NavigationController navigation = UI.getContext(getContext()).navigation(); + if (needTranslate && navigation != null && navigation.isAnimating()) { + float translate = navigation.getHorizontalTranslate(); + if (c instanceof MessagesController) { + tx = translate; + } + } + setTranslationX(tx); + } + +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/TextFormattingLayout.java b/app/src/main/java/org/thunderdog/challegram/widget/TextFormattingLayout.java index d928c93fcc..97bc2597ca 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/TextFormattingLayout.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/TextFormattingLayout.java @@ -79,6 +79,7 @@ public class TextFormattingLayout extends FrameLayout implements TranslationsMan private static final int FLAG_STRIKETHROUGH = 1 << 4; private static final int FLAG_LINK = 1 << 5; private static final int FLAG_SPOILER = 1 << 6; + private static final int FLAG_BLOCK_QUOTE = 1 << 7; private static final int FLAG_CLEAR = 1 << 30; private static final int[] buttonIds = new int[]{ @@ -416,7 +417,7 @@ private static int checkSpans (TdApi.FormattedText text, int start, int end) { if (text == null || text.entities == null) return 0; int flags = 0; - for (TdApi.TextEntity entity: text.entities) { + for (TdApi.TextEntity entity : text.entities) { final int entityStart = entity.offset, entityEnd = entity.offset + entity.length; if (!(entityStart >= end || start >= entityEnd)) { @@ -452,24 +453,48 @@ private static int getTypeFlagFromButtonId (@IdRes int id) { return -1; } - private static int getTypeFlagFromEntityType (TdApi.TextEntityType entityType) { - int constructor = entityType.getConstructor(); - if (constructor == TdApi.TextEntityTypeBold.CONSTRUCTOR) { - return FLAG_BOLD; - } else if (constructor == TdApi.TextEntityTypeItalic.CONSTRUCTOR) { - return FLAG_ITALIC; - } else if (constructor == TdApi.TextEntityTypeCode.CONSTRUCTOR) { - return FLAG_MONOSPACE; - } else if (constructor == TdApi.TextEntityTypeUnderline.CONSTRUCTOR) { - return FLAG_UNDERLINE; - } else if (constructor == TdApi.TextEntityTypeStrikethrough.CONSTRUCTOR) { - return FLAG_STRIKETHROUGH; - } else if (constructor == TdApi.TextEntityTypeTextUrl.CONSTRUCTOR) { - return FLAG_LINK; - } else if (constructor == TdApi.TextEntityTypeSpoiler.CONSTRUCTOR) { - return FLAG_SPOILER; + private static int getTypeFlagFromEntityType (TdApi.TextEntityType type) { + switch (type.getConstructor()) { + case TdApi.TextEntityTypeBold.CONSTRUCTOR: + return FLAG_BOLD; + case TdApi.TextEntityTypeItalic.CONSTRUCTOR: + return FLAG_ITALIC; + case TdApi.TextEntityTypeCode.CONSTRUCTOR: + case TdApi.TextEntityTypePre.CONSTRUCTOR: + case TdApi.TextEntityTypePreCode.CONSTRUCTOR: + return FLAG_MONOSPACE; + case TdApi.TextEntityTypeUnderline.CONSTRUCTOR: + return FLAG_UNDERLINE; + case TdApi.TextEntityTypeStrikethrough.CONSTRUCTOR: + return FLAG_STRIKETHROUGH; + case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: + return FLAG_LINK; + case TdApi.TextEntityTypeSpoiler.CONSTRUCTOR: + return FLAG_SPOILER; + case TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR: + return FLAG_BLOCK_QUOTE; + + // immutable + case TdApi.TextEntityTypeCustomEmoji.CONSTRUCTOR: + case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: + + // auto-detected + case TdApi.TextEntityTypeBankCardNumber.CONSTRUCTOR: + case TdApi.TextEntityTypeBotCommand.CONSTRUCTOR: + case TdApi.TextEntityTypeCashtag.CONSTRUCTOR: + case TdApi.TextEntityTypeEmailAddress.CONSTRUCTOR: + case TdApi.TextEntityTypeHashtag.CONSTRUCTOR: + case TdApi.TextEntityTypeMediaTimestamp.CONSTRUCTOR: + case TdApi.TextEntityTypeMention.CONSTRUCTOR: + case TdApi.TextEntityTypePhoneNumber.CONSTRUCTOR: + case TdApi.TextEntityTypeUrl.CONSTRUCTOR: + return -1; + + // unsupported + default: + Td.assertTextEntityType_91234a79(); + throw Td.unsupported(type); } - return -1; } private static TdApi.TextEntityType getEntityTypeFromTypeFlag (int flag) { @@ -487,6 +512,8 @@ private static TdApi.TextEntityType getEntityTypeFromTypeFlag (int flag) { return new TdApi.TextEntityTypeTextUrl(); } else if (flag == FLAG_SPOILER) { return new TdApi.TextEntityTypeSpoiler(); + } else if (flag == FLAG_BLOCK_QUOTE) { + return new TdApi.TextEntityTypeBlockQuote(); } return null; } @@ -560,7 +587,7 @@ protected void dispatchDraw (Canvas canvas) { super.dispatchDraw(canvas); if (drawable != null) { int color = ColorUtils.fromToArgb(Theme.iconColor(), Theme.getColor(ColorId.iconActive), isActive.getFloatValue()); - Drawables.drawCentered(canvas, drawable, getMeasuredWidth() / 2f, getMeasuredHeight() / 2f, needDrawWithoutRepainting ? null: Paints.getPorterDuffPaint(color)); + Drawables.drawCentered(canvas, drawable, getMeasuredWidth() / 2f, getMeasuredHeight() / 2f, needDrawWithoutRepainting ? null : Paints.getPorterDuffPaint(color)); } } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/TrendingPackHeaderView.java b/app/src/main/java/org/thunderdog/challegram/widget/TrendingPackHeaderView.java new file mode 100644 index 0000000000..6fbbd4c081 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/TrendingPackHeaderView.java @@ -0,0 +1,203 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 18/08/2023 + */ +package org.thunderdog.challegram.widget; + +import android.content.Context; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TGStickerSetInfo; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.support.ViewSupport; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Fonts; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.util.text.Highlight; + +public class TrendingPackHeaderView extends RelativeLayout { + private final android.widget.TextView newView; + private final NonMaterialButton button; + private final TextView titleView; + private final TextView subtitleView; + + public TrendingPackHeaderView (Context context) { + super(context); + + RelativeLayout.LayoutParams params; + params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, Screen.dp(16f)); + params.addRule(Lang.alignParent()); + if (Lang.rtl()) { + params.leftMargin = Screen.dp(6f); + } else { + params.rightMargin = Screen.dp(6f); + } + params.topMargin = Screen.dp(3f); + newView = new NoScrollTextView(context); + newView.setId(R.id.btn_new); + newView.setSingleLine(true); + newView.setPadding(Screen.dp(4f), Screen.dp(1f), Screen.dp(4f), 0); + newView.setTextColor(Theme.getColor(ColorId.promoContent)); + newView.setTypeface(Fonts.getRobotoBold()); + newView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 10f); + newView.setText(Lang.getString(R.string.New).toUpperCase()); + newView.setLayoutParams(params); + + params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, Screen.dp(28f)); + if (Lang.rtl()) { + params.rightMargin = Screen.dp(16f); + } else { + params.leftMargin = Screen.dp(16f); + } + params.topMargin = Screen.dp(5f); + params.addRule(Lang.rtl() ? RelativeLayout.ALIGN_PARENT_LEFT : RelativeLayout.ALIGN_PARENT_RIGHT); + button = new NonMaterialButton(context); + button.setId(R.id.btn_addStickerSet); + button.setText(R.string.Add); + button.setLayoutParams(params); + + params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, Screen.dp(28f)); + if (Lang.rtl()) { + params.rightMargin = Screen.dp(16f); + } else { + params.leftMargin = Screen.dp(16f); + } + params.topMargin = Screen.dp(5f); + params.addRule(Lang.rtl() ? RelativeLayout.ALIGN_PARENT_LEFT : RelativeLayout.ALIGN_PARENT_RIGHT); + params.width = params.height = Screen.dp(16); + + params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + if (Lang.rtl()) { + params.leftMargin = Screen.dp(12f); + params.addRule(RelativeLayout.LEFT_OF, R.id.btn_new); + params.addRule(RelativeLayout.RIGHT_OF, R.id.btn_addStickerSet); + } else { + params.rightMargin = Screen.dp(12f); + params.addRule(RelativeLayout.RIGHT_OF, R.id.btn_new); + params.addRule(RelativeLayout.LEFT_OF, R.id.btn_addStickerSet); + } + titleView = new NoScrollTextView(context); + titleView.setTypeface(Fonts.getRobotoMedium()); + titleView.setTextColor(Theme.textAccentColor()); + titleView.setGravity(Lang.gravity()); + titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16f); + titleView.setSingleLine(true); + titleView.setEllipsize(TextUtils.TruncateAt.END); + titleView.setLayoutParams(params); + + params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + if (Lang.rtl()) { + params.leftMargin = Screen.dp(12f); + params.addRule(RelativeLayout.LEFT_OF, R.id.btn_new); + params.addRule(RelativeLayout.RIGHT_OF, R.id.btn_addStickerSet); + } else { + params.rightMargin = Screen.dp(12f); + params.addRule(RelativeLayout.RIGHT_OF, R.id.btn_new); + params.addRule(RelativeLayout.LEFT_OF, R.id.btn_addStickerSet); + } + + params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.addRule(Lang.alignParent()); + params.topMargin = Screen.dp(22f); + subtitleView = new NoScrollTextView(context); + subtitleView.setTypeface(Fonts.getRobotoRegular()); + subtitleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15f); + subtitleView.setTextColor(Theme.textDecentColor()); + subtitleView.setSingleLine(true); + subtitleView.setEllipsize(TextUtils.TruncateAt.END); + subtitleView.setLayoutParams(params); + + addView(newView); + addView(button); + addView(titleView); + addView(subtitleView); + } + + public void setButtonOnClickListener (View.OnClickListener listener) { + button.setOnClickListener(listener); + } + + public void setThemeProvider (@Nullable ViewController themeProvider) { + if (themeProvider != null) { + themeProvider.addThemeTextColorListener(newView, ColorId.promoContent); + themeProvider.addThemeInvalidateListener(newView); + themeProvider.addThemeInvalidateListener(this); + themeProvider.addThemeInvalidateListener(button); + themeProvider.addThemeTextAccentColorListener(titleView); + themeProvider.addThemeTextDecentColorListener(subtitleView); + ViewSupport.setThemedBackground(newView, ColorId.promo, themeProvider).setCornerRadius(3f); + } + } + + public void setStickerSetInfo (@Nullable TGStickerSetInfo stickerSet, @Nullable String highlight, boolean isInProgress, boolean isNew) { + setTag(stickerSet); + + newView.setVisibility(!isNew ? View.GONE : View.VISIBLE); + button.setInProgress(stickerSet != null && !stickerSet.isRecent() && isInProgress, false); + button.setIsDone(stickerSet != null && stickerSet.isInstalled(), false); + button.setTag(stickerSet); + + Views.setMediumText(titleView, Highlight.toSpannable(stickerSet != null ? stickerSet.getTitle() : "", highlight)); + subtitleView.setText(stickerSet != null ? Lang.plural(stickerSet.isEmoji() ? R.string.xEmoji : R.string.xStickers, stickerSet.getFullSize()) : ""); + + if (Views.setAlignParent(newView, Lang.rtl())) { + int rightMargin = Screen.dp(6f); + int topMargin = Screen.dp(3f); + Views.setMargins(newView, Lang.rtl() ? rightMargin : 0, topMargin, Lang.rtl() ? 0 : rightMargin, 0); + Views.updateLayoutParams(newView); + } + + if (Views.setAlignParent(button, Lang.rtl() ? RelativeLayout.ALIGN_PARENT_LEFT : RelativeLayout.ALIGN_PARENT_RIGHT)) { + int leftMargin = Screen.dp(16f); + int topMargin = Screen.dp(5f); + Views.setMargins(button, Lang.rtl() ? 0 : leftMargin, topMargin, Lang.rtl() ? leftMargin : 0, 0); + Views.updateLayoutParams(button); + } + RelativeLayout.LayoutParams params; + params = (RelativeLayout.LayoutParams) titleView.getLayoutParams(); + if (Lang.rtl()) { + int leftMargin = Screen.dp(12f); + if (params.leftMargin != leftMargin) { + params.leftMargin = leftMargin; + params.rightMargin = 0; + params.addRule(RelativeLayout.LEFT_OF, R.id.btn_new); + params.addRule(RelativeLayout.RIGHT_OF, R.id.btn_addStickerSet); + Views.updateLayoutParams(titleView); + } + } else { + int rightMargin = Screen.dp(12f); + if (params.rightMargin != rightMargin) { + params.rightMargin = rightMargin; + params.leftMargin = 0; + params.addRule(RelativeLayout.RIGHT_OF, R.id.btn_new); + params.addRule(RelativeLayout.LEFT_OF, R.id.btn_addStickerSet); + Views.updateLayoutParams(titleView); + } + } + Views.setTextGravity(titleView, Lang.gravity()); + if (Views.setAlignParent(subtitleView, Lang.rtl())) { + Views.updateLayoutParams(subtitleView); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/TripleAvatarView.java b/app/src/main/java/org/thunderdog/challegram/widget/TripleAvatarView.java index f34d477c3c..5ca29838d5 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/TripleAvatarView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/TripleAvatarView.java @@ -101,7 +101,7 @@ private void requestUserFile (long[] users, int index, Tdlib tdlib, ImageReceive TdApi.User user = tdlib.chatUser(users[index]); if (user == null || TD.isPhotoEmpty(user.profilePhoto)) { - placeholders[index] = new AvatarPlaceholder(AVATAR_SIZE / 2f, new AvatarPlaceholder.Metadata(TD.getAvatarColorId(user, tdlib.myUserId()), TD.getLetters(user)), null); + placeholders[index] = new AvatarPlaceholder(AVATAR_SIZE / 2f, new AvatarPlaceholder.Metadata(tdlib.cache().userAccentColor(user), TD.getLetters(user)), null); receiver.requestFile(null); } else { placeholders[index] = null; diff --git a/app/src/main/java/org/thunderdog/challegram/widget/VerticalChatView.java b/app/src/main/java/org/thunderdog/challegram/widget/VerticalChatView.java index 15fd8a2a53..2cd7916d4c 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/VerticalChatView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/VerticalChatView.java @@ -44,6 +44,7 @@ import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Icons; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; import org.thunderdog.challegram.util.text.Counter; @@ -373,7 +374,7 @@ protected void onDraw (Canvas c) { if (drawAnonymousSender) { identityAvatarReceiver.drawPlaceholderRounded(c, identityAvatarReceiver.getDisplayRadius(), Theme.getColor(ColorId.iconLight)); - Drawables.draw(c, Drawables.get(R.drawable.infanf_baseline_incognito_14), identityAvatarReceiver.centerX() - Screen.dp(7), identityAvatarReceiver.centerY() - Screen.dp(7), Paints.getPorterDuffPaint(Theme.getColor(ColorId.badgeMutedText))); + Drawables.draw(c, Drawables.get(R.drawable.infanf_baseline_incognito_14), identityAvatarReceiver.centerX() - Screen.dp(7), identityAvatarReceiver.centerY() - Screen.dp(7), PorterDuffPaint.get(ColorId.badgeMutedText)); } else { identityAvatarReceiver.draw(c); } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/ViewControllerPagerAdapter.java b/app/src/main/java/org/thunderdog/challegram/widget/ViewControllerPagerAdapter.java index 6a78eb6a69..4f84cb0012 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/ViewControllerPagerAdapter.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/ViewControllerPagerAdapter.java @@ -22,16 +22,21 @@ import androidx.collection.SparseArrayCompat; import androidx.viewpager.widget.PagerAdapter; +import org.thunderdog.challegram.navigation.NavigationController; import org.thunderdog.challegram.navigation.ViewController; +import java.util.HashSet; +import java.util.Set; + import me.vkryl.core.lambda.Destroyable; -public class ViewControllerPagerAdapter extends PagerAdapter implements Destroyable { +public class ViewControllerPagerAdapter extends PagerAdapter implements Destroyable, ViewController.AttachListener { public interface ControllerProvider { int getControllerCount (); ViewController createControllerForPosition (int position); void onPrepareToShow (int position, ViewController controller); void onAfterHide (int position, ViewController controller); + ViewController getParentOrSelf (); } private final ControllerProvider provider; @@ -40,6 +45,14 @@ public interface ControllerProvider { public ViewControllerPagerAdapter (@NonNull ControllerProvider provider) { this.provider = provider; this.controllers = new SparseArrayCompat<>(); + provider.getParentOrSelf().addAttachStateListener(this); + } + + @Override + public void onAttachStateChanged (ViewController context, NavigationController navigation, boolean isAttached) { + for (ViewController c : visibleControllers) { + c.onAttachStateChanged(navigation, isAttached); + } } public void notifyItemInserted (int index) { @@ -113,6 +126,8 @@ public int getItemPosition (@NonNull Object object) { return POSITION_NONE; } + private final Set> visibleControllers = new HashSet<>(); + @Override @NonNull public Object instantiateItem (@NonNull ViewGroup container, int position) { @@ -128,6 +143,10 @@ public Object instantiateItem (@NonNull ViewGroup container, int position) { provider.onPrepareToShow(position, c); c.onPrepareToShow(); container.addView(view); + visibleControllers.add(c); + if (!c.getAttachState()) { + c.onAttachStateChanged(provider.getParentOrSelf().navigationController(), true); + } return c; } @@ -135,6 +154,10 @@ public Object instantiateItem (@NonNull ViewGroup container, int position) { public void destroyItem (ViewGroup container, int position, @NonNull Object object) { ViewController c = (ViewController) object; container.removeView(c.getValue()); + visibleControllers.remove(c); + if (c.getAttachState()) { + c.onAttachStateChanged(provider.getParentOrSelf().navigationController(), false); + } provider.onAfterHide(position, c); c.onCleanAfterHide(); } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/decoration/ItemDecorationFirstViewTop.java b/app/src/main/java/org/thunderdog/challegram/widget/decoration/ItemDecorationFirstViewTop.java new file mode 100644 index 0000000000..b446bc69f4 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/decoration/ItemDecorationFirstViewTop.java @@ -0,0 +1,123 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 27/09/2023 + */ +package org.thunderdog.challegram.widget.decoration; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.util.ScrollJumpCompensator; + +public class ItemDecorationFirstViewTop extends RecyclerView.ItemDecoration { + private final RecyclerView recyclerView; + private final LinearLayoutManager linearLayoutManager; + private final RecyclerView.OnScrollListener scrollListener; + private final Callback callback; + + private boolean isScheduledDecorationOffsetDisable; + private boolean isDecorationOffsetDisabled; + private int lastTopDecorationOffset; + + public interface Callback { + int getTopDecorationOffset (); + } + + public static ItemDecorationFirstViewTop attach (RecyclerView recyclerView, Callback callback) { + ItemDecorationFirstViewTop decoration = new ItemDecorationFirstViewTop(recyclerView, (LinearLayoutManager) recyclerView.getLayoutManager(), callback); + recyclerView.addItemDecoration(decoration); + recyclerView.addOnScrollListener(decoration.scrollListener); + return decoration; + } + + public void scheduleDisableDecorationOffset () { + this.isScheduledDecorationOffsetDisable = true; + checkCanDisableDecorationOffset(); + } + + public void enableDecorationOffset () { + setDecorationOffsetDisabled(false); + } + + /* * */ + + private ItemDecorationFirstViewTop (RecyclerView recyclerView, LinearLayoutManager linearLayoutManager, Callback callback) { + this.recyclerView = recyclerView; + this.linearLayoutManager = linearLayoutManager; + this.scrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newState) { + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + UI.post(() -> checkCanDisableDecorationOffset()); + } + } + }; + this.callback = callback; + } + + private void checkCanDisableDecorationOffset () { + if (isScheduledDecorationOffsetDisable && !isDecorationOffsetDisabled) { + View v = linearLayoutManager.findViewByPosition(0); + if (v == null || v.getTop() <= 0) { + setDecorationOffsetDisabled(true); + } else { + recyclerView.smoothScrollBy(0, v.getTop()); + } + } + } + + private void setDecorationOffsetDisabled (boolean disabled) { + if (isDecorationOffsetDisabled == disabled) return; + isDecorationOffsetDisabled = disabled; + isScheduledDecorationOffsetDisable &= disabled; + + final int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition(); + final int offset = lastTopDecorationOffset * (disabled ? -1 : 1); + + // Changing the height of the first view can be animated, this leads to unwanted behavior. + // Temporarily disable animation. + + final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); + recyclerView.setItemAnimator(null); + + recyclerView.invalidateItemDecorations(); + if (firstVisibleItemPosition == 0 && offset != 0) { + ScrollJumpCompensator.compensate(recyclerView, offset); + } + + if (itemAnimator != null) { + UI.post(() -> { + if (recyclerView.getItemAnimator() == null) { + recyclerView.setItemAnimator(itemAnimator); + } + }); + } + } + + @Override + public void getItemOffsets (@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + final int position = parent.getChildAdapterPosition(view); + final boolean isUnknown = position == RecyclerView.NO_POSITION; + int top = 0; + if (position == 0 || isUnknown) { + top = lastTopDecorationOffset = callback.getTopDecorationOffset(); + } + + outRect.set(0, isDecorationOffsetDisabled ? 0 : top, 0, 0); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiHeaderCollapsibleSectionView.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiHeaderCollapsibleSectionView.java new file mode 100644 index 0000000000..53c9da8bb3 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiHeaderCollapsibleSectionView.java @@ -0,0 +1,142 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 18/08/2023 + */ +package org.thunderdog.challegram.widget.emoji; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.RectF; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.widget.emoji.section.EmojiSection; +import org.thunderdog.challegram.widget.emoji.section.EmojiSectionView; + +import java.util.ArrayList; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.BoolAnimator; +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.ColorUtils; +import me.vkryl.core.MathUtils; + +@SuppressLint("ViewConstructor") +public class EmojiHeaderCollapsibleSectionView extends FrameLayout implements FactorAnimator.Target { + private final BoolAnimator expandAnimator; + private final RectF bgRect = new RectF(); + private final ArrayList emojiSectionsViews; + private ArrayList emojiSections; + private int currentSelectedIndex = -1; + + public EmojiHeaderCollapsibleSectionView (Context context) { + super(context); + emojiSectionsViews = new ArrayList<>(6); + expandAnimator = new BoolAnimator(0, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 220L); + } + + public void init (ArrayList sections) { + this.emojiSections = sections; + emojiSectionsViews.clear(); + for (EmojiSection emojiSection : sections) { + EmojiSectionView sectionView = new EmojiSectionView(getContext()); + sectionView.setId(R.id.btn_section); + sectionView.setSection(emojiSection); + sectionView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + sectionView.setForceWidth(Screen.dp(35)); + addView(sectionView); + emojiSectionsViews.add(sectionView); + } + } + + public void setOnButtonClickListener (View.OnClickListener listener) { + for (EmojiSectionView view : emojiSectionsViews) { + view.setOnClickListener(listener); + } + } + + public void setThemeInvalidateListener (ViewController themeProvider) { + if (themeProvider != null) { + for (EmojiSectionView view : emojiSectionsViews) { + themeProvider.addThemeInvalidateListener(view); + } + } + } + + public void setSelectedObject (EmojiSection section, boolean animated) { + if (section != null) { + for (int i = 0; i < emojiSections.size(); i++) { + if (emojiSections.get(i).index == section.index) { + setSelectedIndex(i, animated); + return; + } + } + } + setSelectedIndex(-1, animated); + } + + private void setSelectedIndex (int index, boolean animated) { + if (currentSelectedIndex >= 0) { + emojiSections.get(currentSelectedIndex).setFactor(0f, animated); + } + currentSelectedIndex = index; + if (currentSelectedIndex >= 0) { + emojiSections.get(currentSelectedIndex).setFactor(1f, animated); + } + + expandAnimator.setValue(currentSelectedIndex >= 0, animated); + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + int defaultWidth = Screen.dp(44); + int expandedWidth = Math.max(Screen.dp(35 * emojiSectionsViews.size() + 9), defaultWidth); + int width = MathUtils.fromTo(defaultWidth, expandedWidth, expandAnimator.getFloatValue()); + super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), heightMeasureSpec); + updatePositions(); + } + + private void updatePositions () { + final float factor = expandAnimator.getFloatValue(); + for (int a = 0; a < emojiSectionsViews.size(); a++) { + EmojiSectionView view = emojiSectionsViews.get(a); + view.setForceWidth(Screen.dp(35)); + view.setTranslationX(Screen.dp(4.5f + 35 * a)); + if (a != 0) { + view.setAlpha(factor); + } + } + bgRect.set(Screen.dp(2), Screen.dp(4), getMeasuredWidth() - Screen.dp(2), getMeasuredHeight() - Screen.dp(4)); + invalidate(); + } + + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + requestLayout(); + } + + @Override + protected void dispatchDraw (Canvas canvas) { + canvas.drawRoundRect(bgRect, Screen.dp(20), Screen.dp(20), + Paints.fillingPaint(ColorUtils.alphaColor(expandAnimator.getFloatValue(), Theme.backgroundColor()))); + super.dispatchDraw(canvas); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiLayoutRecyclerController.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiLayoutRecyclerController.java new file mode 100644 index 0000000000..4116ecbbaf --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiLayoutRecyclerController.java @@ -0,0 +1,1066 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 18/08/2023 + */ +package org.thunderdog.challegram.widget.emoji; + +import android.content.Context; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.IdRes; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.component.chat.EmojiView; +import org.thunderdog.challegram.component.emoji.MediaStickersAdapter; +import org.thunderdog.challegram.component.sticker.StickerSmallView; +import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.config.Config; +import org.thunderdog.challegram.data.TGDefaultEmoji; +import org.thunderdog.challegram.data.TGStickerSetInfo; +import org.thunderdog.challegram.emoji.Emoji; +import org.thunderdog.challegram.emoji.RecentEmoji; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.EmojiMediaType; +import org.thunderdog.challegram.telegram.StickersListener; +import org.thunderdog.challegram.telegram.TGLegacyManager; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibEmojiManager; +import org.thunderdog.challegram.telegram.TdlibThread; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.tool.Views; +import org.thunderdog.challegram.v.CustomRecyclerView; +import org.thunderdog.challegram.v.RtlGridLayoutManager; +import org.thunderdog.challegram.widget.EmojiLayout; + +import java.util.ArrayList; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.StringUtils; +import me.vkryl.td.Td; + +public class EmojiLayoutRecyclerController extends ViewController implements + StickerSmallView.StickerMovementCallback, + TGLegacyManager.EmojiLoadListener, + Emoji.EmojiChangeListener, + TdlibEmojiManager.Watcher, + StickersListener { + + private static final int SCROLL_BY_SECTION_LIMIT = 8; + + private final @IdRes int controllerId; + protected @EmojiMediaType int mediaType; + + protected Callback callbacks; + protected RtlGridLayoutManager manager; + public CustomRecyclerView recyclerView; + protected MediaStickersAdapter adapter; + protected int spanCount; + + private ArrayList classicEmojiSets; + public ArrayList stickerSets; + + public EmojiLayoutRecyclerController (Context context, Tdlib tdlib, @IdRes int controllerId) { + super(context, tdlib); + this.controllerId = controllerId; + this.mediaType = EmojiLayout.getEmojiMediaType(controllerId); + } + + @Override + public int getId () { + return controllerId; + } + + @Override + public void setArguments (Callback args) { + super.setArguments(args); + this.callbacks = args; + } + + @Override + protected View onCreateView (Context context) { + manager = new RtlGridLayoutManager(context, spanCount = Math.max(1, calculateSpanCount())).setAlignOnly(true); + manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize (int position) { + int type = adapter.getItem(position).viewType; + return (type == MediaStickersAdapter.StickerHolder.TYPE_DEFAULT_EMOJI || type == MediaStickersAdapter.StickerHolder.TYPE_STICKER) ? 1 : spanCount; + } + }); + + recyclerView = new CustomRecyclerView(context) { + @Override + protected void onMeasure (int widthSpec, int heightSpec) { + super.onMeasure(widthSpec, heightSpec); + checkWidth(getMeasuredWidth()); + } + }; + recyclerView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + recyclerView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? View.OVER_SCROLL_IF_CONTENT_SCROLLS :View.OVER_SCROLL_NEVER); + recyclerView.setHasFixedSize(true); + recyclerView.setLayoutManager(manager); + recyclerView.setAdapter(adapter); + adapter.setManager(manager); + + return recyclerView; + } + + public void setAdapter (MediaStickersAdapter adapter) { + this.adapter = adapter; + } + + @Nullable public RtlGridLayoutManager getManager () { + return manager; + } + + public int getSpanCount () { + return spanCount; + } + + + private int lastMeasuredWidth; + + private void checkWidth (int width) { + if (width != 0 && lastMeasuredWidth != width) { + lastMeasuredWidth = width; + checkSpanCount(); + } + } + + public void checkSpanCount () { + if (manager != null) { + int spanCount = Math.max(1, calculateSpanCount()); + if (this.spanCount != spanCount) { + this.spanCount = spanCount; + manager.setSpanCount(spanCount); + } + } + } + + public void clearAllItems () { + clearAllItems(null); + } + + public void clearAllItems (@Nullable MediaStickersAdapter.StickerItem item) { + this.classicEmojiSets = null; + this.stickerSets = null; + this.lastStickerSetInfo = null; + if (item != null) { + adapter.setItem(item); + } else { + adapter.removeRange(0, adapter.getItemCount()); + } + } + + public void setDefaultEmojiPacks (ArrayList stickerSets, ArrayList items) { + this.classicEmojiSets = stickerSets; + this.stickerSets = stickerSets; + this.lastStickerSetInfo = null; + adapter.addItems(modifyStickers(items)); + TGLegacyManager.instance().addEmojiListener(this); + Emoji.instance().addEmojiChangeListener(this); + } + + public void setStickers (ArrayList stickerSets, ArrayList items) { + this.lastStickerSetInfo = null; + if (classicEmojiSets != null) { + this.stickerSets = new ArrayList<>(classicEmojiSets.size() + stickerSets.size()); + this.stickerSets.addAll(classicEmojiSets); + this.stickerSets.addAll(stickerSets); + } else { + this.stickerSets = stickerSets; + } + + adapter.addItems(modifyStickers(items)); + } + + public void addStickers (ArrayList stickerSets, ArrayList items) { + this.stickerSets.addAll(stickerSets); + this.adapter.addItems(modifyStickers(items)); + } + + public int getItemsHeight (boolean showRecentTitle) { + return adapter.measureScrollTop(adapter.getItemCount(), spanCount, Integer.MAX_VALUE - 1, stickerSets, recyclerView, showRecentTitle); + } + + public void applyStickerSet (TdApi.StickerSet stickerSet, TGStickerObj.DataProvider dataProvider) { + applyStickerSet(stickerSet, dataProvider, true); + } + + public void applyStickerSet (TdApi.StickerSet stickerSet, TGStickerObj.DataProvider dataProvider, boolean allowRefreshWithoutAdapterNotification) { + if (stickerSets == null || stickerSets.isEmpty()) return; + + final int actualSize = stickerSet.stickers.length; + int i = 0; + for (TGStickerSetInfo oldStickerSet : stickerSets) { + if (oldStickerSet.isSystem()) { + i++; + continue; + } + if (oldStickerSet.getId() == stickerSet.id) { + oldStickerSet.setStickerSet(stickerSet); + final int oldSize = oldStickerSet.getSize(); + // If something has suddenly changed with this sticker set + if (oldSize != actualSize) { + if (actualSize == 0) { + if (callbacks != null) { + callbacks.setIgnoreMovement(true); + } + stickerSets.remove(i); + if (stickerSets.isEmpty()) { + adapter.setItem(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_NO_STICKERSETS)); + } else { + // Shifting next sticker sets bounds + int startIndex; + if (i != 0) { + TGStickerSetInfo prevStickerSet = stickerSets.get(i - 1); + startIndex = prevStickerSet.getStartIndex() + prevStickerSet.getSize() + 1; + } else { + startIndex = 1; + } + for (int j = i; j < stickerSets.size(); j++) { + TGStickerSetInfo nextStickerSet = stickerSets.get(j); + nextStickerSet.setStartIndex(startIndex); + startIndex += nextStickerSet.getSize() + 1; + } + adapter.removeRange(oldStickerSet.getStartIndex(), oldStickerSet.getSize() + 1); + } + + if (callbacks != null) { + callbacks.setIgnoreMovement(false); + } + + return; + } else { + oldStickerSet.setSize(actualSize); + + // Shifting next sticker sets bounds + int startIndex = oldStickerSet.getStartIndex() + actualSize + 1; + for (int j = i + 1; j < stickerSets.size(); j++) { + TGStickerSetInfo nextStickerSet = stickerSets.get(j); + nextStickerSet.setStartIndex(startIndex); + startIndex += nextStickerSet.getSize() + 1; + } + + if (actualSize < oldSize) { + adapter.removeRange(oldStickerSet.getStartIndex() + 1 + actualSize, oldSize - actualSize); + } else { + ArrayList items = new ArrayList<>(actualSize - oldSize); + for (int j = oldSize; j < actualSize; j++) { + TdApi.Sticker sticker = stickerSet.stickers[j]; + TGStickerObj obj = new TGStickerObj(tdlib, sticker, sticker.fullType, stickerSet.emojis[j].emojis); + obj.setStickerSetId(stickerSet.id, stickerSet.emojis[j].emojis); + obj.setDataProvider(dataProvider); + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, modifySticker(obj))); + } + adapter.insertRange(oldStickerSet.getStartIndex() + 1 + oldSize, modifyStickers(items)); + } + } + + if (callbacks != null) { + callbacks.setIgnoreMovement(false); + } + } + + for (int stickerIndex = oldStickerSet.getCoverCount(), j = oldStickerSet.getStartIndex() + 1 + oldStickerSet.getCoverCount(); stickerIndex < stickerSet.stickers.length; stickerIndex++, j++) { + MediaStickersAdapter.StickerItem item = adapter.getItem(j); + TdApi.Sticker sticker = stickerSet.stickers[stickerIndex]; + if (item.sticker != null) { + item.sticker.set(tdlib, sticker, sticker.fullType, stickerSet.emojis[stickerIndex].emojis); + modifySticker(item.sticker); + } + + View view = recyclerView != null ? manager.findViewByPosition(j) : null; + if (view instanceof StickerSmallView && allowRefreshWithoutAdapterNotification) { + ((StickerSmallView) view).refreshSticker(); + } else { + adapter.notifyItemChanged(j); + } + } + + break; + } + i++; + } + + } + + public int indexOfStickerSet (TGStickerSetInfo stickerSet) { + if (stickerSets != null) { + if (stickerSet.isFakeClassicEmoji()) { + for (TGStickerSetInfo oldStickerSet : stickerSets) { + if (stickerSet.getTitleRes() == oldStickerSet.getTitleRes()) { + return stickerSet.getStartIndex(); + } + } + } else { + for (TGStickerSetInfo oldStickerSet : stickerSets) { + if (stickerSet.getId() == oldStickerSet.getId()) { + return stickerSet.getStartIndex(); + } + } + } + } + return -1; + } + + public int indexOfStickerSetById (long setId) { + int index = 0; + for (TGStickerSetInfo setInfo : stickerSets) { + if (!setInfo.isSystem() && !setInfo.isFakeClassicEmoji()) { + if (setInfo.getId() == setId) { + return index; + } + index++; + } + } + return -1; + } + + public @Nullable TGStickerSetInfo lastStickerSetInfo; + private int lastStickerSetIndex; + + private void setLastStickerSetInfo (TGStickerSetInfo info, int index) { + lastStickerSetInfo = info; + lastStickerSetIndex = index; + } + + public int indexOfStickerSetByAdapterPosition (int position) { + if (position == 0) { + return 0; + } + if (stickerSets != null) { + if (lastStickerSetInfo != null) { + if (position >= lastStickerSetInfo.getStartIndex() && position < lastStickerSetInfo.getEndIndex()) { + return lastStickerSetIndex; + } else if (position >= lastStickerSetInfo.getEndIndex()) { + for (int i = lastStickerSetIndex + 1; i < stickerSets.size(); i++) { + TGStickerSetInfo oldStickerSet = stickerSets.get(i); + if (position >= oldStickerSet.getStartIndex() && position < oldStickerSet.getEndIndex()) { + setLastStickerSetInfo(oldStickerSet, i); + return lastStickerSetIndex; + } + } + } else if (position < lastStickerSetInfo.getStartIndex()) { + for (int i = lastStickerSetIndex - 1; i >= 0; i--) { + TGStickerSetInfo oldStickerSet = stickerSets.get(i); + if (position >= oldStickerSet.getStartIndex() && position < oldStickerSet.getEndIndex()) { + setLastStickerSetInfo(oldStickerSet, i); + return lastStickerSetIndex; + } + } + } + } + int i = 0; + for (TGStickerSetInfo oldStickerSet : stickerSets) { + if (position >= oldStickerSet.getStartIndex() && position < oldStickerSet.getEndIndex()) { + setLastStickerSetInfo(oldStickerSet, i); + return lastStickerSetIndex; + } + i++; + } + } + return -1; + } + + public int getStickerSetSection () { + return getStickerSetSection(0); + } + + public int getStickerSetSection (int offset) { + if (spanCount == 0 || manager == null) { + return -1; + } + int i = Views.findFirstCompletelyVisibleItemPositionWithOffset(manager, offset); + if (i != -1) { + int r = indexOfStickerSetByAdapterPosition(i); + return r; + } + return 0; + } + + public int getStickersScrollY (boolean showRecentTitle) { + if (spanCount == 0 || manager == null) { + return 0; + } + int i = manager.findFirstVisibleItemPosition(); + if (i != -1) { + View v = manager.findViewByPosition(i); + int additional = v != null ? -v.getTop() : 0; + int stickerSet = indexOfStickerSetByAdapterPosition(i); + return additional + adapter.measureScrollTop(i, spanCount, stickerSet, stickerSets, recyclerView, showRecentTitle); + } + return 0; + } + + + public interface TGStickerObjModifier { + TGStickerObj modifyStickerObj (TGStickerObj sticker); + } + + private TGStickerObjModifier stickerObjModifier; + + public void setStickerObjModifier (TGStickerObjModifier stickerObjModifier) { + this.stickerObjModifier = stickerObjModifier; + } + + public TGStickerObj modifySticker (TGStickerObj sticker) { + return stickerObjModifier != null ? stickerObjModifier.modifyStickerObj(sticker) : sticker; + } + + public ArrayList modifyStickers (ArrayList items) { + for (MediaStickersAdapter.StickerItem item: items) { + if (item.sticker == null) continue; + modifySticker(item.sticker); + } + return items; + } + + public void invalidateStickerObjModifiers () { + modifyStickers(adapter.getItems()); + } + + private FactorAnimator lastScrollAnimator; + + public boolean scrollAnimationIsActive () { + return lastScrollAnimator != null && lastScrollAnimator.isAnimating(); + } + + public void scrollAnimatedImpl (final int scrollDiff, final int currentSection, final int futureSection) { + final int[] totalScrolled = new int[1]; + + if (lastScrollAnimator != null) { + lastScrollAnimator.cancel(); + } + recyclerView.setScrollDisabled(true); + setIgnoreRequests(true, stickerSets.get(futureSection).getId()); + if (callbacks != null) { + callbacks.setIgnoreMovement(true); + callbacks.setCurrentStickerSectionByPosition(controllerId, futureSection, true, true); + } + + lastScrollAnimator = new FactorAnimator(0, new FactorAnimator.Target() { + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + int diff = (int) ((float) scrollDiff * factor); + recyclerView.scrollBy(0, diff - totalScrolled[0]); + totalScrolled[0] = diff; + } + + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + recyclerView.setScrollDisabled(false); + setIgnoreRequests(false, 0); + if (callbacks != null) { + callbacks.setIgnoreMovement(false); + } + } + }, AnimatorUtils.DECELERATE_INTERPOLATOR, Math.min(450, Math.max(250, Math.abs(currentSection - futureSection) * 150))); + lastScrollAnimator.animateTo(1f); + } + + private boolean ignoreRequests; + private long ignoreException; + + private void setIgnoreRequests (boolean ignoreRequests, long exceptSetId) { + if (this.ignoreRequests != ignoreRequests) { + this.ignoreRequests = ignoreRequests; + this.ignoreException = exceptSetId; + if (!ignoreRequests && manager != null) { + final int firstVisiblePosition = manager.findFirstVisibleItemPosition(); + final int lastVisiblePosition = manager.findLastVisibleItemPosition(); + + if (firstVisiblePosition != RecyclerView.NO_POSITION && lastVisiblePosition != RecyclerView.NO_POSITION) { + for (int i = lastVisiblePosition; i >= firstVisiblePosition; i--) { + MediaStickersAdapter.StickerItem item = adapter.getItem(i); + if (item != null && item.viewType == MediaStickersAdapter.StickerHolder.TYPE_STICKER && item.sticker != null) { + item.sticker.requestRequiredInformation(); + } + } + } + } + } + } + + public int indexOfSticker (TGStickerObj sticker) { + if (stickerSets != null) { + for (TGStickerSetInfo stickerSet : stickerSets) { + boolean isFavorite = stickerSet.isFavorite(); + boolean isRecent = stickerSet.isRecent(); + boolean stickerFavorite = sticker.isFavorite(); + boolean stickerRecent = sticker.isRecent(); + if ((isFavorite && stickerFavorite) || (isRecent && stickerRecent) || (isFavorite == stickerFavorite && isRecent == stickerRecent && stickerSet.getId() == sticker.getStickerSetId())) { + return adapter.indexOfSticker(sticker, stickerSet.getStartIndex()); + } + } + } + return -1; + } + + public boolean isIgnoreRequests (long stickerSetId) { + return ignoreRequests && stickerSetId != ignoreException; + } + + @Override + public void onEmojiUpdated (boolean isPackSwitch) { + Views.invalidateChildren(recyclerView); + } + + @Override + public void destroy () { + super.destroy(); + TGLegacyManager.instance().removeEmojiListener(this); + Emoji.instance().removeEmojiChangeListener(this); + Views.destroyRecyclerView(recyclerView); + } + + public void scrollToStickerSet (int stickerSetIndex, boolean showRecentTitle, boolean animated) { + scrollToStickerSet(stickerSetIndex, EmojiLayout.getHeaderSize() + EmojiLayout.getHeaderPadding(), showRecentTitle, animated); + } + + public void scrollToStickerSet (int stickerSetIndex, int scrollOffset, boolean showRecentTitle, boolean animated) { + scrollToStickerSet(stickerSetIndex, scrollOffset, Integer.MIN_VALUE, showRecentTitle, animated); + } + + public void scrollToStickerSet (int stickerSetIndex, int scrollOffset, int forceScrollTop, boolean showRecentTitle, boolean animated) { + final int futureSection = indexOfStickerSetByAdapterPosition(stickerSetIndex); + if (futureSection == -1) { + return; + } + + recyclerView.stopScroll(); + + final int currentSection = getStickerSetSection(); + + if (!animated || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || callbacks == null || Math.abs(futureSection - currentSection) > SCROLL_BY_SECTION_LIMIT) { + if (callbacks != null) { + callbacks.setIgnoreMovement(true); + callbacks.setCurrentStickerSectionByPosition(controllerId, futureSection, true, true); + } + manager.scrollToPositionWithOffset(stickerSetIndex, stickerSetIndex == 0 ? 0 : scrollOffset); + UI.post(() -> { + if (callbacks != null) { + callbacks.setIgnoreMovement(false); + } + }); + } else { + final int scrollTop; + + if (forceScrollTop != Integer.MIN_VALUE) { + scrollTop = forceScrollTop; + } else if (stickerSetIndex == 0) { + scrollTop = 0; + } else { + scrollTop = Math.max(0, adapter.measureScrollTop(stickerSetIndex, spanCount, futureSection, stickerSets, recyclerView, showRecentTitle) - scrollOffset); + } + + final int currentScrollTop = getStickersScrollY(showRecentTitle); + final int scrollDiff = scrollTop - currentScrollTop; + + scrollAnimatedImpl(scrollDiff, currentSection, futureSection); + } + } + + public TGStickerSetInfo getStickerSetBySectionIndex (int index) { + for (TGStickerSetInfo set: stickerSets) { + if (set.isFakeClassicEmoji() && set.getFakeClassicEmojiSectionId() == index) { + return set; + } + } + return null; + } + + /* * */ + + @Override + public void setStickerPressed (StickerSmallView view, TGStickerObj sticker, boolean isPressed) { + int i = indexOfSticker(sticker); + if (i != -1) { + adapter.setStickerPressed(i, isPressed, manager); + } + } + + @Override + public boolean canFindChildViewUnder (StickerSmallView view, int recyclerX, int recyclerY) { + return callbacks != null && callbacks.canFindChildViewUnder(controllerId, view, recyclerX, recyclerY); + } + + @Override + public void onStickerPreviewOpened (StickerSmallView view, TGStickerObj sticker) { + if (callbacks != null) { + callbacks.onSectionInteracted(mediaType, false); + } + } + + @Override + public void onStickerPreviewChanged (StickerSmallView view, TGStickerObj otherOrThisSticker) { + if (callbacks != null) { + callbacks.onSectionInteracted(mediaType, false); + } + } + + @Override + public void onStickerPreviewClosed (StickerSmallView view, TGStickerObj thisSticker) { + if (callbacks != null) { + callbacks.onSectionInteracted(mediaType, true); + } + } + + @Override + public boolean needsLongDelay (StickerSmallView view) { + return false; + } + + @Override + public int getStickersListTop () { + return Views.getLocationInWindow(recyclerView)[1]; + } + + @Override + public int getViewportHeight () { + return -1; + } + + @Override + public boolean onStickerClick (StickerSmallView view, View clickView, TGStickerObj sticker, boolean isMenuClick, TdApi.MessageSendOptions sendOptions) { + if (callbacks != null) { + int i = indexOfStickerSetById(sticker.getStickerSetId()); + return callbacks.onStickerClick(controllerId, view, clickView, i != -1 ? stickerSets.get(i): null, sticker, isMenuClick, sendOptions); + } + return false; + } + + @Override + public long getStickerOutputChatId () { + return callbacks != null ? callbacks.findOutputChatId() : 0; + } + + /* * */ + + private int getHeaderItemCount () { + if (!stickerSets.isEmpty()) { + return stickerSets.get(0).getStartIndex(); + } + + return 0; + } + + public int getRecentItemCount () { + if (!stickerSets.isEmpty()) { + return stickerSets.get(0).getSize(); + } + + return 0; + } + + public void onResetRecentEmoji () { + int headerItemsCount = getHeaderItemCount(); + int recentItemsCount = getRecentItemCount(); + + if (recentItemsCount > 0) { + for (int i = 0; i < recentItemsCount; i++) { + adapter.getItems().remove(headerItemsCount /*+ i*/); + } + } + ArrayList recents = Emoji.instance().getRecents(); + int newRecentItemsCount = recents.size(); + + for (int i = 0; i < stickerSets.size(); i++) { + TGStickerSetInfo info = stickerSets.get(i); + if (i == 0) { + info.setSize(newRecentItemsCount); + } else { + info.setStartIndex(info.getStartIndex() + (newRecentItemsCount - recentItemsCount)); + } + } + + adapter.getItems().ensureCapacity(adapter.getItems().size() + newRecentItemsCount); + int i = headerItemsCount; + for (RecentEmoji emoji : recents) { + adapter.getItems().add(i, new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_DEFAULT_EMOJI, new TGDefaultEmoji(emoji.emoji, true))); + i++; + } + + if (newRecentItemsCount > recentItemsCount) { + adapter.notifyItemRangeInserted(headerItemsCount + recentItemsCount, newRecentItemsCount - recentItemsCount); + } else if (newRecentItemsCount < recentItemsCount) { + adapter.notifyItemRangeRemoved(headerItemsCount + newRecentItemsCount, recentItemsCount - newRecentItemsCount); + } + adapter.notifyItemRangeChanged(headerItemsCount, Math.min(newRecentItemsCount, recentItemsCount)); + } + + @Override + public void moveEmoji (int oldIndex, int newIndex) { + if (callbacks != null) { + callbacks.setIgnoreMovement(true); + } + oldIndex += getHeaderItemCount(); + newIndex += getHeaderItemCount(); + MediaStickersAdapter.StickerItem item = adapter.getItems().remove(oldIndex); + adapter.getItems().add(newIndex, item); + adapter.notifyItemMoved(oldIndex, newIndex); + if (callbacks != null) { + recyclerView.post(() -> callbacks.setIgnoreMovement(false)); + } + } + + @Override + public void addEmoji (int newIndex, RecentEmoji emoji) { + MediaStickersAdapter.StickerItem item = processRecentEmojiItem(emoji); + if (item == null) return; + + if (callbacks != null) { + callbacks.setIgnoreMovement(true); + } + newIndex += getHeaderItemCount(); + + for (int i = 0; i < stickerSets.size(); i++) { + TGStickerSetInfo info = stickerSets.get(i); + if (i == 0) { + info.setSize(info.getSize() + 1); + } else { + info.setStartIndex(info.getStartIndex() + 1); + } + } + adapter.getItems().add(newIndex, item); + adapter.notifyItemInserted(newIndex); + if (emoji.isCustomEmoji()) { + tdlib.emoji().performPostponedRequests(); + } + if (callbacks != null) { + recyclerView.post(() -> callbacks.setIgnoreMovement(false)); + } + } + + @Override + public void replaceEmoji (int newIndex, RecentEmoji emoji) { + MediaStickersAdapter.StickerItem item = processRecentEmojiItem(emoji); + if (item == null) { + return; + } + + adapter.replaceItem(newIndex + getHeaderItemCount(), item); + } + + @Override + public void removeEmoji (int oldIndex, RecentEmoji emoji) { + MediaStickersAdapter.StickerItem item = processRecentEmojiItem(emoji); + if (item == null) { + return; + } + adapter.removeRange(oldIndex + getHeaderItemCount(), 1); + } + + @Override + public void onToneChanged (@Nullable String newDefaultTone) { + int firstVisiblePosition = manager.findFirstVisibleItemPosition(); + int lastVisiblePosition = manager.findLastVisibleItemPosition(); + if (firstVisiblePosition == -1 || lastVisiblePosition == -1) { + adapter.notifyItemRangeChanged(0, adapter.getItemCount()); + return; + } + + int lastChangedPosition = -1; + int lastChangedCount = 0; + final ArrayList changes = new ArrayList<>(); + for (int i = firstVisiblePosition; i <= lastVisiblePosition; i++) { + MediaStickersAdapter.StickerItem item = adapter.getItems().get(i); + boolean changed = item.viewType == MediaStickersAdapter.StickerHolder.TYPE_DEFAULT_EMOJI && item.defaultEmoji != null && item.defaultEmoji.canBeColored(); + if (changed) { + if (lastChangedPosition == -1) { + lastChangedPosition = i; + } + lastChangedCount++; + } else if (lastChangedPosition != -1) { + changes.add(new int[] {lastChangedPosition, lastChangedCount}); + lastChangedPosition = -1; + lastChangedCount = 0; + } + } + if (lastChangedPosition != -1) { + changes.add(new int[] {lastChangedPosition, lastChangedCount}); + } + for (int[] change : changes) { + if (change[1] == 1) { + adapter.notifyItemChanged(change[0]); + } else { + adapter.notifyItemRangeChanged(change[0], change[1]); + } + } + if (firstVisiblePosition > 0) { + adapter.notifyItemRangeChanged(0, firstVisiblePosition); + } + if (lastVisiblePosition < adapter.getItemCount() - 1) { + adapter.notifyItemRangeChanged(lastVisiblePosition + 1, adapter.getItemCount() - lastVisiblePosition); + } + } + + @Override + public void onCustomToneApplied (String emoji, @Nullable String newTone, @Nullable String[] newOtherTones) { + int firstVisiblePosition = manager.findFirstVisibleItemPosition(); + int lastVisiblePosition = manager.findLastVisibleItemPosition(); + + int i = 0; + for (MediaStickersAdapter.StickerItem item : adapter.getItems()) { + if (item.viewType == MediaStickersAdapter.StickerHolder.TYPE_DEFAULT_EMOJI && item.defaultEmoji != null && StringUtils.equalsOrBothEmpty(item.defaultEmoji.emoji, emoji)) { + View view = i >= firstVisiblePosition && i <= lastVisiblePosition ? manager.findViewByPosition(i) : null; + if (!(view instanceof EmojiView) || !((EmojiView) view).applyTone(emoji, newTone, newOtherTones)) { + adapter.notifyItemChanged(i); + } + } + i++; + } + } + + public ArrayList makeRecentEmojiItems () { + ArrayList recents = Emoji.instance().getRecents(); + ArrayList items = new ArrayList<>(recents.size()); + for (RecentEmoji recentEmoji : recents) { + MediaStickersAdapter.StickerItem item = processRecentEmojiItem(recentEmoji); + if (item == null) { + continue; + } + items.add(item); + } + tdlib.emoji().performPostponedRequests(); + return items; + } + + private boolean onlyClassicEmoji; + + public void setOnlyClassicEmoji (boolean onlyClassicEmoji) { + this.onlyClassicEmoji = onlyClassicEmoji; + } + + @Nullable + private MediaStickersAdapter.StickerItem processRecentEmojiItem (RecentEmoji recentEmoji) { + if (recentEmoji.isCustomEmoji()) { + if (onlyClassicEmoji) { + return null; + } + final TdlibEmojiManager.Entry entry = tdlib.emoji().findOrPostponeRequest(recentEmoji.customEmojiId, this); + final TGStickerObj stickerObj; + if (entry != null && entry.value != null) { + stickerObj = new TGStickerObj(tdlib, entry.value, entry.value.fullType, null); + stickerObj.setIsRecent(); + } else { + stickerObj = new TGStickerObj(tdlib, null, new TdApi.StickerFullTypeCustomEmoji(recentEmoji.customEmojiId, false), null); + stickerObj.setTag(recentEmoji.customEmojiId); + stickerObj.setIsRecent(); + } + return new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_STICKER, modifySticker(stickerObj)); + } else { + return new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_DEFAULT_EMOJI, new TGDefaultEmoji(recentEmoji.emoji, true)); + } + } + + @TdlibThread + @Override + public void onCustomEmojiLoaded (TdlibEmojiManager context, TdlibEmojiManager.Entry entry) { + UI.post(() -> onRecentCustomEmojiLoaded(entry)); // bad solution: Multiple call to UI.post + } + + public void onRecentCustomEmojiLoaded (TdlibEmojiManager.Entry entry) { + if (entry == null || entry.value == null || stickerSets == null || stickerSets.isEmpty()) return; + + int startIndex = stickerSets.get(0).getStartIndex(); + int endIndex = stickerSets.get(0).getEndIndex(); + for (int a = startIndex; a < endIndex; a++) { + TGStickerObj stickerObj = adapter.getItem(a).sticker; + if (stickerObj != null && stickerObj.getTag() == Td.customEmojiId(entry.value)) { + stickerObj.set(tdlib, entry.value, entry.value.fullType, null); + modifySticker(stickerObj); + adapter.notifyItemChanged(a); + return; + } + } + } + + /* * */ + + private int ignoreStickersScroll; + + public boolean isNeedIgnoreScroll () { + return ignoreStickersScroll != 0; + } + + private void beforeStickerChanges () { + ignoreStickersScroll++; + } + + private void resetScrollCache () { + // lastStickerSetInfo = null; // FIXME removing current sticker set does not update selection + // ignoreStickersScroll--; + + if (callbacks != null) { + callbacks.resetScrollState(true); // FIXME upd: ... fixme what? + } + UI.post(() -> { + /*if (emojiLayout != null && contentView.getCurrentSection() == SECTION_STICKERS) { + emojiLayout.setCurrentStickerSectionByPosition(getStickerSetSection(), true, true); + emojiLayout.resetScrollState(true); + }*/ + ignoreStickersScroll--; + }, 400); + } + + public void addStickerSet (TGStickerSetInfo stickerSet, ArrayList items, int index) { + if (index < 0 || index >= stickerSets.size()) { + return; + } + + beforeStickerChanges(); + + if (callbacks != null) { + callbacks.onAddStickerSection(controllerId, index, stickerSet); + } + + int startIndex = stickerSets.get(index).getStartIndex(); + stickerSets.add(index, stickerSet); + for (int i = index; i < stickerSets.size(); i++) { + TGStickerSetInfo nextStickerSet = stickerSets.get(i); + nextStickerSet.setStartIndex(startIndex); + startIndex += nextStickerSet.getSize() + 1; + } + + adapter.addRange(stickerSet.getStartIndex(), modifyStickers(items)); + resetScrollCache(); + } + + public int removeStickerSet (TGStickerSetInfo stickerSet) { + int i = stickerSets.indexOf(stickerSet); + if (i != -1) { + beforeStickerChanges(); + stickerSets.remove(i); + if (callbacks != null) { + callbacks.onRemoveStickerSection(controllerId, i); + } + int startIndex = stickerSet.getStartIndex(); + adapter.removeRange(startIndex, stickerSet.getSize() + 1); + for (int j = i; j < stickerSets.size(); j++) { + TGStickerSetInfo nextStickerSet = stickerSets.get(j); + nextStickerSet.setStartIndex(startIndex); + startIndex += nextStickerSet.getSize() + 1; + } + resetScrollCache(); + } + return i; + } + + public void moveStickerSet (int oldPosition, int newPosition) { + beforeStickerChanges(); + + if (callbacks != null) { + callbacks.onMoveStickerSection(controllerId, oldPosition, newPosition); + } + + TGStickerSetInfo stickerSet = stickerSets.remove(oldPosition); + + final int startIndex = stickerSet.getStartIndex(); + final int itemCount = stickerSet.getSize() + 1; + + int startPosition; + if (oldPosition < newPosition) { + startPosition = startIndex; + } else { + startPosition = stickerSets.get(newPosition).getStartIndex(); + } + + stickerSets.add(newPosition, stickerSet); + + for (int i = Math.min(oldPosition, newPosition); i < stickerSets.size(); i++) { + TGStickerSetInfo nextSet = stickerSets.get(i); + nextSet.setStartIndex(startPosition); + startPosition += nextSet.getSize() + 1; + } + + adapter.moveRange(startIndex, itemCount, stickerSet.getStartIndex()); + resetScrollCache(); + } + + + /**/ + + public interface SpanCountDelegate { + int calculateSpanCount (int controllerId); + } + + private SpanCountDelegate spanCountDelegate; + private int forceSpanCount; + + public final void setItemWidth (int minSpan, int itemWidthDp) { + setSpanCount(id -> { + final int width = recyclerView != null ? + recyclerView.getMeasuredWidth() - recyclerView.getPaddingLeft() - recyclerView.getPaddingRight(): + Screen.currentWidth(); + return calculateSpanCount(width, minSpan, Screen.dp(itemWidthDp)); + }); + } + + public static int calculateSpanCount (int width, int minSpan, int itemWidth) { + return Math.max(minSpan, width / itemWidth); + } + + public final void setSpanCount (int spanCount) { + forceSpanCount = spanCount; + checkSpanCount(); + } + + public final void setSpanCount (SpanCountDelegate spanCountDelegate) { + this.spanCountDelegate = spanCountDelegate; + checkSpanCount(); + } + + private int calculateSpanCount () { + if (forceSpanCount != 0) { + return forceSpanCount; + } else if (spanCountDelegate != null) { + return spanCountDelegate.calculateSpanCount(getId()); + } + return 1; + } + + /* */ + + public interface Callback { + void setIgnoreMovement (boolean silent); + void resetScrollState (boolean silent); + void moveHeader (int totalDy); + void setHasNewHots (@IdRes int controllerId, boolean hasHots); + + boolean onStickerClick (@IdRes int controllerId, StickerSmallView view, View clickView, TGStickerSetInfo stickerSet, TGStickerObj sticker, boolean isMenuClick, TdApi.MessageSendOptions sendOptions); + boolean canFindChildViewUnder (@IdRes int controllerId, StickerSmallView view, int recyclerX, int recyclerY); + + Context getContext (); + boolean isUseDarkMode (); + long findOutputChatId (); + void onSectionInteracted (@EmojiMediaType int mediaType, boolean interactionFinished); + void onSectionInteractedScroll (@EmojiMediaType int mediaType, boolean moved); + void setCurrentStickerSectionByPosition (@IdRes int controllerId, int i, boolean isStickerSection, boolean animated); + + void onAddStickerSection (@IdRes int controllerId, int section, TGStickerSetInfo info); + void onMoveStickerSection (@IdRes int controllerId, int fromSection, int toSection); + void onRemoveStickerSection (@IdRes int controllerId, int section); + + @Deprecated + boolean isAnimatedEmojiOnly (); + float getHeaderHideFactor (); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiLayoutSectionPager.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiLayoutSectionPager.java new file mode 100644 index 0000000000..98888c253a --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiLayoutSectionPager.java @@ -0,0 +1,148 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 18/08/2023 + */ +package org.thunderdog.challegram.widget.emoji; + +import android.content.Context; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import org.thunderdog.challegram.core.Lang; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.FactorAnimator; + +public abstract class EmojiLayoutSectionPager extends FrameLayout implements FactorAnimator.Target { + private static final int CHANGE_SECTION_ANIMATOR = 0; + + private View currentSectionView; + private int nextSection = -1; + private View nextSectionView; + private boolean sectionIsLeft; + private int currentSection; + + private FactorAnimator sectionAnimator; + private float sectionChangeFactor; + + public EmojiLayoutSectionPager (@NonNull Context context) { + super(context); + } + + public void init (int currentSection) { + this.currentSection = currentSection; + this.currentSectionView = getSectionView(currentSection); + addView(currentSectionView); + } + + public int getCurrentSection () { + return currentSection; + } + + public int getNextSection () { + return currentSection; + } + + public boolean canChangeSection () { + return sectionAnimator == null || (!sectionAnimator.isAnimating() && sectionAnimator.getFactor() == 0f && sectionChangeFactor == 0f); + } + + public boolean isAnimationNotActive () { + return sectionAnimator == null || !sectionAnimator.isAnimating(); + } + + public boolean isSectionStable () { + return sectionAnimator == null || sectionAnimator.getFactor() == 0f; + } + + public boolean changeSection (int sectionId, boolean fromLeft, int stickerSetSection) { + if (currentSection == sectionId || !canChangeSection()) { + return false; + } + + View sectionView = getSectionView(sectionId); + + this.nextSection = sectionId; + this.nextSectionView = sectionView; + this.sectionIsLeft = fromLeft; + + this.addView(sectionView); + + if (this.sectionAnimator == null) { + this.sectionAnimator = new FactorAnimator(CHANGE_SECTION_ANIMATOR, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180L); + } + + this.sectionAnimator.animateTo(1f); + this.onSectionChangeStart(currentSection, nextSection, stickerSetSection); + + return true; + } + + private void updatePositions () { + if (sectionIsLeft != Lang.rtl()) { + currentSectionView.setTranslationX((float) currentSectionView.getMeasuredWidth() * sectionChangeFactor); + if (nextSectionView != null) { + nextSectionView.setTranslationX((float) (-nextSectionView.getMeasuredWidth()) * (1f - sectionChangeFactor)); + } + } else { + currentSectionView.setTranslationX((float) (-currentSectionView.getMeasuredWidth()) * sectionChangeFactor); + if (nextSectionView != null) { + nextSectionView.setTranslationX((float) nextSectionView.getMeasuredWidth() * (1f - sectionChangeFactor)); + } + } + } + + private void applySection () { + removeView(currentSectionView); + + int oldSection = this.currentSection; + + currentSection = nextSection; + nextSection = -1; + currentSectionView = nextSectionView; + nextSectionView = null; + sectionAnimator.forceFactor(0f); + sectionChangeFactor = 0f; + + this.onSectionChangeEnd(oldSection, currentSection); + } + + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + if (id == CHANGE_SECTION_ANIMATOR) { + this.sectionChangeFactor = factor; + updatePositions(); + } + } + + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + if (id == CHANGE_SECTION_ANIMATOR) { + if (finalFactor == 1f) { + applySection(); + } + } + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + updatePositions(); + } + + protected abstract View getSectionView (int section); + protected abstract void onSectionChangeStart (int prevSection, int nextSection, int stickerSetSection); + protected abstract void onSectionChangeEnd (int prevSection, int currentSection); +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiLayoutTrendingController.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiLayoutTrendingController.java new file mode 100644 index 0000000000..85bcec567f --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiLayoutTrendingController.java @@ -0,0 +1,250 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 26/08/2023 + */ +package org.thunderdog.challegram.widget.emoji; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.IdRes; +import androidx.collection.LongSparseArray; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.component.emoji.MediaStickersAdapter; +import org.thunderdog.challegram.component.sticker.StickerSmallView; +import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.data.TGStickerSetInfo; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.ui.EmojiMediaListController; + +import java.util.ArrayList; + +import me.vkryl.core.lambda.CancellableRunnable; + +public class EmojiLayoutTrendingController extends EmojiLayoutRecyclerController implements TGStickerSetInfo.ViewCallback { + + public EmojiLayoutTrendingController (Context context, Tdlib tdlib, @IdRes int controllerId) { + super(context, tdlib, controllerId); + } + + public void setCallbacks (TGStickerObj.DataProvider dataProvider, TdApi.StickerType stickerType) { + this.dataProvider = dataProvider; + this.stickerType = stickerType; + } + + private TGStickerObj.DataProvider dataProvider; + private TdApi.StickerType stickerType; + private TdApi.TrendingStickerSets scheduledFeaturedSets; + public boolean trendingLoading, canLoadMoreTrending; + + + public void loadTrending (int offset, int limit, int cellCount) { + if (!trendingLoading) { + trendingLoading = true; + tdlib.client().send(new TdApi.GetTrendingStickerSets(stickerType, offset, limit), object -> { + final ArrayList parsedStickerSets = new ArrayList<>(); + final ArrayList items = new ArrayList<>(); + final int unreadItemCount; + + if (object.getConstructor() == TdApi.TrendingStickerSets.CONSTRUCTOR) { + TdApi.TrendingStickerSets trendingStickerSets = (TdApi.TrendingStickerSets) object; + if (offset == 0) + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_KEYBOARD_TOP)); + unreadItemCount = EmojiMediaListController.parseTrending(tdlib, parsedStickerSets, items, cellCount, trendingStickerSets.sets, dataProvider, this, false, false, null); + } else { + if (offset == 0) + items.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_COME_AGAIN_LATER)); + unreadItemCount = 0; + } + + UI.post(() -> addTrendingStickers(parsedStickerSets, items, unreadItemCount > 0, offset)); + }); + } + } + + private void addTrendingStickers (ArrayList trendingSets, ArrayList items, boolean hasUnread, int offset) { + if (offset != 0 && (!trendingLoading || offset != stickerSets.size())) + return; + + if (trendingSets != null) { + if (offset == 0) { + lastStickerSetInfo = null; + stickerSets.clear(); + } + stickerSets.addAll(trendingSets); + } + this.canLoadMoreTrending = trendingSets != null && !trendingSets.isEmpty(); + if (callbacks != null && (hasUnread || offset == 0)) { + callbacks.setHasNewHots(getId(), hasUnread); + } + if (offset == 0) { + if (recyclerView != null) { + recyclerView.stopScroll(); + manager.scrollToPositionWithOffset(0, 0); + } + adapter.setItems(items); + } else { + adapter.addItems(items); + } + this.trendingLoading = false; + } + + public void updateTrendingSets (long[] stickerSetIds) { + if (stickerSets == null) return; + + LongSparseArray installedStickerSets = new LongSparseArray<>(stickerSetIds.length); + for (long stickerSetId : stickerSetIds) { + installedStickerSets.put(stickerSetId, null); + } + for (TGStickerSetInfo stickerSet : stickerSets) { + int i = installedStickerSets.indexOfKey(stickerSet.getId()); + if (i >= 0) { + stickerSet.setIsInstalled(); + adapter.updateDone(stickerSet); + } else { + stickerSet.setIsNotInstalled(); + adapter.updateDone(stickerSet); + } + } + } + + private void applyScheduledFeaturedSets (TdApi.TrendingStickerSets sets) { + if (sets != null && !isDestroyed() && !trendingLoading) { + if (stickerSets != null && stickerSets.size() == sets.sets.length && !stickerSets.isEmpty()) { + boolean equal = true; + int i = 0; + for (TGStickerSetInfo stickerSetInfo : stickerSets) { + if (stickerSetInfo.getId() != sets.sets[i].id) { + equal = false; + break; + } + boolean visuallyChanged = stickerSetInfo.isViewed() != sets.sets[i].isViewed; + stickerSetInfo.updateState(sets.sets[i]); + if (visuallyChanged) { + adapter.updateState(stickerSetInfo); + } + i++; + } + if (equal) { + return; + } + } + + final ArrayList stickerItems = new ArrayList<>(sets.sets.length * 2 + 1); + final ArrayList stickerSetInfos = new ArrayList<>(sets.sets.length); + stickerItems.add(new MediaStickersAdapter.StickerItem(MediaStickersAdapter.StickerHolder.TYPE_KEYBOARD_TOP)); + final int unreadItemCount = EmojiMediaListController.parseTrending(tdlib, stickerSetInfos, stickerItems, 0, sets.sets, dataProvider, this, false, false, null); + addTrendingStickers(stickerSetInfos, stickerItems, unreadItemCount > 0, 0); + } + } + + @Override + public void applyStickerSet (TdApi.StickerSet stickerSet, TGStickerObj.DataProvider dataProvider) { + if (stickerSets == null || stickerSets.isEmpty()) { + return; + } + for (TGStickerSetInfo oldStickerSet : stickerSets) { + if (oldStickerSet.getId() == stickerSet.id) { + oldStickerSet.setStickerSet(stickerSet); + for (int stickerIndex = oldStickerSet.getCoverCount(), j = oldStickerSet.getStartIndex() + 1 + oldStickerSet.getCoverCount(); stickerIndex < Math.min(stickerSet.stickers.length - oldStickerSet.getCoverCount(), oldStickerSet.getCoverCount() + 4); stickerIndex++, j++) { + MediaStickersAdapter.StickerItem item = adapter.getItem(j); + if (item.sticker != null) { + TdApi.Sticker sticker = stickerSet.stickers[stickerIndex]; + item.sticker.set(tdlib, sticker, sticker.fullType, stickerSet.emojis[stickerIndex].emojis); + } + + View view = recyclerView != null ? manager.findViewByPosition(j) : null; + if (view instanceof StickerSmallView && view.getTag() == item) { + ((StickerSmallView) view).refreshSticker(); + } else { + adapter.notifyItemChanged(j); + } + } + break; + } + } + } + + public void scheduleFeaturedSets (TdApi.TrendingStickerSets stickerSets, boolean isSectionVisible) { + if (isSectionVisible) { + scheduledFeaturedSets = stickerSets; + } else { + scheduledFeaturedSets = null; + applyScheduledFeaturedSets(stickerSets); + } + } + + public void applyScheduledFeaturedSets () { + if (scheduledFeaturedSets != null) { + applyScheduledFeaturedSets(scheduledFeaturedSets); + scheduledFeaturedSets = null; + } + } + + public void onScrolledImpl (int dy, boolean showRecentTitle) { + if (callbacks != null) { + callbacks.moveHeader(getStickersScrollY(showRecentTitle)); + callbacks.onSectionInteractedScroll(mediaType, dy != 0); + } + if (!trendingLoading && canLoadMoreTrending) { + int lastVisiblePosition = manager.findLastVisibleItemPosition(); + if (lastVisiblePosition != -1) { + int index = stickerSets.indexOf(adapter.getItem(lastVisiblePosition).stickerSet); + if (index != -1 && index + 5 >= stickerSets.size()) { + loadTrending(stickerSets.size(), 25, adapter.getItemCount()); + } + } + } + } + + + + /* View sets */ + + @Override + public void viewStickerSet (TGStickerSetInfo stickerSetInfo) { + viewStickerSetInternal(stickerSetInfo.getId()); + } + + private LongSparseArray pendingViewStickerSets; + private CancellableRunnable viewSets; + + private void viewStickerSetInternal (long stickerSetId) { + if (pendingViewStickerSets == null) { + pendingViewStickerSets = new LongSparseArray<>(); + } else if (pendingViewStickerSets.indexOfKey(stickerSetId) >= 0) { + return; + } + pendingViewStickerSets.put(stickerSetId, true); + if (viewSets != null) { + viewSets.cancel(); + } + viewSets = new CancellableRunnable() { + @Override + public void act () { + if (pendingViewStickerSets != null && pendingViewStickerSets.size() > 0) { + final int size = pendingViewStickerSets.size(); + long[] setIds = new long[size]; + for (int i = 0; i < size; i++) { + setIds[i] = pendingViewStickerSets.keyAt(i); + } + pendingViewStickerSets.clear(); + tdlib.client().send(new TdApi.ViewTrendingStickerSets(setIds), tdlib.okHandler()); + } + } + }; + UI.post(viewSets, 750L); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiToneListView.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiToneListView.java new file mode 100644 index 0000000000..a5fb3ef5f3 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/EmojiToneListView.java @@ -0,0 +1,335 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 21/08/2023 + */ +package org.thunderdog.challegram.widget.emoji; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.Nullable; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.sticker.StickerSmallView; +import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.emoji.Emoji; +import org.thunderdog.challegram.emoji.EmojiInfo; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.EmojiData; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.Screen; + +import java.util.ArrayList; + +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.MathUtils; +import me.vkryl.core.StringUtils; + +public class EmojiToneListView extends FrameLayout { + public static final float ITEM_SIZE = 36f; + public static final float ITEM_PADDING = 4f; + public static final float CORNER_WIDTH = 18f; + public static final float CORNER_HEIGHT = 8f; + public static final float VIEW_PADDING_HORIZONTAL = 1f + (1f / 3f); + public static final float VIEW_PADDING_TOP = 1f + (1f / 3f); + public static final float VIEW_PADDING_BOTTOM = 4f + (2f / 3f); + public static final float EMOJI_DRAW_SIZE = 29f; + + private EmojiInfo[] tones; + private Drawable backgroundDrawable, cornerDrawable; + private ArrayList stickerViews; + private ArrayList stickerObjs; + private Tdlib tdlib; + private int emojiColorState; + private @Nullable TdApi.Sticker[] stickersFromLocal; + private @Nullable TdApi.Sticker[] stickersFromServer; + + public EmojiToneListView (Context context) { + super(context); + } + + public void init (ViewController themeProvider, Tdlib tdlib) { + this.tones = new EmojiInfo[EmojiData.emojiColors.length]; + this.backgroundDrawable = Theme.filteredDrawable(R.drawable.stickers_back_all, ColorId.overlayFilling, themeProvider); + this.cornerDrawable = Theme.filteredDrawable(R.drawable.stickers_back_arrow, ColorId.overlayFilling, themeProvider); + this.tdlib = tdlib; + } + + private View boundView; + private int offsetLeft; + + public void setAnchorView (View view, int offsetLeft) { + this.boundView = view; + this.offsetLeft = offsetLeft; + setPivotX(view.getMeasuredWidth() / 2f - offsetLeft); + setPivotY(Screen.dp(ITEM_SIZE + 4) + Screen.dp(3.5f) + Screen.dp(8f) / 2f); + } + + public View getAnchorView () { + return boundView; + } + + private int toneIndex = -1; + private int toneIndexVertical = -1; + + public int getToneIndex () { + return toneIndex; + } + + public int getToneIndexVertical () { + return toneIndexVertical; + } + + public void setEmoji (String emoji, String currentTone, int emojiColorState) { + this.emojiColorState = emojiColorState; + int i = 0; + for (String tone : EmojiData.emojiColors) { + if (tone == null && currentTone == null) { + toneIndex = 0; + } else if (StringUtils.equalsOrBothEmpty(tone, currentTone)) { + toneIndex = i; + } + tones[i] = Emoji.instance().getEmojiInfo(EmojiData.instance().colorize(emoji, tone)); + i++; + } + } + + @Nullable + public TGStickerObj getSelectedCustomEmoji () { + boolean hasToneEmoji = hasToneEmoji(); + if (hasToneEmoji && toneIndexVertical == 0 || toneIndexVertical == -1) { + return null; + } + int rowStart = hasToneEmoji ? 1 : 0; + int index = toneIndex; + for (int a = rowStart; a < toneIndexVertical; a++) { + index += getRowSize(a); + } + if (stickerObjs != null && index >= 0 && index < stickerObjs.size()) { + return stickerObjs.get(index); + } + return null; + } + + public void setCustomEmoji (@Nullable TdApi.Sticker[] stickersFromLocal, @Nullable TdApi.Sticker[] stickersFromServer) { + if ((stickersFromLocal == null || stickersFromLocal.length == 0) && (stickersFromServer == null || stickersFromServer.length == 0)) { + return; + } + + this.stickersFromLocal = stickersFromLocal; + this.stickersFromServer = stickersFromServer; + + stickerViews = new ArrayList<>(); + stickerObjs = new ArrayList<>(); + + for (int a = 0; a < 2; a++) { + TdApi.Sticker[] stickers = a == 0 ? stickersFromServer : stickersFromLocal; + if (stickers == null) { + continue; + } + for (int i = 0; i < Math.min(stickers.length, 6); i++) { + TdApi.Sticker sticker = stickers[i]; + TGStickerObj stickerObj = new TGStickerObj(tdlib, sticker, sticker.emoji, sticker.fullType); + StickerSmallView v = new StickerSmallView(getContext(), Screen.dp(2)); + v.setSticker(stickerObj); + v.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(ITEM_SIZE), Screen.dp(ITEM_SIZE))); + stickerObjs.add(stickerObj); + stickerViews.add(v); + addView(v); + } + } + } + + public boolean changeIndex (float x, float y) { + final int resV = MathUtils.clamp((int)((y - Screen.dp(VIEW_PADDING_TOP + ITEM_PADDING)) / Screen.dp(ITEM_SIZE + ITEM_PADDING * 3)), 0, Math.max(getRowsCount() - 1, 0)); + final int resH = MathUtils.clamp((int)((x - getRowX(resV)) / Screen.dp(ITEM_SIZE)), 0, Math.max(getRowSize(resV) - 1, 0)); + + if (resH != toneIndex || resV != toneIndexVertical) { + toneIndex = resH; + toneIndexVertical = resV; + invalidate(); + return true; + } + return false; + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (stickerViews == null) { + return; + } + + int row = hasToneEmoji() ? 1 : 0; + int x = getRowX(row); + int y = getRowY(row); + + int i = 0; + for (StickerSmallView v : stickerViews) { + v.setTranslationX(x + Screen.dp(ITEM_SIZE * i)); + v.setTranslationY(y); + i++; + if (i == getRowSize(row)) { + row++; + i = 0; + x = getRowX(row); + y = getRowY(row); + } + } + } + + @Override + protected void dispatchDraw (Canvas c) { + dispatchDrawImpl(c); + + final int itemSize = Screen.dp(ITEM_SIZE); + + int x = Screen.dp(VIEW_PADDING_HORIZONTAL + ITEM_PADDING); + int y = Screen.dp(VIEW_PADDING_TOP + ITEM_PADDING); + + if (hasToneEmoji()) { + for (EmojiInfo info : tones) { + int cx = x + itemSize / 2; + int cy = y + itemSize / 2; + int drawSize = Screen.dp(EMOJI_DRAW_SIZE); + Rect rect = Paints.getRect(); + rect.left = cx - drawSize / 2; + rect.top = cy - drawSize / 2; + rect.right = rect.left + drawSize; + rect.bottom = rect.top + drawSize; + Emoji.instance().draw(c, info, rect); + x += itemSize; + } + } + + super.dispatchDraw(c); + } + + private void dispatchDrawImpl (Canvas c) { + int count = getRowsCount(); + + int y = Screen.dp(VIEW_PADDING_TOP + ITEM_PADDING); + for (int a = 0; a < count; a++) { + int x = getRowX(a); + int bx = x - Screen.dp(VIEW_PADDING_HORIZONTAL + ITEM_PADDING); + int by = y - Screen.dp(VIEW_PADDING_TOP + ITEM_PADDING); + backgroundDrawable.setBounds(bx, by, bx + getRowWidth(a), by + Screen.dp(ITEM_SIZE + ITEM_PADDING * 2 + VIEW_PADDING_TOP + VIEW_PADDING_BOTTOM)); + backgroundDrawable.draw(c); + + if (toneIndexVertical == a) { + int rectX = x + Screen.dp(ITEM_SIZE) * toneIndex; + RectF rectF = Paints.getRectF(); + rectF.set(rectX, y, rectX + Screen.dp(ITEM_SIZE), y + Screen.dp(ITEM_SIZE)); + c.drawRoundRect(rectF, Screen.dp(4f), Screen.dp(4f), Paints.fillingPaint(Theme.HALF_RIPPLE_COLOR)); + } + + y += Screen.dp(ITEM_SIZE + ITEM_PADDING * 3); + } + + int cornerX = getCornerX() - Screen.dp(CORNER_WIDTH / 2); + int cornerY = calcViewHeight() - Screen.dp(VIEW_PADDING_BOTTOM); + cornerDrawable.setBounds(cornerX, cornerY, cornerX + Screen.dp(CORNER_WIDTH), cornerY + Screen.dp(CORNER_HEIGHT)); + cornerDrawable.draw(c); + } + + private int getCornerX () { + if (boundView != null) { + return boundView.getMeasuredWidth() / 2 - offsetLeft; + } + return 0; + } + + public int getRowWidth (int rowIndex) { + return Screen.dp(ITEM_SIZE * getRowSize(rowIndex) + ITEM_PADDING * 2 + VIEW_PADDING_HORIZONTAL * 2); + } + + public int getRowY (int rowIndex) { + return Screen.dp(VIEW_PADDING_TOP + ITEM_PADDING + (ITEM_SIZE + ITEM_PADDING * 3) * rowIndex); + } + + public int getRowX (int rowIndex) { + int boundWidth = (boundView != null) ? boundView.getMeasuredWidth() : Screen.dp(48); + + int totalWidth = calcViewWidth(); + int freeSpace = totalWidth - getRowWidth(rowIndex); + + float p = MathUtils.clamp((((float) getCornerX() - boundWidth / 2f)) / (totalWidth - boundWidth)); + return Screen.dp(VIEW_PADDING_HORIZONTAL + ITEM_PADDING) + (int) (freeSpace * p); + } + + public int getRowSize (int rowIndex) { + boolean hasTones = emojiColorState != EmojiData.STATE_NO_COLORS; + if (hasTones) { + if (rowIndex == 0) { + return tones.length; + } else { + rowIndex -= 1; + } + } + + boolean isStickersSmall = stickerObjs != null && stickerObjs.size() <= 6; + if (isStickersSmall) { + return rowIndex == 0 ? stickerObjs.size() : 0; + } + + if (stickerObjs != null) { + int count = (rowIndex == 0 && stickersFromServer != null && stickersFromServer.length > 0) ? + stickersFromServer.length: + (stickersFromLocal != null ? stickersFromLocal.length: 0); + return Math.min(6, count); + } + return 0; + } + + public boolean hasToneEmoji () { + return emojiColorState != EmojiData.STATE_NO_COLORS; + } + + public int getRowsCount () { + boolean isStickersSmall = stickerObjs != null && stickerObjs.size() <= 6; + + int count = (emojiColorState != EmojiData.STATE_NO_COLORS ? 1 : 0); + if (isStickersSmall) { + count += 1; + } else { + count += ((stickersFromServer != null && stickersFromServer.length > 0) ? 1 : 0); + count += ((stickersFromLocal != null && stickersFromLocal.length > 0) ? 1 : 0); + } + + return count; + } + + public int calcViewWidth () { + int count = getRowsCount(); + int maxSize = 0; + for (int a = 0; a < count; a++) { + maxSize = Math.max(maxSize, getRowWidth(a)); + } + + return maxSize; + } + + public int calcViewHeight () { + int count = getRowsCount(); + return Screen.dp(ITEM_SIZE * count + ITEM_PADDING * (3 * count - 1) + VIEW_PADDING_TOP + VIEW_PADDING_BOTTOM); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/EmojiCategoriesRecyclerView.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/EmojiCategoriesRecyclerView.java new file mode 100644 index 0000000000..80043301d2 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/EmojiCategoriesRecyclerView.java @@ -0,0 +1,354 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 25/09/2023 + */ +package org.thunderdog.challegram.widget.emoji.header; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.component.sticker.StickerSmallView; +import org.thunderdog.challegram.component.sticker.TGStickerObj; +import org.thunderdog.challegram.loader.ComplexReceiver; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.v.CustomRecyclerView; + +import java.util.Arrays; + +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.MathUtils; +import me.vkryl.core.StringUtils; +import me.vkryl.core.lambda.Destroyable; +import me.vkryl.core.lambda.RunnableData; + +public class EmojiCategoriesRecyclerView extends CustomRecyclerView implements Destroyable { + private static final int CATEGORY_WIDTH = 38; + private static final int SHADOW_SIZE = 30; + + private final GradientDrawable gradientDrawableLeft = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, new int[]{ 0, Theme.fillingColor() }); + private final GradientDrawable gradientDrawableRight = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[]{ 0, Theme.fillingColor() }); + private final ComplexReceiver receiverForPriorityLoading = new ComplexReceiver(); + private final LinearLayoutManager layoutManager; + private EmojiSearchTypesAdapter emojiSearchTypesAdapter; + private int minimalLeftPadding; + + public EmojiCategoriesRecyclerView (Context context) { + super(context); + layoutManager = new LinearLayoutManager(context); + layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + setLayoutManager(layoutManager); + + addItemDecoration(new RecyclerView.ItemDecoration() { + @Override + public void getItemOffsets (@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull State state) { + int position = parent.getChildAdapterPosition(view); + if (position == 0) { + float availWidth = parent.getMeasuredWidth() - minimalLeftPadding - Screen.dp(SHADOW_SIZE); + float itemsCount = (float) (Math.floor((availWidth - Screen.dp(CATEGORY_WIDTH * 0.65f)) / Screen.dp(CATEGORY_WIDTH)) + 0.65f); + int itemsWidth = Screen.dp(CATEGORY_WIDTH * itemsCount); + + outRect.set(parent.getMeasuredWidth() - itemsWidth, 0, 0, 0); + } + } + }); + + setPadding(0, 0, Screen.dp(7), 0); + setClipToPadding(false); + } + + public void init (ViewController controller, RunnableData onSelectCategoryListener) { + emojiSearchTypesAdapter = new EmojiSearchTypesAdapter(controller, layoutManager); + emojiSearchTypesAdapter.setOnSectionClickListener(onSelectCategoryListener); + setAdapter(emojiSearchTypesAdapter); + + controller.tdlib().send(new TdApi.GetEmojiCategories(new TdApi.EmojiCategoryTypeDefault()), (emojiCategories, error) -> { + if (emojiCategories != null) { + UI.post(() -> emojiSearchTypesAdapter.requestEmojiCategories(emojiCategories.categories, receiverForPriorityLoading)); + } + }); + } + + @Override + protected void onAttachedToWindow () { + super.onAttachedToWindow(); + receiverForPriorityLoading.attach(); + } + + @Override + protected void onDetachedFromWindow () { + super.onDetachedFromWindow(); + receiverForPriorityLoading.detach(); + } + + @Override + public void performDestroy () { + receiverForPriorityLoading.performDestroy(); + } + + public void setMinimalLeftPadding (int minimalLeftPadding) { + this.minimalLeftPadding = minimalLeftPadding; + invalidateItemDecorations(); + } + + public void reset () { + emojiSearchTypesAdapter.setActiveIndex(-1); + } + + @Override + protected void dispatchDraw (Canvas canvas) { + int sL = getFirstItemX(); + int sR = computeHorizontalScrollRange() - computeHorizontalScrollOffset() - computeHorizontalScrollExtent(); + + int alphaL = (int) ((1f - (MathUtils.clamp((float) sL / Screen.dp(SHADOW_SIZE)))) * 255); + int alphaR = (int) (MathUtils.clamp((float) sR / Screen.dp(SHADOW_SIZE)) * 255); + + canvas.drawRect(sL, 0, getMeasuredWidth(), getMeasuredHeight(), Paints.fillingPaint(Theme.fillingColor())); + + super.dispatchDraw(canvas); + + checkGradients(); + + gradientDrawableLeft.setAlpha(alphaL); + gradientDrawableLeft.setBounds(0, 0, Screen.dp(SHADOW_SIZE), getMeasuredHeight()); + gradientDrawableLeft.draw(canvas); + + gradientDrawableRight.setAlpha(255); + gradientDrawableRight.setBounds(sL - Screen.dp(SHADOW_SIZE), 0, sL, getMeasuredHeight()); + gradientDrawableRight.draw(canvas); + + gradientDrawableRight.setAlpha(alphaR); + gradientDrawableRight.setBounds(getMeasuredWidth() - Screen.dp(SHADOW_SIZE), 0, getMeasuredWidth(), getMeasuredHeight()); + gradientDrawableRight.draw(canvas); + } + + @Override + public boolean onTouchEvent (MotionEvent e) { + int sL = getFirstItemX(); + float x = e.getX(); + return x > sL && super.onTouchEvent(e); + } + + @Override + public void onScrolled (int dx, int dy) { + super.onScrolled(dx, dy); + invalidate(); + } + + private int getFirstItemX () { + View view = layoutManager.findViewByPosition(0); + if (view == null) { + return 0; + } + return view.getLeft(); + } + + private int lastColor; + private void checkGradients () { + int color = Theme.backgroundColor(); + if (color != lastColor) { + gradientDrawableRight.setColors(new int[]{ 0, lastColor = Theme.fillingColor() }); + } + } + + + + /* */ + + private static class EmojiSearchTypesViewHolder extends RecyclerView.ViewHolder { + public EmojiSearchTypesViewHolder (@NonNull View itemView) { + super(itemView); + } + + public static EmojiSearchTypesViewHolder create (ViewController context, View.OnClickListener onClickListener) { + StickerSmallView stickerSmallView = new StickerSmallView(context.context()); + stickerSmallView.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(CATEGORY_WIDTH), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER, 0, Screen.dp(9), 0, Screen.dp(9))); + stickerSmallView.setOnClickListener(onClickListener); + stickerSmallView.setPadding(Screen.dp(5.5f)); + stickerSmallView.init(context.tdlib()); + stickerSmallView.setStickerMovementCallback(new StickerSmallView.StickerMovementCallback() { + @Override + public boolean onStickerClick (StickerSmallView view, View clickView, TGStickerObj sticker, boolean isMenuClick, TdApi.MessageSendOptions sendOptions) { + onClickListener.onClick(view); + return true; + } + + @Override + public long getStickerOutputChatId () { + return 0; + } + + @Override + public void setStickerPressed (StickerSmallView view, TGStickerObj sticker, boolean isPressed) { + + } + + @Override + public boolean canFindChildViewUnder (StickerSmallView view, int recyclerX, int recyclerY) { + return false; + } + + @Override + public boolean onStickerLongClick (StickerSmallView view, TGStickerObj sticker) { + return true; + } + + @Override + public boolean needsLongDelay (StickerSmallView view) { + return false; + } + + @Override + public int getStickersListTop () { + return 0; + } + + @Override + public int getViewportHeight () { + return 0; + } + }); + context.addThemeInvalidateListener(stickerSmallView); + + return new EmojiSearchTypesViewHolder(stickerSmallView); + } + } + + public static class EmojiSearchTypesAdapter extends RecyclerView.Adapter implements View.OnClickListener { + private final ViewController context; + private final RecyclerView.LayoutManager manager; + private RunnableData onSectionClickListener; + private int activeIndex = -1; + + public EmojiSearchTypesAdapter (ViewController context, RecyclerView.LayoutManager manager) { + this.context = context; + this.manager = manager; + } + + public void setOnSectionClickListener (RunnableData onSectionClickListener) { + this.onSectionClickListener = onSectionClickListener; + } + + @NonNull + @Override + public EmojiSearchTypesViewHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { + return EmojiSearchTypesViewHolder.create(context, this); + } + + @Override + public void onBindViewHolder (@NonNull EmojiSearchTypesViewHolder holder, int position) { + StickerSmallView view = (StickerSmallView) holder.itemView; + view.setSticker(categoryStickers[position]); + view.setThemedColorId(position == activeIndex ? ColorId.iconActive : ColorId.icon); + view.setTag(position); + view.invalidate(); + } + + @Override + public void onViewAttachedToWindow (EmojiSearchTypesViewHolder holder) { + ((StickerSmallView) holder.itemView).attach(); + } + + @Override + public void onViewDetachedFromWindow (EmojiSearchTypesViewHolder holder) { + ((StickerSmallView) holder.itemView).detach(); + } + + @Override + public void onViewRecycled (EmojiSearchTypesViewHolder holder) { + ((StickerSmallView) holder.itemView).performDestroy(); + } + + @Override + public int getItemCount () { + return categories != null ? categories.length : 0; + } + + public void setActiveIndex (int activeIndex) { + final int oldActiveIndex = this.activeIndex; + if (oldActiveIndex == activeIndex) { + return; + } + + this.activeIndex = activeIndex; + if (activeIndex != -1) { + View view = manager.findViewByPosition(activeIndex); + if (view instanceof StickerSmallView) { + ((StickerSmallView) view).setThemedColorId(ColorId.iconActive); + view.invalidate(); + } else { + notifyItemChanged(activeIndex); + } + } + if (oldActiveIndex != -1) { + View view = manager.findViewByPosition(oldActiveIndex); + if (view instanceof StickerSmallView) { + ((StickerSmallView) view).setThemedColorId(ColorId.icon); + view.invalidate(); + } else { + notifyItemChanged(oldActiveIndex); + } + } + } + + @Override + public void onClick (View v) { + int index = (int) v.getTag(); + if (index != activeIndex) { + setActiveIndex(index); + onSectionClickListener.runWithData(StringUtils.join(" ", " ", Arrays.asList(categories[index].emojis))); + } else { + setActiveIndex(-1); + onSectionClickListener.runWithData(null); + } + } + + private TdApi.EmojiCategory[] categories; + private TGStickerObj[] categoryStickers; + + public void requestEmojiCategories (TdApi.EmojiCategory[] categories, ComplexReceiver receiverForPriorityLoading) { + final int itemCount = getItemCount(); + if (itemCount > 0) { + notifyItemRangeRemoved(0, itemCount); + } + this.categories = categories; + this.categoryStickers = new TGStickerObj[categories.length]; + for (int a = 0; a < categories.length; a++) { + TdApi.EmojiCategory category = categories[a]; + categoryStickers[a] = new TGStickerObj(context.tdlib(), category.icon, category.icon.fullType, category.emojis); + if (categoryStickers[a].getPreviewAnimation() != null) { + categoryStickers[a].getPreviewAnimation().setHighPriorityForDecode(); + categoryStickers[a].getPreviewAnimation().setPlayOnce(true); + categoryStickers[a].getPreviewAnimation().setLooped(false); + receiverForPriorityLoading.getGifReceiver(a).requestFile(categoryStickers[a].getPreviewAnimation()); + } + } + notifyItemRangeInserted(0, categories.length); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/EmojiHeaderView.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/EmojiHeaderView.java new file mode 100644 index 0000000000..e5b91e4466 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/EmojiHeaderView.java @@ -0,0 +1,608 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 18/08/2023 + */ +package org.thunderdog.challegram.widget.emoji.header; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Shader; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.jetbrains.annotations.Nullable; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.attach.CustomItemAnimator; +import org.thunderdog.challegram.config.Config; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TGStickerSetInfo; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.widget.emoji.EmojiHeaderCollapsibleSectionView; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutRecyclerController; +import org.thunderdog.challegram.widget.emoji.section.EmojiSection; +import org.thunderdog.challegram.widget.emoji.section.EmojiSectionView; +import org.thunderdog.challegram.widget.emoji.section.StickerSectionView; + +import java.util.ArrayList; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.BoolAnimator; +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.MathUtils; + +@SuppressLint("ViewConstructor") +public class EmojiHeaderView extends FrameLayout implements FactorAnimator.Target { + public static final int DEFAULT_PADDING = 4; + + private final EmojiLayoutEmojiHeaderAdapter adapter; + private final RecyclerView recyclerView; + private final EmojiSectionView goToMediaPageSection; + private final EmojiHeaderViewNonPremium emojiHeaderViewNonPremium; + private final BoolAnimator hasStickers = new BoolAnimator(0, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 220L); + + private final EmojiLayoutRecyclerController.Callback emojiLayout; + private Paint shadowPaint; + private boolean isPremium; + + public EmojiHeaderView (@NonNull Context context, EmojiLayoutRecyclerController.Callback emojiLayout, ViewController themeProvider, ArrayList emojiSections, @Nullable ArrayList expandableSections, boolean allowMedia) { + super(context); + this.emojiLayout = emojiLayout; + this.allowMedia = allowMedia; + setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(48))); + + LinearLayoutManager manager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, Lang.rtl()); + + adapter = new EmojiLayoutEmojiHeaderAdapter(manager, themeProvider, emojiSections, expandableSections); + + recyclerView = new RecyclerView(context) { + @Override + protected void onLayout (boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + checkShadow(); + } + }; + recyclerView.setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180)); + recyclerView.setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? OVER_SCROLL_IF_CONTENT_SCROLLS : OVER_SCROLL_NEVER); + recyclerView.setLayoutManager(manager); + recyclerView.setPadding(Screen.dp(DEFAULT_PADDING), 0, Screen.dp(DEFAULT_PADDING + 44), 0); + recyclerView.setClipToPadding(false); + recyclerView.setAdapter(adapter); + recyclerView.setVisibility(GONE); + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled (@NonNull RecyclerView recyclerView, int dx, int dy) { + checkShadow(); + } + }); + addView(recyclerView, FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + goToMediaPageSection = new EmojiSectionView(context); + goToMediaPageSection.setSection(new EmojiSection(emojiLayout, EmojiSection.SECTION_SWITCH_TO_MEDIA, R.drawable.deproko_baseline_stickers_24, 0).setActiveDisabled()); + goToMediaPageSection.setForceWidth(Screen.dp(48)); + goToMediaPageSection.setId(R.id.btn_section); + checkAllowMedia(); + if (themeProvider != null) { + themeProvider.addThemeInvalidateListener(goToMediaPageSection); + themeProvider.addThemeInvalidateListener(this); + } + + addView(goToMediaPageSection, FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.RIGHT)); + + emojiHeaderViewNonPremium = new EmojiHeaderViewNonPremium(context); + emojiHeaderViewNonPremium.init(emojiLayout, themeProvider, allowMedia); + addView(emojiHeaderViewNonPremium, FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + updatePaints(Theme.fillingColor()); + setSelectedObjectByPosition(1, false); + } + + private final boolean allowMedia; + private boolean mediaMustBeVisibility = false; + + private void checkAllowMedia () { + goToMediaPageSection.setVisibility(allowMedia && mediaMustBeVisibility ? VISIBLE : GONE); + recyclerView.setPadding(Screen.dp(DEFAULT_PADDING), 0, Screen.dp(DEFAULT_PADDING + (allowMedia ? 44 : 0)), 0); + } + + public void setSectionsOnClickListener (OnClickListener onClickListener) { + this.adapter.setOnClickListener(onClickListener); + this.goToMediaPageSection.setOnClickListener(onClickListener); + this.emojiHeaderViewNonPremium.setOnClickListener(onClickListener); + } + + public void setSectionsOnLongClickListener (OnLongClickListener onLongClickListener) { + this.adapter.setOnLongClickListener(onLongClickListener); + this.goToMediaPageSection.setOnLongClickListener(onLongClickListener); + this.emojiHeaderViewNonPremium.setOnLongClickListener(onLongClickListener); + } + + public void setCurrentStickerSectionByPosition (int i, boolean animated) { + setSelectedObjectByPosition(i, animated); + } + + public void setSelectedObjectByPosition (int i, boolean animated) { + emojiHeaderViewNonPremium.setSelectedIndex(i - 1, animated); + setSelectedObject(adapter.getObject(i), animated); + } + + private static final int OFFSET = 2; + + private void setSelectedObject (Object obj, boolean animated) { + if (!adapter.setSelectedObject(obj, animated, adapter.manager)) { + return; + } + + int section = adapter.getPositionFromIndex(adapter.indexOfObject(obj)); + int first = adapter.manager.findFirstVisibleItemPosition(); + int last = adapter.manager.findLastVisibleItemPosition(); + int itemWidth = Screen.dp(44); + float sectionsCount = (float) Screen.currentWidth() / itemWidth; + + if (first != -1) { + int scrollX = first * itemWidth; + View v = adapter.manager.findViewByPosition(first); + if (v != null) { + scrollX -= v.getLeft(); + } + + if (section - OFFSET < first) { + int desiredScrollX = section * itemWidth - itemWidth / 2 - itemWidth; + int scrollLimit = scrollX + recyclerView.getPaddingLeft(); + int scrollValue = Math.max(desiredScrollX - scrollX, -scrollLimit); + if (scrollValue < 0) { + if (animated && emojiLayout.getHeaderHideFactor() != 1f) { + recyclerView.smoothScrollBy(scrollValue, 0); + } else { + recyclerView.scrollBy(scrollValue, 0); + } + } + } else if (section + OFFSET > last) { + int desiredScrollX = (int) Math.max(0, (section - sectionsCount + 1) * itemWidth + itemWidth * OFFSET + (emojiLayout.isAnimatedEmojiOnly() ? -itemWidth : itemWidth / 2f)); + int scrollValue = desiredScrollX - scrollX; + if (last != -1 && last == adapter.getItemCount() - 1) { + View vr = adapter.manager.findViewByPosition(last); + if (vr != null) { + scrollValue = Math.min(scrollValue, vr.getRight() + recyclerView.getPaddingRight() - recyclerView.getMeasuredWidth()); + } + } + if (scrollValue > 0) { + if (animated && emojiLayout.getHeaderHideFactor() != 1f) { + recyclerView.smoothScrollBy(scrollValue, 0); + } else { + recyclerView.scrollBy(scrollValue, 0); + } + } + } + } + } + + public void addStickerSection (int index, TGStickerSetInfo info) { + adapter.addStickerSection(index - adapter.getAddIndexCount(), info); + checkStickerSections(true); + } + + public void moveStickerSection (int fromIndex, int toIndex) { + int addItems = adapter.getAddIndexCount(); + adapter.moveStickerSection(fromIndex - addItems, toIndex - addItems); + } + + public void removeStickerSection (int index) { + adapter.removeStickerSection(index); + checkStickerSections(true); + } + + public void setStickerSets (ArrayList stickers) { + adapter.setStickerSets(stickers); + checkStickerSections(false); + } + + public void setMediaSection (boolean isGif) { + emojiHeaderViewNonPremium.setMediaSection(isGif); + goToMediaPageSection.getSection().changeIcon(isGif ? R.drawable.deproko_baseline_gif_24 : R.drawable.deproko_baseline_stickers_24); + } + + public void setIsPremium (boolean isPremium, boolean animated) { + this.isPremium = isPremium; + checkStickerSections(animated); + } + + private int shadowColor; + + private void updatePaints (int color) { + if (color == shadowColor && shadowPaint != null) { + return; + } + + LinearGradient shader = new LinearGradient(0, 0, Screen.dp(48), 0, 0, color, Shader.TileMode.CLAMP); + if (shadowPaint == null) { + shadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + } + shadowPaint.setShader(shader); + shadowColor = color; + invalidate(); + } + + private void checkShadow () { + float range = recyclerView.computeHorizontalScrollRange(); + float offset = recyclerView.computeHorizontalScrollOffset(); + float extent = recyclerView.computeHorizontalScrollExtent(); + float s = range - offset - extent; + + int alpha = (int) (MathUtils.clamp(s / Screen.dp(20f)) * 255); + shadowPaint.setAlpha(alpha); + invalidate(); + } + + @Override + protected boolean drawChild (Canvas canvas, View child, long drawingTime) { + if (child == goToMediaPageSection) { + updatePaints(Theme.fillingColor()); + canvas.save(); + canvas.translate(getMeasuredWidth() - Screen.dp(96), 0); + canvas.drawRect(0, 0, Screen.dp(96), getMeasuredHeight(), shadowPaint); + canvas.restore(); + } + return super.drawChild(canvas, child, drawingTime); + } + + private void checkStickerSections (boolean animated) { + boolean value = /*adapter.hasStickers() &&*/ isPremium; + hasStickers.setValue(value, animated); + if (value) { + recyclerView.setVisibility(VISIBLE); + mediaMustBeVisibility = true; + checkAllowMedia(); + } else { + emojiHeaderViewNonPremium.setVisibility(VISIBLE); + } + } + + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + recyclerView.setAlpha(hasStickers.getFloatValue()); + goToMediaPageSection.setAlpha(hasStickers.getFloatValue()); + emojiHeaderViewNonPremium.setAlpha(1f - hasStickers.getFloatValue()); + } + + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + if (hasStickers.getValue()) { + emojiHeaderViewNonPremium.setVisibility(GONE); + } else { + recyclerView.setVisibility(GONE); + mediaMustBeVisibility = false; + checkAllowMedia(); + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public static final int TYPE_SECTION = 0; + public static final int TYPE_STICKER_SET = 1; + public static final int TYPE_SECTIONS_EXPANDABLE = 2; + + public ViewHolder (@NonNull View itemView) { + super(itemView); + } + + public static ViewHolder create (Context context, int viewType, ViewController themeProvider, ArrayList expandableSections, View.OnClickListener onClickListener, View.OnLongClickListener onLongClickListener) { + if (viewType == TYPE_SECTION) { + EmojiSectionView sectionView = new EmojiSectionView(context); + if (themeProvider != null) { + themeProvider.addThemeInvalidateListener(sectionView); + } + sectionView.setId(R.id.btn_section); + sectionView.setOnClickListener(onClickListener); + sectionView.setOnLongClickListener(onLongClickListener); + sectionView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + return new ViewHolder(sectionView); + } else if (viewType == TYPE_STICKER_SET) { + StickerSectionView sectionView = new StickerSectionView(context); + if (themeProvider != null) { + themeProvider.addThemeInvalidateListener(sectionView); + } + sectionView.setOnLongClickListener(onLongClickListener); + sectionView.setId(R.id.btn_stickerSet); + sectionView.setOnClickListener(onClickListener); + sectionView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + return new ViewHolder(sectionView); + } else if (viewType == TYPE_SECTIONS_EXPANDABLE) { + EmojiHeaderCollapsibleSectionView v = new EmojiHeaderCollapsibleSectionView(context); + v.init(expandableSections); + v.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + v.setOnButtonClickListener(onClickListener); + if (themeProvider != null) { + v.setThemeInvalidateListener(themeProvider); + themeProvider.addThemeInvalidateListener(v); + } + return new ViewHolder(v); + } + + return new ViewHolder(new View(context)); + } + } + + public static class EmojiLayoutEmojiHeaderAdapter extends RecyclerView.Adapter { + private final ArrayList emojiSections; + private final @Nullable ArrayList expandableSections; + private final ViewController themeProvider; + private final ArrayList stickerSets; + private final LinearLayoutManager manager; + private final int expandableItemSize; + private final int expandableItemPosition; + + private View.OnClickListener onClickListener; + private View.OnLongClickListener onLongClickListener; + private Object selectedObject; + + public EmojiLayoutEmojiHeaderAdapter (LinearLayoutManager manager, ViewController themeProvider, ArrayList emojiSections, @Nullable ArrayList expandableSections) { + this.themeProvider = themeProvider; + this.emojiSections = emojiSections; + this.stickerSets = new ArrayList<>(); + this.manager = manager; + this.expandableItemPosition = emojiSections.size(); + this.expandableSections = expandableSections; + this.expandableItemSize = expandableSections != null ? expandableSections.size(): -1; + } + + public void setOnClickListener (OnClickListener onClickListener) { + this.onClickListener = onClickListener; + } + + public void setOnLongClickListener (OnLongClickListener onLongClickListener) { + this.onLongClickListener = onLongClickListener; + } + + public int getAddIndexCount () { + return emojiSections.size() + (expandableSections != null ? expandableSections.size() - 1: 0); + } + + public int getAddItemCount () { + return emojiSections.size() + (expandableItemSize > 0 ? 1 : 0); + } + + public boolean hasStickers () { + return !stickerSets.isEmpty(); + } + + public void addStickerSection (int index, TGStickerSetInfo info) { + stickerSets.add(index, info); + notifyItemInserted(index + getAddItemCount()); + } + + public void removeStickerSection (int index) { + if (index >= getAddIndexCount()) { + index -= getAddIndexCount(); + if (index >= 0 && index < stickerSets.size()) { + stickerSets.remove(index); + notifyItemRemoved(index + getAddItemCount()); + } + } else if (index >= 0 && index < emojiSections.size()) { + emojiSections.remove(index); + notifyItemRemoved(index); + } + } + + public void moveStickerSection (int fromIndex, int toIndex) { + TGStickerSetInfo info = stickerSets.remove(fromIndex); + stickerSets.add(toIndex, info); + notifyItemMoved(fromIndex + getAddItemCount(), toIndex + getAddItemCount()); + } + + public void setStickerSets (ArrayList stickers) { + if (!stickerSets.isEmpty()) { + int removedCount = stickerSets.size(); + stickerSets.clear(); + notifyItemRangeRemoved(getAddItemCount(), removedCount); + } + if (stickers != null && !stickers.isEmpty()) { + int addedCount; + if (!stickers.get(0).isSystem()) { + stickerSets.addAll(stickers); + addedCount = stickers.size(); + } else { + addedCount = 0; + for (int i = 0; i < stickers.size(); i++) { + TGStickerSetInfo stickerSet = stickers.get(i); + if (stickerSet.isSystem()) { + continue; + } + stickerSets.add(stickerSet); + addedCount++; + } + } + notifyItemRangeInserted(getAddItemCount(), addedCount); + } + } + + public boolean setSelectedObject (Object obj, boolean animated, RecyclerView.LayoutManager manager) { + if (this.selectedObject == obj) return false; + + final int oldIndex = indexOfObject(selectedObject); + final int oldSelectedPosition = getPositionFromIndex(oldIndex); + final int oldSelectedViewType = getItemViewType(oldSelectedPosition); + + final int index = indexOfObject(obj); + final int newSelectedPosition = getPositionFromIndex(index); + final int newSelectedViewType = getItemViewType(newSelectedPosition); + + this.selectedObject = obj; + + if (newSelectedViewType == ViewHolder.TYPE_SECTIONS_EXPANDABLE) { + View view = manager.findViewByPosition(expandableItemPosition); + if (view instanceof EmojiHeaderCollapsibleSectionView) { + ((EmojiHeaderCollapsibleSectionView) view).setSelectedObject((EmojiSection) obj, animated); + } else { + notifyItemChanged(newSelectedPosition); + } + } + + if (oldSelectedPosition == newSelectedPosition) { + return true; + } + + if (newSelectedViewType == ViewHolder.TYPE_STICKER_SET) { + View view = manager.findViewByPosition(newSelectedPosition); + if (view instanceof StickerSectionView) { + ((StickerSectionView) view).setSelectionFactor(1f, animated); + } else { + notifyItemChanged(newSelectedPosition); + } + } else if (newSelectedViewType == ViewHolder.TYPE_SECTION) { + ((EmojiSection) getObject(index)).setFactor(1f, animated); + } + + if (oldIndex == -1) { + return true; + } + + if (oldSelectedViewType == ViewHolder.TYPE_STICKER_SET) { + View view = manager.findViewByPosition(oldSelectedPosition); + if (view instanceof StickerSectionView) { + ((StickerSectionView) view).setSelectionFactor(0f, animated); + } else { + notifyItemChanged(oldSelectedPosition); + } + } else if (oldSelectedViewType == ViewHolder.TYPE_SECTION) { + ((EmojiSection) getObject(oldIndex)).setFactor(0f, animated); + } else if (oldSelectedViewType == ViewHolder.TYPE_SECTIONS_EXPANDABLE) { + View view = manager.findViewByPosition(oldSelectedPosition); + if (view instanceof EmojiHeaderCollapsibleSectionView) { + ((EmojiHeaderCollapsibleSectionView) view).setSelectedObject(null, animated); + } else { + notifyItemChanged(oldSelectedPosition); + } + } + + return true; + } + + private Object getObject (int i) { + if (i < 0) return null; + if (i < emojiSections.size()) { + return emojiSections.get(i); + } + i -= emojiSections.size(); + if (expandableSections != null) { + if (i < expandableSections.size()) { + return expandableSections.get(i); + } + i -= expandableSections.size(); + } + + return i < stickerSets.size() ? stickerSets.get(i) : null; + } + + private int indexOfObject (Object obj) { + Object item; + int i = 0; + do { + item = getObject(i); + if (obj == item) { + return i; + } + i++; + } while (item != null); + return -1; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { + return ViewHolder.create(parent.getContext(), viewType, themeProvider, expandableSections, onClickListener, onLongClickListener); + } + + @Override + public void onBindViewHolder (@NonNull ViewHolder holder, int position) { + final int viewType = getItemViewType(position); + + if (position > expandableItemPosition && expandableItemSize > 0) { + position -= 1; + } + + if (viewType == ViewHolder.TYPE_SECTION) { + ((EmojiSectionView) holder.itemView).setSection(emojiSections.get(position)); + } else if (viewType == ViewHolder.TYPE_STICKER_SET) { + TGStickerSetInfo info = stickerSets.get(position - emojiSections.size()); + ((StickerSectionView) holder.itemView).setSelectionFactor(info == selectedObject ? 1f : 0f, false); + ((StickerSectionView) holder.itemView).setStickerSet(info); + } else if (viewType == ViewHolder.TYPE_SECTIONS_EXPANDABLE) { + EmojiSection obj = selectedObject instanceof EmojiSection ? ((EmojiSection) selectedObject) : null; + ((EmojiHeaderCollapsibleSectionView) holder.itemView).setSelectedObject(obj, false); + } + } + + private int getPositionFromIndex (int index) { + if (expandableItemSize > 0 && index >= expandableItemPosition) { + if (index < expandableItemPosition + expandableItemSize) { + return expandableItemPosition; + } else { + return index - expandableItemSize + 1; + } + } + return index; + } + + @Override + public int getItemViewType (int position) { + if (position == expandableItemPosition && expandableItemSize > 0) { + return ViewHolder.TYPE_SECTIONS_EXPANDABLE; + } else if (position > expandableItemPosition && expandableItemSize > 0) { + position -= 1; + } + + if (position < emojiSections.size()) { + return ViewHolder.TYPE_SECTION; + } else { + return ViewHolder.TYPE_STICKER_SET; + } + } + + @Override + public int getItemCount () { + return emojiSections.size() + stickerSets.size() + (expandableItemSize > 0 ? 1 : 0) ; + } + + @Override + public void onViewAttachedToWindow (ViewHolder holder) { + if (holder.getItemViewType() == ViewHolder.TYPE_STICKER_SET) { + ((StickerSectionView) holder.itemView).attach(); + } + } + + @Override + public void onViewDetachedFromWindow (ViewHolder holder) { + if (holder.getItemViewType() == ViewHolder.TYPE_STICKER_SET) { + ((StickerSectionView) holder.itemView).detach(); + } + } + + @Override + public void onViewRecycled (ViewHolder holder) { + if (holder.getItemViewType() == ViewHolder.TYPE_STICKER_SET) { + ((StickerSectionView) holder.itemView).performDestroy(); + } + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/EmojiHeaderViewNonPremium.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/EmojiHeaderViewNonPremium.java new file mode 100644 index 0000000000..1cfd36c1e0 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/EmojiHeaderViewNonPremium.java @@ -0,0 +1,117 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 24/08/2023 + */ +package org.thunderdog.challegram.widget.emoji.header; + +import android.content.Context; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutRecyclerController; +import org.thunderdog.challegram.widget.emoji.section.EmojiSection; +import org.thunderdog.challegram.widget.emoji.section.EmojiSectionView; + +import java.util.ArrayList; + +import me.vkryl.android.widget.FrameLayoutFix; + +public class EmojiHeaderViewNonPremium extends FrameLayoutFix { + private final ArrayList emojiSections = new ArrayList<>(9); + private final ArrayList emojiSectionViews = new ArrayList<>(9); + private int currentSelectedIndex = -1; + private boolean allowMedia; + + public EmojiHeaderViewNonPremium (@NonNull Context context) { + super(context); + } + + public void init (EmojiLayoutRecyclerController.Callback emojiLayout, ViewController themeProvider, boolean allowMedia) { + this.allowMedia = allowMedia; + + emojiSections.add(new EmojiSection(emojiLayout, EmojiSection.SECTION_EMOJI_RECENT, R.drawable.baseline_access_time_24, R.drawable.baseline_watch_later_24)/*.setFactor(1f, false)*/.setMakeFirstTransparent().setOffsetHalf(false)); + emojiSections.add(new EmojiSection(emojiLayout, EmojiSection.SECTION_EMOJI_SMILEYS, R.drawable.baseline_emoticon_outline_24, R.drawable.baseline_emoticon_24).setMakeFirstTransparent()); + emojiSections.add(new EmojiSection(emojiLayout, EmojiSection.SECTION_EMOJI_ANIMALS, R.drawable.deproko_baseline_animals_outline_24, R.drawable.deproko_baseline_animals_24));/*.setIsPanda(!useDarkMode)*/ + emojiSections.add(new EmojiSection(emojiLayout, EmojiSection.SECTION_EMOJI_FOOD, R.drawable.baseline_restaurant_menu_24, R.drawable.baseline_restaurant_menu_24)); + emojiSections.add(new EmojiSection(emojiLayout, EmojiSection.SECTION_EMOJI_TRAVEL, R.drawable.baseline_directions_car_24, R.drawable.baseline_directions_car_24)); + emojiSections.add(new EmojiSection(emojiLayout, EmojiSection.SECTION_EMOJI_SYMBOLS, R.drawable.deproko_baseline_lamp_24, R.drawable.deproko_baseline_lamp_filled_24)); + emojiSections.add(new EmojiSection(emojiLayout, EmojiSection.SECTION_EMOJI_FLAGS, R.drawable.deproko_baseline_flag_outline_24, R.drawable.deproko_baseline_flag_filled_24).setMakeFirstTransparent()); + if (allowMedia) { + emojiSections.add(new EmojiSection(emojiLayout, EmojiSection.SECTION_SWITCH_TO_MEDIA, R.drawable.deproko_baseline_stickers_24, 0).setActiveDisabled()); + } + emojiSectionViews.clear(); + for (int a = 0; a < emojiSections.size(); a++) { + EmojiSectionView emojiSectionView = new EmojiSectionView(getContext()); + emojiSectionView.setSection(emojiSections.get(a)); + emojiSectionView.setId(R.id.btn_section); + if (themeProvider != null) { + themeProvider.addThemeInvalidateListener(emojiSectionView); + } + addView(emojiSectionView, FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + emojiSectionViews.add(emojiSectionView); + } + } + + public void setMediaSection (boolean isGif) { + if (allowMedia) { + emojiSections.get(emojiSections.size() - 1).changeIcon(isGif ? R.drawable.deproko_baseline_gif_24 : R.drawable.deproko_baseline_stickers_24); + } + } + + public void setSelectedIndex (int index, boolean animated) { + if (index == currentSelectedIndex) { + return; + } + if (currentSelectedIndex != -1) { + emojiSections.get(currentSelectedIndex).setFactor(0f, animated); + } + if (index >= 0 && index < emojiSections.size()) { + this.currentSelectedIndex = index; + emojiSections.get(currentSelectedIndex).setFactor(1f, animated); + } + } + + public void setOnClickListener (OnClickListener onClickListener) { + for (EmojiSectionView view : emojiSectionViews) { + view.setOnClickListener(onClickListener); + } + } + + public void setOnLongClickListener (OnLongClickListener onLongClickListener) { + for (EmojiSectionView view : emojiSectionViews) { + view.setOnLongClickListener(onLongClickListener); + } + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + updatePositions(); + } + + private void updatePositions () { + int itemCount = emojiSections.size(); + float itemWidth = (float) (getMeasuredWidth() - Screen.dp(EmojiHeaderView.DEFAULT_PADDING * 2f)) / itemCount; + float itemOffset = (itemWidth - Screen.dp(44)) / 2f; + float itemPadding = (getMeasuredWidth() - Screen.dp(EmojiHeaderView.DEFAULT_PADDING * 2) - itemWidth * itemCount) / (itemCount - 1); + + for (int a = 0; a < itemCount; a++) { + EmojiSectionView v = emojiSectionViews.get(a); + v.setTranslationX((int) (Screen.dp(EmojiHeaderView.DEFAULT_PADDING) + itemOffset + (itemWidth + itemPadding) * a)); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/MediaHeaderView.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/MediaHeaderView.java new file mode 100644 index 0000000000..9d18b1a575 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/header/MediaHeaderView.java @@ -0,0 +1,560 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 19/08/2023 + */ +package org.thunderdog.challegram.widget.emoji.header; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.component.attach.CustomItemAnimator; +import org.thunderdog.challegram.config.Config; +import org.thunderdog.challegram.core.Lang; +import org.thunderdog.challegram.data.TGStickerSetInfo; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.telegram.EmojiMediaType; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.unsorted.Settings; +import org.thunderdog.challegram.widget.EmojiLayout; +import org.thunderdog.challegram.widget.emoji.section.EmojiSection; +import org.thunderdog.challegram.widget.emoji.section.EmojiSectionView; +import org.thunderdog.challegram.widget.emoji.section.StickerSectionView; + +import java.util.ArrayList; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.widget.FrameLayoutFix; + +public class MediaHeaderView extends RecyclerView { + private static final int OFFSET = 2; + + private MediaAdapter mediaAdapter; + private EmojiLayout emojiLayout; + + public MediaHeaderView (@NonNull Context context) { + super(context); + + setHasFixedSize(true); + setItemAnimator(new CustomItemAnimator(AnimatorUtils.DECELERATE_INTERPOLATOR, 180)); + setOverScrollMode(Config.HAS_NICE_OVER_SCROLL_EFFECT ? OVER_SCROLL_IF_CONTENT_SCROLLS :OVER_SCROLL_NEVER); + setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, Lang.rtl())); + setPadding(EmojiLayout.getHorizontalPadding(), 0, EmojiLayout.getHorizontalPadding(), 0); + setClipToPadding(false); + setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, EmojiLayout.getHeaderSize())); + } + + public void init (EmojiLayout emojiLayout, ViewController themeProvider, View.OnClickListener onClickListener) { + this.emojiLayout = emojiLayout; + setAdapter(mediaAdapter = new MediaAdapter(getContext(), emojiLayout, onClickListener, emojiLayout.isAnimatedEmojiOnly() ? 8 : emojiLayout.getEmojiSectionsSize(), !emojiLayout.isAnimatedEmojiOnly() && Settings.instance().getEmojiMediaSection() == EmojiMediaType.GIF, themeProvider, emojiLayout.isAnimatedEmojiOnly())); + } + + public boolean hasRecents () { + return mediaAdapter.hasRecents; + } + + public void setCurrentStickerSectionByPosition (int i, boolean isStickerSection, boolean animated) { + if (mediaAdapter.hasRecents && mediaAdapter.hasFavorite && isStickerSection && i >= 1) { + i--; + } + if (isStickerSection) { + i += mediaAdapter.headerItems.size() - mediaAdapter.getAddItemCount(false); + } + setCurrentStickerSection(mediaAdapter.getObject(i), animated); + } + + public void setShowRecents (boolean showRecents) { + mediaAdapter.setHasRecents(showRecents); + } + + public void setShowFavorite (boolean showFavorite) { + mediaAdapter.setHasFavorite(showFavorite); + } + + public void setHasNewHots (boolean hasHots) { + mediaAdapter.setHasNewHots(hasHots); + } + + public void addStickerSection (int section, TGStickerSetInfo info) { + mediaAdapter.addStickerSet(section - mediaAdapter.getAddItemCount(true), info); + } + + public void moveStickerSection (int fromSection, int toSection) { + int addItems = mediaAdapter.getAddItemCount(true); + mediaAdapter.moveStickerSet(fromSection - addItems, toSection - addItems); + } + + public void removeStickerSection (int section) { + mediaAdapter.removeStickerSet(section - mediaAdapter.getAddItemCount(true)); + } + + public void invalidateStickerSets () { + mediaAdapter.notifyDataSetChanged(); + } + + public void setStickerSets (ArrayList stickers, boolean showFavorite, boolean showRecents, boolean showTrending, boolean isFound) { + mediaAdapter.setHasFavorite(showFavorite); + mediaAdapter.setHasRecents(showRecents); + mediaAdapter.setShowRecentsAsFound(isFound); + mediaAdapter.setHasTrending(showTrending); + mediaAdapter.setStickerSets(stickers); + } + + private void setCurrentStickerSection (Object obj, boolean animated) { + if (mediaAdapter.setSelectedObject(obj, animated, getLayoutManager())) { + int section = mediaAdapter.indexOfObject(obj); + int first = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition(); + int last = ((LinearLayoutManager) getLayoutManager()).findLastVisibleItemPosition(); + int itemWidth = Screen.dp(44); + float sectionsCount = (float) Screen.currentWidth() / itemWidth; + + if (first != -1) { + int scrollX = first * itemWidth; + View v = getLayoutManager().findViewByPosition(first); + if (v != null) { + scrollX += -v.getLeft(); + } + + if (section - OFFSET < first) { + int desiredScrollX = section * itemWidth - itemWidth / 2 - itemWidth; + int scrollLimit = scrollX + getPaddingLeft(); + int scrollValue = Math.max(desiredScrollX - scrollX, -scrollLimit); + if (scrollValue < 0) { + if (animated && emojiLayout.getHeaderHideFactor() != 1f) { + smoothScrollBy(scrollValue, 0); + } else { + scrollBy(scrollValue, 0); + } + } + } else if (section + OFFSET > last) { + int desiredScrollX = (int) Math.max(0, (section - sectionsCount) * itemWidth + itemWidth * OFFSET + (emojiLayout.isAnimatedEmojiOnly() ? -itemWidth : itemWidth / 2)); + int scrollValue = desiredScrollX - scrollX; + if (last != -1 && last == mediaAdapter.getItemCount() - 1) { + View vr = getLayoutManager().findViewByPosition(last); + if (vr != null) { + scrollValue = Math.min(scrollValue, vr.getRight() + getPaddingRight() - getMeasuredWidth()); + } + } + if (scrollValue > 0) { + if (animated && emojiLayout.getHeaderHideFactor() != 1f) { + smoothScrollBy(scrollValue, 0); + } else { + scrollBy(scrollValue, 0); + } + } + } + } + } + } + + private static class MediaHolder extends RecyclerView.ViewHolder { + public static final int TYPE_EMOJI_SECTION = 0; + public static final int TYPE_STICKER_SECTION = 1; + + public MediaHolder (View itemView) { + super(itemView); + } + + public static MediaHolder create (Context context, int viewType, View.OnClickListener onClickListener, View.OnLongClickListener onLongClickListener, int emojiSectionCount, @Nullable ViewController themeProvider) { + switch (viewType) { + case TYPE_EMOJI_SECTION: { + EmojiSectionView sectionView = new EmojiSectionView(context); + if (themeProvider != null) { + themeProvider.addThemeInvalidateListener(sectionView); + } + sectionView.setId(R.id.btn_section); + sectionView.setOnClickListener(onClickListener); + sectionView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + return new MediaHolder(sectionView); + } + case TYPE_STICKER_SECTION: { + StickerSectionView sectionView = new StickerSectionView(context); + if (themeProvider != null) { + themeProvider.addThemeInvalidateListener(sectionView); + } + sectionView.setOnLongClickListener(onLongClickListener); + sectionView.setId(R.id.btn_stickerSet); + sectionView.setOnClickListener(onClickListener); + sectionView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + return new MediaHolder(sectionView); + } + } + throw new RuntimeException("viewType == " + viewType); + } + } + + private static class MediaAdapter extends RecyclerView.Adapter implements View.OnLongClickListener { + private final Context context; + private final View.OnClickListener onClickListener; + private final ArrayList headerItems; + private final int sectionItemCount; + private final EmojiLayout parent; + + private final @Nullable ViewController themeProvider; + + private Object selectedObject; + private boolean hasRecents, hasFavorite; + + public MediaAdapter (Context context, EmojiLayout parent, OnClickListener onClickListener, int sectionItemCount, boolean selectedIsGifs, @Nullable ViewController themeProvider, boolean hideSectionsExceptRecent) { + this.context = context; + this.parent = parent; + this.onClickListener = onClickListener; + this.themeProvider = themeProvider; + this.headerItems = new ArrayList<>(); + if (!hideSectionsExceptRecent) { + this.headerItems.add(new EmojiSection(parent, -1, R.drawable.baseline_emoticon_outline_24, 0).setActiveDisabled()); + this.headerItems.add(new EmojiSection(parent, -2, R.drawable.deproko_baseline_gif_24, R.drawable.deproko_baseline_gif_filled_24)); + this.headerItems.add(new EmojiSection(parent, -3, R.drawable.outline_whatshot_24, R.drawable.baseline_whatshot_24).setMakeFirstTransparent()); + } + // this.favoriteSection = new EmojiSection(parent, -4, R.drawable.baseline_star_border_24, R.drawable.baseline_star_24).setMakeFirstTransparent(); + this.recentSection = new EmojiSection(parent, -4, R.drawable.baseline_access_time_24, R.drawable.baseline_watch_later_24).setMakeFirstTransparent(); + this.trendingSection = new EmojiSection(parent, -5, R.drawable.outline_whatshot_24, R.drawable.baseline_whatshot_24).setMakeFirstTransparent(); + this.trendingSection.setIsTrending(); + + this.selectedObject = selectedIsGifs ? headerItems.get(1) : recentSection; + if (selectedIsGifs) { + this.headerItems.get(1).setFactor(1f, false); + } else { + this.recentSection.setFactor(1f, false); + } + + this.sectionItemCount = sectionItemCount; + this.stickerSets = new ArrayList<>(); + } + + public void addHeaderItem (EmojiSection emojiSection) { + this.headerItems.add(emojiSection); + } + + public void setHasRecents (boolean hasRecents) { + if (this.hasRecents != hasRecents) { + this.hasRecents = hasRecents; + checkRecent(); + } + } + + public void setShowRecentsAsFound (boolean showRecentAsFound) { + recentSection.changeIcon( + showRecentAsFound ? R.drawable.baseline_emoticon_outline_24 : R.drawable.baseline_access_time_24, + showRecentAsFound ? 0 : R.drawable.baseline_watch_later_24); + } + + public int getAddItemCount (boolean allowHidden) { + int i = 0; + if (allowHidden) { + if (hasFavorite) { + i++; + } + if (hasRecents) { + i++; + } + if (hasTrending) { + i++; + } + } else { + if (showingRecentSection) { + i++; + } + if (showingTrendingSection) { + i++; + } + } + return i; + } + + private boolean showingRecentSection; + + private void checkRecent () { + boolean showRecent = hasFavorite || hasRecents; + if (this.showingRecentSection != showRecent) { + this.showingRecentSection = showRecent; + if (showRecent) { + headerItems.add(recentSection); + notifyItemInserted(headerItems.size() - 1); + } else { + int i = headerItems.indexOf(recentSection); + if (i != -1) { + headerItems.remove(i); + notifyItemRemoved(i); + } + } + } else if (selectedObject != null) { + int i = indexOfObject(selectedObject); + if (i != -1) { + notifyItemRangeChanged(i, 2); + } + } + } + + private boolean showingTrendingSection; + + private void checkTrending () { + boolean showTrending = hasTrending; + if (this.showingTrendingSection != showTrending) { + this.showingTrendingSection = showTrending; + if (showTrending) { + headerItems.add(trendingSection); + notifyItemInserted(headerItems.size() - 1); + } else { + int i = headerItems.indexOf(trendingSection); + if (i != -1) { + headerItems.remove(i); + notifyItemRemoved(i); + } + } + } else if (selectedObject != null) { + int i = indexOfObject(selectedObject); + if (i != -1) { + notifyItemRangeChanged(i, 2); + } + } + } + + public void setHasFavorite (boolean hasFavorite) { + if (this.hasFavorite != hasFavorite) { + this.hasFavorite = hasFavorite; + checkRecent(); + } + /*if (this.showFavorite != showFavorite) { + this.showFavorite = showFavorite; + if (showFavorite) { + int i = showRecents ? headerItems.size() - 1 : headerItems.size(); + headerItems.add(i, favoriteSection); + notifyItemInserted(i); + } else { + int i = headerItems.indexOf(favoriteSection); + if (i != -1) { + headerItems.remove(i); + notifyItemRemoved(i); + } + } + }*/ + } + + private boolean hasNewHots; + + public void setHasNewHots (boolean hasHots) { + if (this.hasNewHots != hasHots) { + this.hasNewHots = hasHots; + // TODO + } + } + + private boolean hasTrending; + + public void setHasTrending (boolean hasTrending) { + if (this.hasTrending != hasTrending) { + this.hasTrending = hasTrending; + checkTrending(); + } + } + + public boolean setSelectedObject (Object obj, boolean animated, RecyclerView.LayoutManager manager) { + if (this.selectedObject != obj) { + setSelected(this.selectedObject, false, animated, manager); + this.selectedObject = obj; + setSelected(obj, true, animated, manager); + return true; + } + return false; + } + + private Object getObject (int i) { + if (i < 0) return null; + if (i < headerItems.size()) { + return headerItems.get(i); + } else { + int index = i - headerItems.size(); + return index >= 0 && index < stickerSets.size() ? stickerSets.get(index) : null; + } + } + + private int indexOfObject (Object obj) { + int itemCount = getItemCount(); + for (int i = 0; i < itemCount; i++) { + if (getObject(i) == obj) { + return i; + } + } + return -1; + } + + private void setSelected (Object obj, boolean selected, boolean animated, RecyclerView.LayoutManager manager) { + int index = indexOfObject(obj); + if (index != -1) { + switch (getItemViewType(index)) { + case MediaHolder.TYPE_EMOJI_SECTION: { + if (index >= 0 && index < headerItems.size()) { + headerItems.get(index).setFactor(selected ? 1f : 0f, animated); + } + break; + } + case MediaHolder.TYPE_STICKER_SECTION: { + View view = manager.findViewByPosition(index); + if (view != null && view instanceof StickerSectionView) { + ((StickerSectionView) view).setSelectionFactor(selected ? 1f : 0f, animated); + } else { + notifyItemChanged(index); + } + break; + } + } + } + } + + private final ArrayList stickerSets; + private final EmojiSection recentSection; // favoriteSection + private final EmojiSection trendingSection; // favoriteSection + + public void removeStickerSet (int index) { + if (index >= 0 && index < stickerSets.size()) { + stickerSets.remove(index); + notifyItemRemoved(index + headerItems.size()); + } + } + + public void addStickerSet (int index, TGStickerSetInfo info) { + stickerSets.add(index, info); + notifyItemInserted(index + headerItems.size()); + } + + public void moveStickerSet (int fromIndex, int toIndex) { + TGStickerSetInfo info = stickerSets.remove(fromIndex); + stickerSets.add(toIndex, info); + fromIndex += headerItems.size(); + toIndex += headerItems.size(); + notifyItemMoved(fromIndex, toIndex); + } + + public void setStickerSets (ArrayList stickers) { + if (!stickerSets.isEmpty()) { + int removedCount = stickerSets.size(); + stickerSets.clear(); + notifyItemRangeRemoved(headerItems.size(), removedCount); + } + if (stickers != null && !stickers.isEmpty()) { + int addedCount; + if (!stickers.get(0).isSystem()) { + stickerSets.addAll(stickers); + addedCount = stickers.size(); + } else { + addedCount = 0; + for (int i = 0; i < stickers.size(); i++) { + TGStickerSetInfo stickerSet = stickers.get(i); + if (stickerSet.isSystem()) { + continue; + } + stickerSets.add(stickerSet); + addedCount++; + } + } + notifyItemRangeInserted(headerItems.size(), addedCount); + } + } + + @Override + public MediaHolder onCreateViewHolder (ViewGroup parent, int viewType) { + return MediaHolder.create(context, viewType, onClickListener, this, sectionItemCount, themeProvider); + } + + @Override + public boolean onLongClick (View v) { + // if (parent != null && parent.animatedEmojiOnly) return false; + if (v instanceof StickerSectionView) { + StickerSectionView sectionView = (StickerSectionView) v; + TGStickerSetInfo info = sectionView.getStickerSet(); + if (parent != null) { + if (parent.isAnimatedEmojiOnly()) { + parent.openEmojiSetOptions(info); + } else { + parent.removeStickerSet(info); + } + return true; + } + return false; + } + if ((v instanceof EmojiSectionView)) { + EmojiSectionView sectionView = (EmojiSectionView) v; + EmojiSection section = sectionView.getSection(); + + if (parent != null) { + if (section == recentSection) { + parent.clearRecentStickers(); + return true; + } + } + } + + return false; + } + + @Override + public void onBindViewHolder (MediaHolder holder, int position) { + switch (holder.getItemViewType()) { + case MediaHolder.TYPE_EMOJI_SECTION: { + EmojiSection section = headerItems.get(position); + ((EmojiSectionView) holder.itemView).setSection(section); + holder.itemView.setOnLongClickListener(section == recentSection ? this : null); + break; + } + case MediaHolder.TYPE_STICKER_SECTION: { + Object obj = getObject(position); + ((StickerSectionView) holder.itemView).setSelectionFactor(selectedObject == obj ? 1f : 0f, false); + ((StickerSectionView) holder.itemView).setStickerSet((TGStickerSetInfo) obj); + break; + } + } + } + + @Override + public int getItemViewType (int position) { + if (position < headerItems.size()) { + return MediaHolder.TYPE_EMOJI_SECTION; + } else { + return MediaHolder.TYPE_STICKER_SECTION; + } + } + + @Override + public int getItemCount () { + return headerItems.size() + (stickerSets != null ? stickerSets.size() : 0); + } + + @Override + public void onViewAttachedToWindow (MediaHolder holder) { + if (holder.getItemViewType() == MediaHolder.TYPE_STICKER_SECTION) { + ((StickerSectionView) holder.itemView).attach(); + } + } + + @Override + public void onViewDetachedFromWindow (MediaHolder holder) { + if (holder.getItemViewType() == MediaHolder.TYPE_STICKER_SECTION) { + ((StickerSectionView) holder.itemView).detach(); + } + } + + @Override + public void onViewRecycled (MediaHolder holder) { + if (holder.getItemViewType() == MediaHolder.TYPE_STICKER_SECTION) { + ((StickerSectionView) holder.itemView).performDestroy(); + } + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/section/EmojiSection.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/section/EmojiSection.java new file mode 100644 index 0000000000..54f763dfbb --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/section/EmojiSection.java @@ -0,0 +1,292 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 19/08/2023 + */ +package org.thunderdog.challegram.widget.emoji.section; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.view.View; + +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; + +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.theme.ThemeId; +import org.thunderdog.challegram.tool.Drawables; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.widget.emoji.EmojiLayoutRecyclerController; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.FactorAnimator; + +public class EmojiSection implements FactorAnimator.Target { + public static final int SECTION_EMOJI_RECENT = 0; + public static final int SECTION_EMOJI_SMILEYS = 1; + public static final int SECTION_EMOJI_ANIMALS = 2; + public static final int SECTION_EMOJI_FOOD = 3; + public static final int SECTION_EMOJI_TRAVEL = 4; + public static final int SECTION_EMOJI_SYMBOLS = 5; + public static final int SECTION_EMOJI_FLAGS = 6; + + public static final int SECTION_SWITCH_TO_MEDIA = -11; + public static final int SECTION_EMOJI_TRENDING = -12; + + public final int index; + public float selectionFactor; + + private int iconRes; + public Drawable icon; + public @Nullable Drawable activeIcon; + + private boolean activeDisabled; + private boolean isTrending; + + private @Nullable View view; + private final EmojiLayoutRecyclerController.Callback callback; + + private int activeIconRes; + + public EmojiSection (EmojiLayoutRecyclerController.Callback callback, int sectionIndex, @DrawableRes int iconRes, @DrawableRes int activeIconRes) { + this.callback = callback; + this.index = sectionIndex; + this.activeIconRes = activeIconRes; + this.activeIcon = Drawables.get(callback.getContext().getResources(), activeIconRes); + changeIcon(iconRes); + } + + public void setIsTrending () { + this.isTrending = true; + } + + public boolean isTrending () { + return isTrending; + } + + @Nullable public View getView () { + return view; + } + + public EmojiSection setActiveDisabled () { + activeDisabled = true; + return this; + } + + public void changeIcon (final int iconRes, final int activeIconRes) { + changeIcon(iconRes); + if (this.activeIconRes != activeIconRes) { + this.activeIcon = Drawables.get(callback.getContext().getResources(), this.activeIconRes = activeIconRes); + if (view != null) { + view.invalidate(); + } + } + } + + public void changeIcon (final int iconRes) { + if (this.iconRes != iconRes) { + this.icon = Drawables.get(callback.getContext().getResources(), this.iconRes = iconRes); + if (view != null) { + view.invalidate(); + } + } + } + + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + setFactor(factor); + } + + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) {} + + private @Nullable FactorAnimator animator; + + public EmojiSection setFactor (float toFactor, boolean animated) { + if (selectionFactor != toFactor && animated && view != null) { + if (animator == null) { + animator = new FactorAnimator(0, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180, selectionFactor); + } + animator.animateTo(toFactor); + } else { + if (animator != null) { + animator.forceFactor(toFactor); + } + setFactor(toFactor); + } + return this; + } + + private void setFactor (float factor) { + if (this.selectionFactor != factor) { + this.selectionFactor = factor; + + if (isPanda) { + if (factor == 1f) { + startPandaTimer(); + } else { + cancelPandaTimer(); + } + } + + if (view != null) { + view.invalidate(); + } + } + } + + public void setCurrentView (View view) { + this.view = view; + } + + private boolean makeFirstTransparent; + + public EmojiSection setMakeFirstTransparent () { + this.makeFirstTransparent = true; + return this; + } + + private int offsetHalf; + + public EmojiSection setOffsetHalf (boolean fromRight) { + this.offsetHalf = fromRight ? 1 : -1; + return this; + } + + private boolean isPanda, doesPandaBlink, isPandaBlinking; + private Runnable pandaBlink; + + public EmojiSection setIsPanda (boolean isPanda) { + this.isPanda = isPanda; + return this; + } + + private void setPandaBlink (boolean inBlink) { + if (this.doesPandaBlink != inBlink) { + this.doesPandaBlink = inBlink; + this.activeIcon = Drawables.get(callback.getContext().getResources(), inBlink ? R.drawable.deproko_baseline_animals_filled_blink_24 : activeIconRes); + if (view != null) { + view.invalidate(); + } + } + } + + private void startPandaTimer () { + if (!isPandaBlinking) { + this.isPandaBlinking = true; + if (pandaBlink == null) { + this.pandaBlink = () -> { + if (isPandaBlinking || doesPandaBlink) { + setPandaBlink(!doesPandaBlink); + if (isPandaBlinking) { + scheduleBlink(false); + } + } + }; + } + blinkNum = 0; + scheduleBlink(true); + } + } + + private int blinkNum; + + private void scheduleBlink (boolean firstTime) { + if (view != null) { + long delay; + switch (blinkNum++) { + case 0: { + setPandaBlink(false); + delay = firstTime ? 6000 : 1000; + break; + } + case 1: + case 3: + case 5: { + delay = 140; + break; + } + case 2: + case 4: { + delay = 4000; + break; + } + case 6: { + delay = 370; + break; + } + case 7: { + delay = 130; + break; + } + case 8: { + delay = 4000; + blinkNum = 0; + break; + } + default: { + delay = 1000; + blinkNum = 0; + break; + } + } + view.postDelayed(pandaBlink, delay); + } + + } + + private void cancelPandaTimer () { + if (isPandaBlinking) { + isPandaBlinking = false; + setPandaBlink(false); + if (view != null) { + view.removeCallbacks(pandaBlink); + } + } + } + + public void draw (Canvas c, int cx, int cy) { + boolean isUseDarkMode = callback.isUseDarkMode(); + + if (selectionFactor == 0f || activeDisabled) { + Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2, isUseDarkMode ? Paints.getPorterDuffPaint(Theme.getColor(ColorId.icon, ThemeId.NIGHT_BLACK)) : Paints.getIconGrayPorterDuffPaint()); + } else if (selectionFactor == 1f) { + final Drawable icon = this.activeIcon != null ? activeIcon : this.icon; + Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2, isUseDarkMode ? Paints.getPorterDuffPaint(Theme.getColor(ColorId.iconActive, ThemeId.NIGHT_BLACK)) : Paints.getActiveKeyboardPaint()); + } else { + final Paint grayPaint = isUseDarkMode ? Paints.getPorterDuffPaint(Theme.getColor(ColorId.icon, ThemeId.NIGHT_BLACK)) : Paints.getIconGrayPorterDuffPaint(); + final int grayAlpha = grayPaint.getAlpha(); + + if (makeFirstTransparent) { + int newAlpha = (int) ((float) grayAlpha * (1f - selectionFactor)); + grayPaint.setAlpha(newAlpha); + } else if (isPanda) { + int newAlpha = (int) ((float) grayAlpha * (1f - (1f - AnimatorUtils.DECELERATE_INTERPOLATOR.getInterpolation(1f - selectionFactor)))); + grayPaint.setAlpha(newAlpha); + } + + Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2, grayPaint); + grayPaint.setAlpha(grayAlpha); + + final Drawable icon = this.activeIcon != null ? activeIcon : this.icon; + final Paint iconPaint = Paints.getActiveKeyboardPaint(); + final int sourceIconAlpha = iconPaint.getAlpha(); + int alpha = (int) ((float) sourceIconAlpha * selectionFactor); + iconPaint.setAlpha(alpha); + Drawables.draw(c, icon, cx - icon.getMinimumWidth() / 2, cy - icon.getMinimumHeight() / 2, iconPaint); + iconPaint.setAlpha(sourceIconAlpha); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/section/EmojiSectionView.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/section/EmojiSectionView.java new file mode 100644 index 0000000000..d13d585dde --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/section/EmojiSectionView.java @@ -0,0 +1,62 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 18/08/2023 + */ +package org.thunderdog.challegram.widget.emoji.section; + +import android.content.Context; +import android.graphics.Canvas; +import android.view.View; + +import org.thunderdog.challegram.tool.Screen; + +public class EmojiSectionView extends View { + private int forceWidth = -1; + + public EmojiSectionView (Context context) { + super(context); + } + + private EmojiSection section; + + public void setSection (EmojiSection section) { + if (this.section != null) { + this.section.setCurrentView(null); + } + this.section = section; + if (section != null) { + section.setCurrentView(this); + } + } + + public EmojiSection getSection () { + return section; + } + + public void setForceWidth (int width) { + forceWidth = width; + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + int itemWidth = forceWidth > 0 ? forceWidth : Screen.dp(44); + setMeasuredDimension(MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.EXACTLY)); + } + + @Override + protected void onDraw (Canvas c) { + if (section != null) { + section.draw(c, getMeasuredWidth() / 2, getMeasuredHeight() / 2); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/widget/emoji/section/StickerSectionView.java b/app/src/main/java/org/thunderdog/challegram/widget/emoji/section/StickerSectionView.java new file mode 100644 index 0000000000..91bae02994 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/widget/emoji/section/StickerSectionView.java @@ -0,0 +1,175 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 19/08/2023 + */ +package org.thunderdog.challegram.widget.emoji.section; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Path; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thunderdog.challegram.data.TGStickerSetInfo; +import org.thunderdog.challegram.loader.ImageReceiver; +import org.thunderdog.challegram.loader.gif.GifReceiver; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.PorterDuffColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.DrawAlgorithms; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.Screen; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.core.ColorUtils; +import me.vkryl.core.lambda.Destroyable; + +public class StickerSectionView extends View implements Destroyable, FactorAnimator.Target { + private static final int WIDTH = 44; + private static final int PADDING = 10; + + private final ImageReceiver receiver; + private final GifReceiver gifReceiver; + + private float selectionFactor; + + public StickerSectionView (Context context) { + super(context); + receiver = new ImageReceiver(this, 0); + gifReceiver = new GifReceiver(this); + } + + public void attach () { + receiver.attach(); + gifReceiver.attach(); + } + + public void detach () { + receiver.detach(); + gifReceiver.detach(); + } + + @Override + public void performDestroy () { + receiver.destroy(); + gifReceiver.destroy(); + } + + private TGStickerSetInfo info; + private Path contour; + + public void setStickerSet (@NonNull TGStickerSetInfo info) { + this.info = info; + this.contour = info.getPreviewContour(Math.min(receiver.getWidth(), receiver.getHeight())); + receiver.requestFile(info.getPreviewImage()); + gifReceiver.requestFile(info.getPreviewAnimation()); + } + + private FactorAnimator animator; + + public void setSelectionFactor (float factor, boolean animated) { + if (animated && this.selectionFactor != factor) { + if (animator == null) { + animator = new FactorAnimator(0, this, AnimatorUtils.DECELERATE_INTERPOLATOR, 180L, selectionFactor); + } + animator.animateTo(factor); + } else { + if (animator != null) { + animator.forceFactor(factor); + } + setSelectionFactor(factor); + } + } + + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + if (id == 0) { + setSelectionFactor(factor); + } + } + + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + + } + + private void setSelectionFactor (float factor) { + if (this.selectionFactor != factor) { + this.selectionFactor = factor; + invalidate(); + } + } + + public @Nullable TGStickerSetInfo getStickerSet () { + return info; + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(MeasureSpec.makeMeasureSpec(Screen.dp(WIDTH), MeasureSpec.EXACTLY), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); + setBounds(); + } + + private void setBounds () { + int padding = Screen.dp(PADDING); + int width = receiver.getWidth(), height = receiver.getHeight(); + receiver.setBounds(padding, padding, getMeasuredWidth() - padding, getMeasuredHeight() - padding); + gifReceiver.setBounds(padding, padding, getMeasuredWidth() - padding, getMeasuredHeight() - padding); + if (info != null && (width != receiver.getWidth() || height != receiver.getHeight())) { + this.contour = info.getPreviewContour(Math.min(receiver.getWidth(), receiver.getHeight())); + } + } + + @Override + protected void onDraw (Canvas c) { + int cx = getMeasuredWidth() / 2; + int cy = getMeasuredHeight() / 2; + + final boolean saved = selectionFactor != 0f; + if (saved) { + final int selectionColor = Theme.chatSelectionColor(); + final int selectionAlpha = Color.alpha(selectionColor); + int color = ColorUtils.color((int) ((float) selectionAlpha * selectionFactor), selectionColor); + int radius = Screen.dp(18f) - (int) ((float) Screen.dp(4f) * (1f - selectionFactor)); + + c.drawCircle(cx, cy, radius, Paints.fillingPaint(color)); + c.save(); + float scale = .85f + .15f * (1f - selectionFactor); + c.scale(scale, scale, cx, cy); + } + + if (info.needThemedColorFilter()) { + if (selectionFactor == 0f || selectionFactor == 1f) { + @PorterDuffColorId int colorId = selectionFactor == 0f ? ColorId.icon : ColorId.iconActive; + receiver.setThemedPorterDuffColorId(colorId); + gifReceiver.setThemedPorterDuffColorId(colorId); + } else { + int color = ColorUtils.fromToArgb(Theme.getColor(ColorId.icon), Theme.getColor(ColorId.iconActive), selectionFactor); + receiver.setPorterDuffColorFilter(color); + gifReceiver.setPorterDuffColorFilter(color); + } + } else { + receiver.disablePorterDuffColorFilter(); + gifReceiver.disablePorterDuffColorFilter(); + } + DrawAlgorithms.drawSticker(c, info, gifReceiver, receiver, contour); + + if (saved) { + c.restore(); + } + } +} diff --git a/app/src/main/other/themes/Blue.tgx-theme b/app/src/main/other/themes/Blue.tgx-theme index 9e4278936c..308985b677 100644 --- a/app/src/main/other/themes/Blue.tgx-theme +++ b/app/src/main/other/themes/Blue.tgx-theme @@ -24,14 +24,17 @@ attachPhoto: #68B6F3 attachText, avatar_content, badgeFailedText, badgeMutedText, badgeText, bubble_buttonText, bubble_buttonText_noWallpaper, bubble_dateText, bubble_dateText_noWallpaper, bubble_mediaOverlayText, bubble_mediaReplyText, bubble_mediaReplyText_noWallpaper, bubble_mediaTimeText, bubble_mediaTimeText_noWallpaper, bubble_messageCheckOutline, bubble_overlayText, bubble_overlayText_noWallpaper, bubble_unreadText, bubble_unreadText_noWallpaper, bubbleIn_background, bubbleOut_chatCorrectChosenFillingContent, bubbleOut_chatCorrectFillingContent, bubbleOut_chatNegativeFillingContent, bubbleOut_chatNeutralFillingContent, chatBackground, checkContent, circleButtonChat, circleButtonNegativeIcon, circleButtonNewChannelIcon, circleButtonNewChatIcon, circleButtonNewGroupIcon, circleButtonNewSecretIcon, circleButtonOverlay, circleButtonPositiveIcon, circleButtonRegularIcon, circleButtonThemeIcon, controlContent, filling, fillingPositiveContent, headerButton, headerIcon, headerLightBackground, headerTabActive, headerTabActiveText, headerText, inlineContentActive, messageCorrectChosenFillingContent, messageCorrectFillingContent, messageNegativeLineContent, messageNeutralFillingContent, messageSwipeContent, overlayFilling, passcodeIcon, passcodeText, promoContent, snackbarUpdateAction, snackbarUpdateText, statusBarContent, statusBarLegacyContent, togglerNegativeContent, togglerPositiveContent, tooltip_text, videoSliderActive, white: #FFF avatarArchive, avatarReplies, avatarReplies_big, avatarSavedMessages, avatarSavedMessages_big, tooltip_textLink: #61A9E1 avatarArchivePinned, avatarInactive, avatarInactive_big: #ADB1B6 +bubbleOut_messageAuthorPsa, messageAuthorPsa: #7ABB58 avatarBlue, avatarBlue_big, nameBlue: #5E9CD4 -avatarCyan, avatarCyan_big, nameCyan: #64BACE -avatarGreen, avatarGreen_big, bubbleOut_messageAuthorPsa, messageAuthorPsa, nameGreen: #7ABB58 -avatarOrange, avatarOrange_big, nameOrange: #F29154 -avatarPink, avatarPink_big, namePink: #D979A3 -avatarRed, avatarRed_big, nameRed: #EF615C -avatarViolet, avatarViolet_big: #9388E1 -avatarYellow, avatarYellow_big, nameYellow: #F5AD3D +lineBlue: #50AADF +nameInactive, lineInactive: #787878 +avatarCyan, avatarCyan_big, nameCyan, lineCyan: #64BACE +avatarGreen, avatarGreen_big, nameGreen, lineGreen: #7ABB58 +avatarOrange, avatarOrange_big, nameOrange, lineOrange: #F29154 +avatarPink, avatarPink_big, namePink, linePink: #D979A3 +avatarRed, avatarRed_big, nameRed, lineRed: #EF615C +avatarViolet, avatarViolet_big, nameViolet, lineViolet: #9388E1 +avatarYellow, avatarYellow_big, nameYellow, lineYellow: #F5AD3D background, iv_chatLinkBackground, iv_textReferenceBackground: #F2F2F2 background_icon: #9DA1A5 background_text: #71787E @@ -145,8 +148,6 @@ ledWhite: #BBB ledYellow: #FFCE1F messageSelection: #24A2DC10 messageSwipeBackground: #6BADE0 -nameInactive: #787878 -nameViolet: #9388E2 online: #54CA63 photoHighlightTint1: #EF9286 photoHighlightTint2: #EACEA2 diff --git a/app/src/main/other/themes/Night Black.tgx-theme b/app/src/main/other/themes/Night Black.tgx-theme index 6e56dd1ffe..2f795d08a1 100644 --- a/app/src/main/other/themes/Night Black.tgx-theme +++ b/app/src/main/other/themes/Night Black.tgx-theme @@ -6,18 +6,10 @@ time: 1601377382 dark, parentTheme, shadowDepth, wallpaperUsageId: 1 # attachContact, attachFile, attachInlineBot, attachLocation, attachPhoto, badgeMutedText, badgeText, bubbleIn_background, bubbleOut_background, chatKeyboardButton, headerBackground, headerBarCallMuted, headerLightBackground, inlineContentActive, iv_preBlockBackground, iv_textCodeBackground, iv_textCodeBackgroundPressed, messageSwipeBackground, notification, notificationPlayer, passcode, unread: #212121 -avatarBlue, avatarBlue_big, nameBlue: #80C1FA -avatarCyan, avatarCyan_big, nameCyan: #6CC7DC -avatarGreen, avatarGreen_big, bubbleOut_messageAuthorPsa, messageAuthorPsa, nameGreen: #8CC56E -avatarOrange, avatarOrange_big, nameOrange: #F49F69 -avatarPink, avatarPink_big, namePink: #E181AC -avatarRed, avatarRed_big, nameRed: #F07975 -avatarViolet, avatarViolet_big, nameViolet: #BCB3F9 -avatarYellow, avatarYellow_big, nameYellow: #F9C84A background, bubble_chatSeparator, bubbleIn_outline, bubbleIn_separator, bubbleOut_chatCorrectChosenFillingContent, bubbleOut_chatCorrectFillingContent, bubbleOut_chatNeutralFillingContent, bubbleOut_outline, chatKeyboard, chatSeparator, checkContent, controlContent, iv_chatLinkBackground, messageCorrectChosenFillingContent, messageCorrectFillingContent, messageNegativeLineContent, messageNeutralFillingContent, promoContent, separator, shareSeparator, togglerPositiveContent: #000 background_icon, background_text, background_textLight, bubbleIn_time, bubbleOut_progress, bubbleOut_separator, bubbleOut_time, circleButtonChatIcon, circleButtonOverlayIcon, icon, iv_caption, iv_icon, iv_pageAuthor, iv_pageFooter, textLight, textPlaceholder: #A0A0A0 badge, bubbleIn_progress, bubbleIn_text, bubbleOut_chatCorrectChosenFilling, bubbleOut_chatCorrectFilling, bubbleOut_chatVerticalLine, bubbleOut_inlineIcon, bubbleOut_inlineOutline, bubbleOut_inlineText, bubbleOut_text, bubbleOut_ticks, bubbleOut_ticksRead, chatListAction, chatListVerify, chatSendButton, checkActive, controlActive, headerButtonIcon, headerLightIcon, headerLightText, iconActive, inlineIcon, inlineOutline, inlineText, inputActive, introSectionActive, iv_blockQuoteLine, iv_header, iv_pageTitle, iv_text, iv_textCode, iv_textMarked, iv_textReference, messageCorrectChosenFilling, messageCorrectFilling, messageVerticalLine, notificationLink, online, playerButton, profileSectionActive, profileSectionActiveContent, progress, promo, sliderActive, text, textNeutral, themeBlackWhite, ticks, ticksRead, togglerActive, togglerPositive, unreadText, headerBadge: #FFF -badgeMuted, controlInactive, nameInactive, themeNightBlack, togglerInactiveBackground, headerBadgeMuted: #6A6A6A +badgeMuted, controlInactive, nameInactive, lineInactive, themeNightBlack, togglerInactiveBackground, headerBadgeMuted: #6A6A6A bubble_button_noWallpaper, bubble_buttonRipple_noWallpaper: #A0A0A020 bubble_date_noWallpaper, bubble_mediaReply_noWallpaper, bubble_overlay, bubble_unread_noWallpaper: #A0A0A040 bubble_messageCheckOutline: #EAEAEA @@ -66,3 +58,14 @@ bubbleIn_fillingActiveContent, bubbleOut_fillingActiveContent, fillingActiveCont fillingActive: #A0A0A026 fillingPositive, bubbleOut_fillingPositive, bubbleIn_fillingPositive, bubbleOut_fillingPositive_overlay, bubbleIn_fillingPositive_overlay: #FFF fillingPositiveContent, bubbleOut_fillingPositiveContent, bubbleIn_fillingPositiveContent, bubbleOut_fillingPositiveContent_overlay, bubbleIn_fillingPositiveContent_overlay: #000 + +bubbleOut_messageAuthorPsa, messageAuthorPsa: #8CC56E + +avatarBlue, avatarBlue_big, nameBlue: #368AD1 +avatarCyan, avatarCyan_big, nameCyan: #309EBA +avatarGreen, avatarGreen_big, nameGreen: #40A920 +avatarOrange, avatarOrange_big, nameOrange: #D67722 +avatarPink, avatarPink_big, namePink: #C7508B +avatarRed, avatarRed_big, nameRed: #CC5049 +avatarViolet, avatarViolet_big: #955CDB +avatarYellow, avatarYellow_big, nameYellow: #F9C84A \ No newline at end of file diff --git a/app/src/main/other/themes/Night Blue.tgx-theme b/app/src/main/other/themes/Night Blue.tgx-theme index 9e75c75fa4..c13b5de11d 100644 --- a/app/src/main/other/themes/Night Blue.tgx-theme +++ b/app/src/main/other/themes/Night Blue.tgx-theme @@ -47,7 +47,7 @@ iv_textReferenceOutline: #FFF4 iv_textReferenceOutlinePressed: #FFFFFF20 messageSelection: #61A9E110 messageVerticalLine: #538EBD -nameInactive, themeNightBlack: #6A6A6A +nameInactive, lineInactive, themeNightBlack: #6A6A6A notificationSecure, textSecure: #78E27E placeholder: #FFFFFF0C playerButtonActive: #58A6E0 @@ -65,4 +65,4 @@ fillingActiveContent: #FFF bubbleIn_fillingActive: #7D8E9826 bubbleIn_fillingActiveContent: #FFF bubbleOut_fillingActive: #91AFC826 -bubbleOut_fillingActiveContent: #FFF +bubbleOut_fillingActiveContent: #FFF \ No newline at end of file diff --git a/app/src/main/other/themes/colors-and-properties.xml b/app/src/main/other/themes/colors-and-properties.xml index b0866a28b1..e3fc5e1426 100644 --- a/app/src/main/other/themes/colors-and-properties.xml +++ b/app/src/main/other/themes/colors-and-properties.xml @@ -56,7 +56,7 @@ - + @@ -77,23 +77,23 @@ - + - + - - - + + + - + @@ -144,7 +144,7 @@ - + @@ -155,7 +155,7 @@ - + @@ -223,15 +223,25 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + + @@ -267,7 +277,7 @@ - + diff --git a/app/src/main/res/drawable-mdpi/photo_big_down_left.png b/app/src/main/res/drawable-mdpi/photo_big_down_left.png deleted file mode 100644 index 9707dae83a..0000000000 Binary files a/app/src/main/res/drawable-mdpi/photo_big_down_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/photo_big_down_right.png b/app/src/main/res/drawable-mdpi/photo_big_down_right.png deleted file mode 100644 index 3840c62732..0000000000 Binary files a/app/src/main/res/drawable-mdpi/photo_big_down_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/photo_big_up_left.png b/app/src/main/res/drawable-mdpi/photo_big_up_left.png deleted file mode 100644 index 5525ad40cd..0000000000 Binary files a/app/src/main/res/drawable-mdpi/photo_big_up_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/photo_big_up_right.png b/app/src/main/res/drawable-mdpi/photo_big_up_right.png deleted file mode 100644 index feee88fa14..0000000000 Binary files a/app/src/main/res/drawable-mdpi/photo_big_up_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/photo_small_down_left.png b/app/src/main/res/drawable-mdpi/photo_small_down_left.png deleted file mode 100644 index 6c6b20741b..0000000000 Binary files a/app/src/main/res/drawable-mdpi/photo_small_down_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/photo_small_down_right.png b/app/src/main/res/drawable-mdpi/photo_small_down_right.png deleted file mode 100644 index 388fd9d4be..0000000000 Binary files a/app/src/main/res/drawable-mdpi/photo_small_down_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/photo_small_up_left.png b/app/src/main/res/drawable-mdpi/photo_small_up_left.png deleted file mode 100644 index b7288115b6..0000000000 Binary files a/app/src/main/res/drawable-mdpi/photo_small_up_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/photo_small_up_right.png b/app/src/main/res/drawable-mdpi/photo_small_up_right.png deleted file mode 100644 index fb0aa51627..0000000000 Binary files a/app/src/main/res/drawable-mdpi/photo_small_up_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/photo_big_down_left.png b/app/src/main/res/drawable-xhdpi/photo_big_down_left.png deleted file mode 100644 index 1aaead6f8d..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/photo_big_down_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/photo_big_down_right.png b/app/src/main/res/drawable-xhdpi/photo_big_down_right.png deleted file mode 100644 index 817511c007..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/photo_big_down_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/photo_big_up_left.png b/app/src/main/res/drawable-xhdpi/photo_big_up_left.png deleted file mode 100644 index 07c7557693..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/photo_big_up_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/photo_big_up_right.png b/app/src/main/res/drawable-xhdpi/photo_big_up_right.png deleted file mode 100644 index 06374ee4c2..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/photo_big_up_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/photo_small_down_left.png b/app/src/main/res/drawable-xhdpi/photo_small_down_left.png deleted file mode 100644 index 72d0218063..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/photo_small_down_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/photo_small_down_right.png b/app/src/main/res/drawable-xhdpi/photo_small_down_right.png deleted file mode 100644 index 2650442c30..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/photo_small_down_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/photo_small_up_left.png b/app/src/main/res/drawable-xhdpi/photo_small_up_left.png deleted file mode 100644 index d752ce12a0..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/photo_small_up_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/photo_small_up_right.png b/app/src/main/res/drawable-xhdpi/photo_small_up_right.png deleted file mode 100644 index dfb8d0550d..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/photo_small_up_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/photo_big_down_left.png b/app/src/main/res/drawable-xxhdpi/photo_big_down_left.png deleted file mode 100644 index 4b34018944..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/photo_big_down_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/photo_big_down_right.png b/app/src/main/res/drawable-xxhdpi/photo_big_down_right.png deleted file mode 100644 index ae937a4ab9..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/photo_big_down_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/photo_big_up_left.png b/app/src/main/res/drawable-xxhdpi/photo_big_up_left.png deleted file mode 100644 index 25d1913326..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/photo_big_up_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/photo_big_up_right.png b/app/src/main/res/drawable-xxhdpi/photo_big_up_right.png deleted file mode 100644 index 614fbce69a..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/photo_big_up_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/photo_small_down_left.png b/app/src/main/res/drawable-xxhdpi/photo_small_down_left.png deleted file mode 100644 index 3ed10929db..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/photo_small_down_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/photo_small_down_right.png b/app/src/main/res/drawable-xxhdpi/photo_small_down_right.png deleted file mode 100644 index 5014ccca3b..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/photo_small_down_right.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/photo_small_up_left.png b/app/src/main/res/drawable-xxhdpi/photo_small_up_left.png deleted file mode 100644 index 431032f475..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/photo_small_up_left.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/photo_small_up_right.png b/app/src/main/res/drawable-xxhdpi/photo_small_up_right.png deleted file mode 100644 index c6c98daafc..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/photo_small_up_right.png and /dev/null differ diff --git a/app/src/main/res/drawable/deproko_baseline_close_16.xml b/app/src/main/res/drawable/andrejsharapov_baseline_message_check_16.xml similarity index 54% rename from app/src/main/res/drawable/deproko_baseline_close_16.xml rename to app/src/main/res/drawable/andrejsharapov_baseline_message_check_16.xml index e675b71b59..664fd0b790 100644 --- a/app/src/main/res/drawable/deproko_baseline_close_16.xml +++ b/app/src/main/res/drawable/andrejsharapov_baseline_message_check_16.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> - + android:pathData="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.11 18 22 17.11 22 16V4C22 2.89 21.1 2 20 2M10.47 14L7 10.5L8.4 9.09L10.47 11.17L15.6 6L17 7.41L10.47 14Z" /> + \ No newline at end of file diff --git a/app/src/main/res/drawable/templarian_baseline_import_24.xml b/app/src/main/res/drawable/andrejsharapov_baseline_message_check_24.xml similarity index 55% rename from app/src/main/res/drawable/templarian_baseline_import_24.xml rename to app/src/main/res/drawable/andrejsharapov_baseline_message_check_24.xml index da3bc16091..82215bda37 100644 --- a/app/src/main/res/drawable/templarian_baseline_import_24.xml +++ b/app/src/main/res/drawable/andrejsharapov_baseline_message_check_24.xml @@ -1,10 +1,9 @@ - + android:pathData="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.11 18 22 17.11 22 16V4C22 2.89 21.1 2 20 2M10.47 14L7 10.5L8.4 9.09L10.47 11.17L15.6 6L17 7.41L10.47 14Z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_account_circle_16.xml b/app/src/main/res/drawable/baseline_account_circle_16.xml new file mode 100644 index 0000000000..862a92678b --- /dev/null +++ b/app/src/main/res/drawable/baseline_account_circle_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_account_circle_24.xml b/app/src/main/res/drawable/baseline_account_circle_24.xml new file mode 100644 index 0000000000..392a0de70c --- /dev/null +++ b/app/src/main/res/drawable/baseline_account_circle_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_amazon_24.xml b/app/src/main/res/drawable/baseline_amazon_24.xml new file mode 100644 index 0000000000..1f0a5a220d --- /dev/null +++ b/app/src/main/res/drawable/baseline_amazon_24.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_archive_16.xml b/app/src/main/res/drawable/baseline_archive_16.xml new file mode 100644 index 0000000000..0703f40d3e --- /dev/null +++ b/app/src/main/res/drawable/baseline_archive_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_block_18.xml b/app/src/main/res/drawable/baseline_block_18.xml new file mode 100755 index 0000000000..214af017a5 --- /dev/null +++ b/app/src/main/res/drawable/baseline_block_18.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_book_24.xml b/app/src/main/res/drawable/baseline_book_24.xml new file mode 100644 index 0000000000..a27be7872b --- /dev/null +++ b/app/src/main/res/drawable/baseline_book_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_fullscreen_exit_24.xml b/app/src/main/res/drawable/baseline_bookmark_16.xml similarity index 52% rename from app/src/main/res/drawable/baseline_fullscreen_exit_24.xml rename to app/src/main/res/drawable/baseline_bookmark_16.xml index eec51a6bc1..4600c984e9 100755 --- a/app/src/main/res/drawable/baseline_fullscreen_exit_24.xml +++ b/app/src/main/res/drawable/baseline_bookmark_16.xml @@ -1,9 +1,9 @@ + android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z" /> diff --git a/app/src/main/res/drawable/baseline_create_new_folder_24.xml b/app/src/main/res/drawable/baseline_create_new_folder_24.xml new file mode 100644 index 0000000000..a61cf6dbcf --- /dev/null +++ b/app/src/main/res/drawable/baseline_create_new_folder_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_crown_circle_24.xml b/app/src/main/res/drawable/baseline_crown_circle_24.xml new file mode 100644 index 0000000000..483007512f --- /dev/null +++ b/app/src/main/res/drawable/baseline_crown_circle_24.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_currency_bitcoin_24.xml b/app/src/main/res/drawable/baseline_currency_bitcoin_24.xml new file mode 100644 index 0000000000..7c27a884c9 --- /dev/null +++ b/app/src/main/res/drawable/baseline_currency_bitcoin_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_date_range_10.xml b/app/src/main/res/drawable/baseline_date_range_10.xml deleted file mode 100644 index a79681bafe..0000000000 --- a/app/src/main/res/drawable/baseline_date_range_10.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_delete_alert_24.xml b/app/src/main/res/drawable/baseline_delete_alert_24.xml new file mode 100644 index 0000000000..fdf181ef77 --- /dev/null +++ b/app/src/main/res/drawable/baseline_delete_alert_24.xml @@ -0,0 +1,8 @@ + + + \ + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_reply_all_24.xml b/app/src/main/res/drawable/baseline_drag_handle_24.xml similarity index 64% rename from app/src/main/res/drawable/baseline_reply_all_24.xml rename to app/src/main/res/drawable/baseline_drag_handle_24.xml index 6d4f51381e..ee70455f5e 100644 --- a/app/src/main/res/drawable/baseline_reply_all_24.xml +++ b/app/src/main/res/drawable/baseline_drag_handle_24.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:pathData="M20,9H4v2h16V9zM4,15h16v-2H4V15z" /> diff --git a/app/src/main/res/drawable/baseline_edit_folders_24.xml b/app/src/main/res/drawable/baseline_edit_folders_24.xml new file mode 100644 index 0000000000..92c7d616db --- /dev/null +++ b/app/src/main/res/drawable/baseline_edit_folders_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_extension_16.xml b/app/src/main/res/drawable/baseline_extension_16.xml deleted file mode 100755 index 57cf167bd8..0000000000 --- a/app/src/main/res/drawable/baseline_extension_16.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_finance_24.xml b/app/src/main/res/drawable/baseline_finance_24.xml new file mode 100644 index 0000000000..d05abc41b4 --- /dev/null +++ b/app/src/main/res/drawable/baseline_finance_24.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_flight_24.xml b/app/src/main/res/drawable/baseline_flight_24.xml new file mode 100644 index 0000000000..381127194d --- /dev/null +++ b/app/src/main/res/drawable/baseline_flight_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_folder_96.xml b/app/src/main/res/drawable/baseline_folder_96.xml new file mode 100644 index 0000000000..e020f0d471 --- /dev/null +++ b/app/src/main/res/drawable/baseline_folder_96.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_format_list_bulleted_type_24.xml b/app/src/main/res/drawable/baseline_format_list_bulleted_type_24.xml new file mode 100644 index 0000000000..324dc54a1e --- /dev/null +++ b/app/src/main/res/drawable/baseline_format_list_bulleted_type_24.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_format_quote_close_16.xml b/app/src/main/res/drawable/baseline_format_quote_close_16.xml new file mode 100644 index 0000000000..c3a7791aaa --- /dev/null +++ b/app/src/main/res/drawable/baseline_format_quote_close_16.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_format_quote_close_18.xml b/app/src/main/res/drawable/baseline_format_quote_close_18.xml new file mode 100644 index 0000000000..0a86d355df --- /dev/null +++ b/app/src/main/res/drawable/baseline_format_quote_close_18.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_format_text_26.xml b/app/src/main/res/drawable/baseline_format_text_26.xml deleted file mode 100644 index 2e8eacf80d..0000000000 --- a/app/src/main/res/drawable/baseline_format_text_26.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_fullscreen_24.xml b/app/src/main/res/drawable/baseline_fullscreen_24.xml deleted file mode 100755 index 71d345a757..0000000000 --- a/app/src/main/res/drawable/baseline_fullscreen_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_galaxy_store_24.xml b/app/src/main/res/drawable/baseline_galaxy_store_24.xml new file mode 100644 index 0000000000..a6eb3d6bf9 --- /dev/null +++ b/app/src/main/res/drawable/baseline_galaxy_store_24.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_ghost_24.xml b/app/src/main/res/drawable/baseline_ghost_24.xml new file mode 100644 index 0000000000..e87049a0cd --- /dev/null +++ b/app/src/main/res/drawable/baseline_ghost_24.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_ghost_56.xml b/app/src/main/res/drawable/baseline_ghost_56.xml new file mode 100644 index 0000000000..e072b77d72 --- /dev/null +++ b/app/src/main/res/drawable/baseline_ghost_56.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_home_24.xml b/app/src/main/res/drawable/baseline_home_24.xml new file mode 100644 index 0000000000..57ec9bbe61 --- /dev/null +++ b/app/src/main/res/drawable/baseline_home_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_huawei_24.xml b/app/src/main/res/drawable/baseline_huawei_24.xml new file mode 100644 index 0000000000..ddc8fa7bd2 --- /dev/null +++ b/app/src/main/res/drawable/baseline_huawei_24.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/baseline_identifier_24.xml b/app/src/main/res/drawable/baseline_identifier_24.xml new file mode 100644 index 0000000000..3741758b7a --- /dev/null +++ b/app/src/main/res/drawable/baseline_identifier_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_edit_14.xml b/app/src/main/res/drawable/baseline_info_14.xml similarity index 54% rename from app/src/main/res/drawable/baseline_edit_14.xml rename to app/src/main/res/drawable/baseline_info_14.xml index 3c5ba0e06f..f3d1b1f9d9 100755 --- a/app/src/main/res/drawable/baseline_edit_14.xml +++ b/app/src/main/res/drawable/baseline_info_14.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" /> diff --git a/app/src/main/res/drawable/baseline_link_preview_bg_24.xml b/app/src/main/res/drawable/baseline_link_preview_bg_24.xml new file mode 100644 index 0000000000..db50c9d2b3 --- /dev/null +++ b/app/src/main/res/drawable/baseline_link_preview_bg_24.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/baseline_link_preview_top_layer_24.xml b/app/src/main/res/drawable/baseline_link_preview_top_layer_24.xml new file mode 100644 index 0000000000..c2ae343b36 --- /dev/null +++ b/app/src/main/res/drawable/baseline_link_preview_top_layer_24.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/baseline_local_florist_24.xml b/app/src/main/res/drawable/baseline_local_florist_24.xml new file mode 100644 index 0000000000..9eee018b45 --- /dev/null +++ b/app/src/main/res/drawable/baseline_local_florist_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_location_on_16.xml b/app/src/main/res/drawable/baseline_location_on_16.xml deleted file mode 100644 index 3944cc07d9..0000000000 --- a/app/src/main/res/drawable/baseline_location_on_16.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_logo_telegram_24.xml b/app/src/main/res/drawable/baseline_logo_telegram_24.xml new file mode 100644 index 0000000000..6c6ce25e2f --- /dev/null +++ b/app/src/main/res/drawable/baseline_logo_telegram_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_logout_24.xml b/app/src/main/res/drawable/baseline_logout_24.xml new file mode 100644 index 0000000000..52076a032c --- /dev/null +++ b/app/src/main/res/drawable/baseline_logout_24.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_mark_chat_unread_24.xml b/app/src/main/res/drawable/baseline_mark_chat_unread_24.xml new file mode 100644 index 0000000000..bab3e14af5 --- /dev/null +++ b/app/src/main/res/drawable/baseline_mark_chat_unread_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_notifications_off_16.xml b/app/src/main/res/drawable/baseline_notifications_off_16.xml new file mode 100755 index 0000000000..604915163c --- /dev/null +++ b/app/src/main/res/drawable/baseline_notifications_off_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_party_popper_24.xml b/app/src/main/res/drawable/baseline_party_popper_24.xml new file mode 100755 index 0000000000..5403ece012 --- /dev/null +++ b/app/src/main/res/drawable/baseline_party_popper_24.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_picture_in_picture_24.xml b/app/src/main/res/drawable/baseline_picture_in_picture_24.xml deleted file mode 100755 index d2efff146a..0000000000 --- a/app/src/main/res/drawable/baseline_picture_in_picture_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_play_circle_outline_48.xml b/app/src/main/res/drawable/baseline_play_circle_outline_48.xml deleted file mode 100755 index 2c6cf583b6..0000000000 --- a/app/src/main/res/drawable/baseline_play_circle_outline_48.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_premium_star_20.xml b/app/src/main/res/drawable/baseline_premium_star_20.xml deleted file mode 100644 index c57021ebf9..0000000000 --- a/app/src/main/res/drawable/baseline_premium_star_20.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_redeem_16.xml b/app/src/main/res/drawable/baseline_redeem_16.xml new file mode 100644 index 0000000000..5610559d49 --- /dev/null +++ b/app/src/main/res/drawable/baseline_redeem_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_school_24.xml b/app/src/main/res/drawable/baseline_school_24.xml new file mode 100644 index 0000000000..9ab6dda55d --- /dev/null +++ b/app/src/main/res/drawable/baseline_school_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_small_arrow_down_18.xml b/app/src/main/res/drawable/baseline_small_arrow_down_18.xml new file mode 100644 index 0000000000..0bc98a1f3d --- /dev/null +++ b/app/src/main/res/drawable/baseline_small_arrow_down_18.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_small_arrow_down_24.xml b/app/src/main/res/drawable/baseline_small_arrow_down_24.xml new file mode 100644 index 0000000000..6556149fd0 --- /dev/null +++ b/app/src/main/res/drawable/baseline_small_arrow_down_24.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_sports_esports_24.xml b/app/src/main/res/drawable/baseline_sports_esports_24.xml new file mode 100644 index 0000000000..d33654929c --- /dev/null +++ b/app/src/main/res/drawable/baseline_sports_esports_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_sports_soccer_24.xml b/app/src/main/res/drawable/baseline_sports_soccer_24.xml new file mode 100644 index 0000000000..bddbecb1c0 --- /dev/null +++ b/app/src/main/res/drawable/baseline_sports_soccer_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_stars_18.xml b/app/src/main/res/drawable/baseline_stars_18.xml deleted file mode 100755 index d84d035213..0000000000 --- a/app/src/main/res/drawable/baseline_stars_18.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_thumb_up_24.xml b/app/src/main/res/drawable/baseline_thumb_up_24.xml new file mode 100644 index 0000000000..1e47452a0d --- /dev/null +++ b/app/src/main/res/drawable/baseline_thumb_up_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_update_24.xml b/app/src/main/res/drawable/baseline_update_24.xml new file mode 100644 index 0000000000..875a0f56f1 --- /dev/null +++ b/app/src/main/res/drawable/baseline_update_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_updirectory_arrow_left_14.xml b/app/src/main/res/drawable/baseline_updirectory_arrow_left_14.xml deleted file mode 100644 index 837b99f4d8..0000000000 --- a/app/src/main/res/drawable/baseline_updirectory_arrow_left_14.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_video_chat_24.xml b/app/src/main/res/drawable/baseline_video_chat_24.xml new file mode 100644 index 0000000000..19f2cb4c52 --- /dev/null +++ b/app/src/main/res/drawable/baseline_video_chat_24.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_visibility_20.xml b/app/src/main/res/drawable/baseline_visibility_20.xml deleted file mode 100755 index 9fde5691cf..0000000000 --- a/app/src/main/res/drawable/baseline_visibility_20.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_forward_14.xml b/app/src/main/res/drawable/baseline_warning_14.xml similarity index 74% rename from app/src/main/res/drawable/baseline_forward_14.xml rename to app/src/main/res/drawable/baseline_warning_14.xml index fe546192ce..d723a396d8 100755 --- a/app/src/main/res/drawable/baseline_forward_14.xml +++ b/app/src/main/res/drawable/baseline_warning_14.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z" /> diff --git a/app/src/main/res/drawable/baseline_work_24.xml b/app/src/main/res/drawable/baseline_work_24.xml new file mode 100644 index 0000000000..d9288e1c79 --- /dev/null +++ b/app/src/main/res/drawable/baseline_work_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/deproko_baseline_bots_16.xml b/app/src/main/res/drawable/deproko_baseline_bots_16.xml new file mode 100644 index 0000000000..5e2e278677 --- /dev/null +++ b/app/src/main/res/drawable/deproko_baseline_bots_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/deproko_baseline_mention_24.xml b/app/src/main/res/drawable/deproko_baseline_mention_24.xml deleted file mode 100644 index dea7877b20..0000000000 --- a/app/src/main/res/drawable/deproko_baseline_mention_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/dot_baseline_acc_personal_18.xml b/app/src/main/res/drawable/dot_baseline_acc_personal_18.xml new file mode 100644 index 0000000000..bc98d68bde --- /dev/null +++ b/app/src/main/res/drawable/dot_baseline_acc_personal_18.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_baseline_account_circle_18.xml b/app/src/main/res/drawable/dot_baseline_account_circle_18.xml new file mode 100644 index 0000000000..2dddcd0dc3 --- /dev/null +++ b/app/src/main/res/drawable/dot_baseline_account_circle_18.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_baseline_channel_accept_24.xml b/app/src/main/res/drawable/dot_baseline_channel_accept_24.xml new file mode 100644 index 0000000000..51ec7af9fe --- /dev/null +++ b/app/src/main/res/drawable/dot_baseline_channel_accept_24.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_baseline_channel_circle_18.xml b/app/src/main/res/drawable/dot_baseline_channel_circle_18.xml new file mode 100644 index 0000000000..63f1c0c3ed --- /dev/null +++ b/app/src/main/res/drawable/dot_baseline_channel_circle_18.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_baseline_flip_horizontal_24.xml b/app/src/main/res/drawable/dot_baseline_flip_horizontal_24.xml new file mode 100644 index 0000000000..d80baa6e57 --- /dev/null +++ b/app/src/main/res/drawable/dot_baseline_flip_horizontal_24.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_baseline_group_accept_24.xml b/app/src/main/res/drawable/dot_baseline_group_accept_24.xml new file mode 100644 index 0000000000..b6e4928fe2 --- /dev/null +++ b/app/src/main/res/drawable/dot_baseline_group_accept_24.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_baseline_group_circle_18.xml b/app/src/main/res/drawable/dot_baseline_group_circle_18.xml new file mode 100644 index 0000000000..8a95b05507 --- /dev/null +++ b/app/src/main/res/drawable/dot_baseline_group_circle_18.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_baseline_image_check_24.xml b/app/src/main/res/drawable/dot_baseline_image_check_24.xml new file mode 100644 index 0000000000..62a29abb15 --- /dev/null +++ b/app/src/main/res/drawable/dot_baseline_image_check_24.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_baseline_profile_accept_24.xml b/app/src/main/res/drawable/dot_baseline_profile_accept_24.xml new file mode 100644 index 0000000000..956e89a2c5 --- /dev/null +++ b/app/src/main/res/drawable/dot_baseline_profile_accept_24.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dotvhs_baseline_folders_reorder_24.xml b/app/src/main/res/drawable/dotvhs_baseline_folders_reorder_24.xml new file mode 100644 index 0000000000..8b98304cc3 --- /dev/null +++ b/app/src/main/res/drawable/dotvhs_baseline_folders_reorder_24.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/dotvhs_baseline_gift_24.xml b/app/src/main/res/drawable/dotvhs_baseline_gift_24.xml new file mode 100644 index 0000000000..8de3e49b8f --- /dev/null +++ b/app/src/main/res/drawable/dotvhs_baseline_gift_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/templarian_baseline_cat_24.xml b/app/src/main/res/drawable/templarian_baseline_cat_24.xml new file mode 100644 index 0000000000..cf80c9a99f --- /dev/null +++ b/app/src/main/res/drawable/templarian_baseline_cat_24.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/templarian_baseline_comment_12.xml b/app/src/main/res/drawable/templarian_baseline_comment_12.xml deleted file mode 100644 index b3d2f27140..0000000000 --- a/app/src/main/res/drawable/templarian_baseline_comment_12.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/templarian_baseline_comment_20.xml b/app/src/main/res/drawable/templarian_baseline_comment_20.xml deleted file mode 100644 index fa5be38dea..0000000000 --- a/app/src/main/res/drawable/templarian_baseline_comment_20.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/templarian_baseline_folder_plus_24.xml b/app/src/main/res/drawable/templarian_baseline_folder_plus_24.xml new file mode 100644 index 0000000000..fd039bea2b --- /dev/null +++ b/app/src/main/res/drawable/templarian_baseline_folder_plus_24.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/templarian_baseline_folder_remove_24.xml b/app/src/main/res/drawable/templarian_baseline_folder_remove_24.xml new file mode 100644 index 0000000000..205dee6af7 --- /dev/null +++ b/app/src/main/res/drawable/templarian_baseline_folder_remove_24.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/templarian_outline_comment_22.xml b/app/src/main/res/drawable/templarian_outline_comment_22.xml deleted file mode 100644 index 18cdbbe61e..0000000000 --- a/app/src/main/res/drawable/templarian_outline_comment_22.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index cdf01d0c39..d0937a211d 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -33,6 +33,7 @@ + @@ -48,6 +49,7 @@ + @@ -92,6 +94,8 @@ + + @@ -134,6 +138,7 @@ + @@ -171,6 +176,7 @@ + @@ -237,6 +243,9 @@ + + + @@ -295,14 +304,23 @@ + + + + + + + + + + + - - @@ -317,6 +335,8 @@ + + @@ -344,10 +364,13 @@ + + + @@ -392,6 +415,7 @@ + @@ -472,6 +496,8 @@ + + @@ -578,10 +604,11 @@ + - - - + + + @@ -758,6 +785,7 @@ + @@ -833,6 +861,9 @@ + + + @@ -1036,7 +1067,10 @@ + + + @@ -1219,6 +1253,8 @@ + + @@ -1232,6 +1268,20 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 65a41e3931..41f12af399 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,6 +84,7 @@ %2$s, %1$s %1$s %2$s %1$s, %2$s + %1$s, %2$s %1$s, %2$s +%1$d -%1$d @@ -96,6 +97,7 @@ %1$s (%2$s) %1$s and %2$s %1$s (%2$s) + %1$s / %2$s %1$s %1$s %1$s: %2$s @@ -176,6 +178,11 @@ %1$s week %1$s weeks + Slow mode is active. You can send the next message in %1$s second + Slow mode is active. You can send the next message in %1$s seconds + Slow mode is active. You can send the next message in %1$s minute + Slow mode is active. You can send the next message in %1$s minutes + in %1$s hour in %1$s hours %1$s chat @@ -280,21 +287,16 @@ Messages Select Chat Message - Send anonymously - Send as %1$s + Text is disabled + Admins of this group disabled ability to send text messages. Reply - Reply as %1$s - Reply anonymously - Comment anonymously Comment - Comment as %1$s as %1$s Anonymous Admin Deleted message Unsupported message Unsupported message You have a new message - %1$s: %2$s Voice Message %1$s (%2$s) Video Message @@ -423,12 +425,14 @@ Invited %1$s person to the live stream Invited %1$s people to the live stream + + + Audio Voice Photo Video GIF - %1$s Sticker File Location Contact @@ -443,6 +447,8 @@ Disabled %1$s (invisible) Phone number has been copied to the clipboard + Your ID has been copied to the clipboard + Peer ID has been copied to the clipboard Username has been copied to the clipboard Link has been copied to the clipboard Link has been copied to the clipboard. It works only for members of this chat. @@ -489,12 +495,13 @@ Choose from Gallery Remove photo Clear history + Clear history for everyone + Delete all messages + Delete all Saved Messages Cancel Add Members - Add members Delete and leave Just add to group - Remove from group Delete chat Delete Thread Slide to Cancel @@ -503,6 +510,11 @@ Please allow Telegram X to use camera in System Settings > Apps > Telegram X > Permissions Settings Log out + Delete account + Permanently delete account + Access to your chats will be lost forever. All existing chats will see you as Deleted Account. Using the same phone number will create a new account. + Delete my account now + Permanently delete the account, and all associated information from Telegram servers. Mute Unmute Return to Group @@ -580,6 +592,16 @@ Gallery Camera + Set Profile Photo + Set Group Photo + Set Channel Photo + Set Public Photo + Remove Profile Photo + Remove Group Photo + Remove Channel Photo + Remove Public Photo + Delete current photo? + All message types Text messages Photos @@ -635,10 +657,8 @@ %1$s renamed the group to "%2$s" You renamed the group to "%1$s" Channel renamed to "%1$s" - Somebody Group upgraded to supergroup Channel created - You created the channel Channel photo removed Channel renamed Channel renamed to "%1$s" @@ -667,13 +687,13 @@ No photos or videos yet Voice Message Getting Link Info… + Link info is not available Chats and Contacts Global Search Global Recent Admins Clear recent searches? - PS: Rotate your screen to minimize player Public link is too short You can use only a–z, 0–9 and underscores for a Telegram public link. Save to Gallery @@ -845,7 +865,6 @@ Leave no trace on our servers Have a self-destruct timer Do not allow forwarding - Edit message Not now Subscribers will be notified when you post. Subscribers will receive a silent notification. @@ -853,6 +872,7 @@ Self-Destruct Timer Done Off + Immediately Chats Notifications Enabled @@ -918,10 +938,8 @@ Two-Step Verification Enabled Disabled - Current Session This Device Devices - Devices No Other Active Sessions You can log in to Telegram from other mobile, tablet and desktop devices, using the same phone number. All your data will be instantly synchronized. Terminate All Other Sessions @@ -1029,6 +1047,7 @@ Come again later New Add + + Add You haven\'t blocked anyone yet Block %1$s? @@ -1050,6 +1069,12 @@ %2$s gifted you %1$s month of Telegram Premium for %3$s %2$s gifted you %1$s months Telegram Premium for %3$s + You sent Gift Code for %1$s month of Telegram Premium + You sent Gift Code for %1$s months of Telegram Premium + + %2$s sent you a Gift Code for %1$s month of Telegram Premium + %2$s sent you a Gift Code for %1$s months Telegram Premium + Data from the \"%1$s\" button was transferred to the bot You disabled the self-destruct timer @@ -1204,6 +1229,7 @@ %1$s pinned %2$s %1$s pinned a **game** %1$s pinned a **message** + %1$s pinned a **giveaway** %1$s pinned a **location** %1$s pinned a **poll** %1$s pinned a **quiz** @@ -1216,6 +1242,7 @@ %1$s pinned a **video** %1$s pinned a **video message** %1$s pinned a **voice message** + %1$s pinned a **story** %1$s pinned a **contact** Unknown Track Unknown Artist @@ -1268,6 +1295,10 @@ Today Yesterday Public group + Public Photo + Photo set + No Photo set + Public Photo will be visible for everyone who is restricted from seeing your regular profile photo. Group Rename contact Themes @@ -1381,7 +1412,7 @@ Chat preview Sorry, this feature is available yet only on Android %1$s (%2$s) or higher Remove sticker set %1$s? - Remove emoji set %1$s? + Remove emoji pack %1$s? Remove Delete pack Add pack @@ -1613,8 +1644,8 @@ View Pack Bio Any details such as age, occupation or city.\nExample: 23 y.o. designer from San Francisco. - Photo has expired - Video has expired + Expired photo + Expired video Send with Enter Hide keyboard on chat scroll Hashtag has been copied to the clipboard @@ -1624,6 +1655,11 @@ View Profile View Channel View Group + View Channel + View Post + Open App + Open Website + Open Bot View Bot Send as copy Remove captions in copies @@ -1665,8 +1701,12 @@ Note: you will be able to download these files any time later. Freed %1$s of disk space Link Preview + Are you sure you want to clear history for this channel? This action cannot be undone. + **No, seriously.**\n\nThis will delete **all messages** for **all subscribers**. There will be no way to restore them. + Are you sure you want to clear history for this chat for all users? This action cannot be undone. Are you sure you want to clear history for this chat? This action cannot be undone. Are you sure you want to clear all **Saved Messages**? This action cannot be undone. + **No, seriously.**\n\nThis will delete **all Saved Messages**. There will be no way to restore them. Channel Cyan Pink @@ -1751,7 +1791,7 @@ %1$s allowed Enable Quick Reaction - Availability of specific reactions depends on group\'s and channel\'s settings. + Some groups and channels may not allow specific reactions. Allow members to react to group messages Available reactions @@ -1761,6 +1801,7 @@ Quick Reaction Disabled Big reactions are interactive buttons under messages presenting each reaction + When limited, avatars display only in scenarios when you are likely to recognise the user. Big Reactions Chats Channels @@ -1795,6 +1836,12 @@ Edit Messages Manage video chats Manage live streams + Manage topics + Stories + Messages + Post Stories + Edit Stories of Others + Delete Stories of Others Remain anonymous No groups to show Groups in common will be shown here @@ -1976,7 +2023,11 @@ Post messages Edit messages Delete messages + Post stories + Edit stories of others + Delete stories of others Add admins + Manage topics Remain anonymous Manage video chats Manage live streams @@ -2089,6 +2140,44 @@ + Added: – Removed: + %1$s changed accent color from %2$s to %3$s + You changed accent color from %1$s to %2$s + + %1$s changed emoji status from %2$s to %3$s + You changed emoji status from %1$s to %2$s + %1$s changed emoji status from none to %2$s + You changed emoji status from none to %1$s + %1$s changed emoji status from %2$s to none + You changed emoji status from %1$s to none + + %1$s changed emoji from %2$s to %3$s + You changed emoji from %1$s to %2$s + %1$s changed emoji from none to %2$s + You changed emoji from none to %1$s + %1$s changed emoji from %2$s to none + You changed emoji from %1$s to none + + %1$s changed profile emoji from %2$s to %3$s + You changed profile emoji from %1$s to %2$s + %1$s changed profile emoji from none to %2$s + You changed profile emoji from none to %1$s + %1$s changed profile emoji from %2$s to none + You changed profile emoji from %1$s to none + + %1$s changed profile color from %2$s to %3$s + You changed profile color from %1$s to %2$s + %1$s changed profile color from none to %2$s + You changed profile color from none to %1$s + %1$s changed profile color from %2$s to none + You changed profile color from %1$s to none + + %1$s changed profile color and icon from %2$s to %3$s + You changed profile color and icon from %1$s to %2$s + %1$s changed profile color and icon from none to %2$s + You changed profile color and icon from none to %1$s + %1$s changed profile color and icon from %2$s to none + You changed profile color and icon from %1$s to none + Until %1$s Network Usage Recently Used @@ -2097,6 +2186,7 @@ Remove from Favorites Hold to record audio. Tap to switch to video. Hold to record video. Tap to switch to audio. + Sending animated emoji requires **Telegram Premium** %1$s\'s Telegram client doesn\'t support this feature. They need to install an update first. Record HQ Round Videos Discard Video Message @@ -2148,6 +2238,7 @@ Compress audio in videos Edit text in messages using shortcuts: `**`**bold**`**`, `__`__italic__`__`, `~~`~~strikethrough~~`~~`, ````monospace````, `||`||spoiler||`||`, `[`text`](`url`)` Message not found + This message is from a private chat Find Contacts To help you connect with friends and family, allow %1$s access to your contacts. To help you message friends and family on Telegram, allow access to your contacts.\n\nTap Settings > Permissions, and turn Contacts on. @@ -2171,6 +2262,7 @@ Would you like to enable extended link previews in Secret Chats? Note that link previews are generated on Telegram servers. Please note that inline bots are provided by third-party developers. For the bot to work, the symbols you type after the bot\'s username are sent to the respective developer. Clear Recent Emoji + Clear Recent Reactions Doodle Arrow Rectangle @@ -2193,6 +2285,7 @@ Phone Call Set as current Archive sticker set %1$s? You can restore it later in Settings > Stickers > Archived. + Archive emoji pack %1$s? You can restore it later in Settings > Emoji > Archived. Archive Archive pack Clear drawing @@ -2246,6 +2339,7 @@ Scheduled Detect current sunset & sunrise time Determining location… + There is currently no sunrise or sunset at your current location. From To Switch to night theme based on ambient lighting or your time preference. @@ -2254,7 +2348,7 @@ Switch to night theme based on your system settings. Switch to night theme based on the value provided by system. Bots - Logged in with Telegram + Logged In with Telegram **No active logins**\n\nYou can log in on websites that support signing in with Telegram. Disconnect All Websites You can log in on websites that support signing in with Telegram. @@ -2454,6 +2548,9 @@ Delete Synced Contacts This will remove your contacts from the Telegram servers. If \'Sync Contacts\' is enabled, contacts will be re-synced. You allowed this bot to message you when you logged in on %1$s. + You allowed this bot to message you in its web-app. + You allowed this bot to message you when you added it to your attachment menu. + You allowed this bot to message you when launched its "%1$s" app. None Share Reply @@ -2761,6 +2858,8 @@ Contact Support Tell us about any issues; logging out doesn\'t usually help. Remember, logging out kills all your Secret Chats. Downloaded media will be erased from this device. + Sign out without deleting the account. You can sign back in using the same phone number to access your chats. + Tell us about any issues; after you delete the account, we won\'t be able to restore any data you lose in the process. Push Services @@ -2809,6 +2908,14 @@ Attach Menu Media + Red + Orange + Pink + Green + Purple + Cyan + Blue + Hex R @@ -3076,9 +3183,16 @@ Private Secret Group Chats + Groups Channels Bots + Read Unread + Muted + Archived + Contacts + Non-Contacts + %1$s bot %1$s bots @@ -3095,8 +3209,11 @@ This feature is not available. Please make sure the app is up-to-date, or wait for new updates. Page not found or no longer exists. Sorry, you don\'t have access to this chat or channel. + Sorry, anonymous administrators cannot leave reactions or participate in polls. Sorry, you don\'t have access to this chat or channel. Sticker set not found or no longer exists. + Chat is inaccessible + Can\'t access the chat Sorry, you are a member of too many groups or channels. Please leave some before joining a new one. Sorry, this phone number is banned. Contact us at login@telegram.org if you need help. Sorry, you have too many location-based groups already. Please delete one of your existing ones first. @@ -3107,6 +3224,7 @@ Sorry, you can interact only with mutual contacts at the moment. There are too many bots in this group. Please remove some of the bots you\'re not using first. Sorry, this group has too many admins. Please remove one of the existing admins first. + Folder must contain at least one chat Notification Style Sound and pop-up @@ -3281,8 +3399,6 @@ Recurring payment You successfully paid %1$s - https://play.google.com/store/apps/details?id=org.thunderdog.challegram - Other Checking for new messages Account: %1$s @@ -3374,6 +3490,8 @@ %1$s (%2$s)\n%3$s %1$s\n\n%2$s + Allowed %1$s/%2$s + Allowed %1$s/%2$s %1$s of %2$s %1$s of %2$s %1$s of %2$s @@ -3392,6 +3510,7 @@ Only admins can send files in this group Only admins can send photos in this group Only admins can send videos in this group + Only admins can send stories in this group Only admins can send stickers and GIFs in this group Only admins can send voice messages in this group Only admins can send video messages in this group @@ -3424,6 +3543,8 @@ Admins have restricted you from sending photos in this group until %1$s The admins of this group have restricted your ability to send videos. Admins have restricted you from sending videos in this group until %1$s + The admins of this group have restricted your ability to send stories. + Admins have restricted you from sending stories in this group until %1$s The admins of this group have restricted your ability to send stickers and GIFs. Admins have restricted you from sending stickers and GIFs in this group until %1$s The admins of this group have restricted your ability to send polls. @@ -3573,6 +3694,8 @@ Phone Number Who can see your Phone Number? Users who have your number saved in their contacts will also see it on Telegram. + Who can see your bio? + You can restrict who can see the bio on your profile with granular precision. Visible Visible only for contacts @@ -3584,6 +3707,16 @@ Everybody can see your phone number Everybody (%1$s) can see your phone number + Visible + Visible only for contacts + Hidden + Nobody can see your bio + Nobody (%1$s) can see your bio + Only contacts can see your bio + Only contacts (%1$s) can see your bio + Everybody can see your bio + Everybody (%1$s) can see your bio + Only contacts can find you on Telegram @@ -3997,6 +4130,7 @@ Profile Photos Last Seen & Online Phone Number + Bio Forward My Messages Call Me @@ -4115,17 +4249,13 @@ View %1$s other comment View %1$s other comments Discussion started - No comments yet… - No replies yet… Sorry, this post has been removed from the discussion group. View Thread View in chat - View comment Comments Replies Replies - Replies Are you sure you want to delete all **reply notifications**? This action cannot be undone. Are you sure you want to delete all **reply notifications** and stop receiving them? @@ -4158,18 +4288,13 @@ Check for updates Install an update Checking for updates… - Latest version is installed Download update (%1$s) Download update View source code changes A new **Telegram X** update is available. Would you like to download? (%1$s) A new **Telegram X** `%2$s` update is available. Would you like to download? (%1$s) - Download and install - **Commit**: [%1$s](%2$s) **Telegram X** `%1$s` - **Created**: %1$s - Source Code Scan QR Use camera to sign in on the desktop app. @@ -4190,6 +4315,8 @@ Members will be able to send only one message every %1$s hour. Members will be able to send only one message every %1$s hours. Off + %1$sm + %1$sm Invite Links Primary Invite Link @@ -4292,7 +4419,6 @@ Automatically delete new posts after %1$s for subscribers of the channel. Chat Wallpaper Preview - Sorry, this type of chat wallpaper is not yet supported. Apply Chat Wallpaper View Wallpaper Chat Wallpaper @@ -4421,6 +4547,8 @@ App: %1$s System: %1$s + recommended + sponsored What are sponsored messages? Unlike other apps, Telegram never uses your private data to target ads. Sponsored messages on Telegram are based solely on the topic of the public channels in which they are shown. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored messages.\n\nUnlike other apps, Telegram doesn\'t track whether you tapped on a sponsored message and doesn\'t profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can’t spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers a free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible advertisers at: @@ -4554,6 +4682,9 @@ Sending this sticker requires a **Telegram Premium** subscription. This setting requires a **Telegram Premium** subscription. This feature requires a **Telegram Premium** subscription. + Subscribe to **Telegram Premium** to move the "%1$s" folder. + You have reached the limit of **%1$d** folders. You can double the limit to **%2$d** folders by subscribing to **Telegram Premium**. + Sorry, you can\'t add more than **%1$d** chats to a folder. You can increase this limit to **%2$d** by subscribing to **Telegram Premium**. %1$s doesn\'t accept voice messages. %1$s doesn\'t accept video messages. @@ -4619,6 +4750,18 @@ watched %1$s hour ago watched %1$s hours ago + Edited just now + Edited today at %1$s + Edited yesterday at %1$s + Edited on %1$s at %2$s + Edited on %1$s at %2$s + Edited %1$s second ago + Edited %1$s seconds ago + Edited %1$s minute ago + Edited %1$s minutes ago + Edited %1$s hour ago + Edited %1$s hours ago + reacted just now reacted today at %1$s reacted yesterday at %1$s @@ -4639,8 +4782,6 @@ show less - - Unknown Translate Paste translation @@ -4682,8 +4823,184 @@ Set until tomorrow at %1$s Set until %1$s at %2$s - Text formatting Tools Translate + + %1$s emoji + %1$s emoji + %1$s emoji pack + %1$s emoji packs + %1$s sticker pack + %1$s sticker packs + Add %1$s emoji + Add %1$s emoji + Remove %1$s emoji + Remove %1$s emoji + Add %1$s emoji pack + Add %1$s emoji packs + Remove %1$s emoji pack + Remove %1$s emoji packs + Add %1$s sticker set + Add %1$s sticker sets + Remove %1$s sticker set + Remove %1$s sticker sets + Loading… + Emoji used from %1$s pack + Emoji used from %1$s + * %1$s + You can add up to %1$s emoji packs. Unused packs are archived when you add more. + + Suggest Animated Emoji + Emoji Packs + Sticker Packs + Emoji + Stickers and Emoji + Paste Emoji + Share Emoji + + Archive Settings + Control what chats remain archived even if they are unmuted and get a new message. + + Unmuted chats + Always keep archived + Keep archived chats in the Archive even if they are unmuted and get a new message. + + Chats from folders + Always keep archived + Keep archived chats from folders in the Archive even if they are unmuted and get a new message. + + %1$s set installed + %1$s sets installed + Sticker set settings + %1$s set installed + %1$s sets installed + Emoji packs settings + Archive %1$s pack + Archive %1$s packs + Install %1$s pack + Install %1$s packs + + Reactions + + Only admins can write messages in broadcast groups. + + %1$s more + %1$s more + + Avatars in Reactions + Always + Limited + Never + + %1$s more + %1$s more + + Chat types + Chat Folders + Create folders for different groups of chats and quickly switch between them. + Settings + Appearance + Top + Bottom + Change Icon + Add Chats + You have reached the limit of **%1$d** folders. + Sorry, you can\'t add more than **%1$d** chats to a folder. Please create a new one. + New Folder + Create New Folder + Hide Folder + To display %1$s folder again, go to %2$s > %3$s + Hide All Chats + Edit Folder + Edit Folders + Remove Folder + Are you sure you want to remove this folder? Your chats will not be deleted. + **Folder is empty**\n\nNo chats currently belong to this folder. + Are you sure you want to remove "%1$s" from the always included list? + Are you sure you want to remove "%1$s" from the always included list? + Are you sure you want to remove "%1$s" from the always included list? + Are you sure you want to remove "%1$s" from the always excluded list? + Are you sure you want to remove "%1$s" from the always excluded list? + Are you sure you want to remove "%1$s" from the always excluded list? + Include Chats + You can select only one cloud and only one secret chat. + You can select up to %1$s cloud chats and the same number of secret chats. + Exclude Chats + You can select only one cloud chat and only one secret chat + You can select up to %1$s cloud chats and the same number of secret chats. + Included Chats + Choose chats or types of chats that will appear in this folder. + Excluded Chats + Choose chats or types of chats that will **not** appear in this folder. + Add Chats to Included + Add Chats to Excluded + Folder name + Name + Recommended Folders + Add to folder + Remove from folder + Choose a folder + local folder + main list + Icon only + Label only + Label with icon + Display folders at the top + Show %1$s more + Show %1$s more + + Experimental settings + **Warning:** those are experimental settings. Some may not work. Others may break the app. Any of them might disappear without a trace.\n\nUse at your own risk. + Show Peer IDs in Profiles + Show API identifiers of users and chats in their profiles + Enable Chat Folders + Enable early access to folders. Appearance of this feature is planned to be changed in future versions of Telegram X, and it includes some unfinished work. Credit: [PR #297](https://github.com/TGX-Android/Telegram-X/pull/297) by nikita-toropov + + Peer ID + User ID + Secret Chat ID + Bot ID + Group ID + Supergroup ID + Channel ID + + %1$s just started a giveaway of Telegram Premium subscriptions for its followers. + %1$s winner of the giveaway was randomly selected by Telegram and received their gift link in a private message. + %1$s winners of the giveaway were randomly selected by Telegram and received their gift link in a private message. + + Giveaway + + %1$s winner to be selected on %2$s + %1$s winners to be selected on %2$s + %1$s winner to be selected + %1$s winners to be selected + %1$s: %2$s + + Link preview media will be enlarged + Link preview media will be minimized + Link preview will appear above the text + Link preview will appear below the text + + This message is a reply to the message that was deleted. + This message is from another chat. Tap again to view. + + Swipe to choose specific link preview + + This is a service message from Telegram + This message is unsupported in the installed version of Telegram X. + + Please enter your 2-Step verification password to confirm the action. + Reason + **Note:** you can simultaneously use your Telegram account on as many devices as you want, and change the phone number without starting over.\n\nYou should use this option if you want to erase all data associated with your account on Telegram servers forever. + **Wait. You are entering danger zone.**\n\nThis action will permanently erase all data associated with your account on Telegram servers. There will be no way to restore it.\n\nAccess to any channels or groups, where you are the only admin, **will be lost forever**. + Show alternatives + **No, seriously.**\n\nThis will permanently delete all data associated with your account. There will be no way to restore it.\n\nAccess to any channels or groups, where you are the only admin, **will be lost forever**. + Proceed anyway + **Danger:** this is the last confirmation prompt.\n\nOnce you press the button below, all data will be erased from Telegram servers. Using the same phone number will create a new empty account. + Alright, delete account. + Account deleted + Your account %1$s (%2$s) is now deleted from Telegram servers, and can no longer be restored.\n\nUsing the same phone number will create a new account. + View once + diff --git a/app/src/main/res/xml/automotive_app_desc.xml b/app/src/main/res/xml/automotive_app_desc.xml new file mode 100644 index 0000000000..ec76c3d5fb --- /dev/null +++ b/app/src/main/res/xml/automotive_app_desc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index eacc01c233..edd85965e9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -45,10 +45,11 @@ configurations.all { dependencies { compileOnly(gradleApi()) + // 8.1.0-8.1.2 create APKs that do not launch on Android 4.x (armeabi-v7a) implementation("com.android.tools.build:gradle:8.0.2") - implementation("com.google.gms:google-services:4.3.15") + implementation("com.google.gms:google-services:4.4.0") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") - implementation("com.squareup.okhttp3:okhttp:4.9.3") - implementation("com.squareup.okhttp3:logging-interceptor:4.9.3") + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") implementation("com.beust:klaxon:5.6") } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 8271283eef..97976ba118 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -14,10 +14,11 @@ // File with static configuration, that is meant to be adjusted only once object Config { + const val PRIMARY_SDK_VERSION = 21 const val MIN_SDK_VERSION = 16 val JAVA_VERSION = org.gradle.api.JavaVersion.VERSION_11 val EXOPLAYER_EXTENSIONS = arrayOf("ffmpeg", "flac", "opus", "vp9") - val SUPPORTED_ABI = arrayOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") + val SUPPORTED_ABI = arrayOf("armeabi-v7a", "arm64-v8a"/*, "x86"*/, "x86_64") } object LibraryVersions { @@ -27,7 +28,7 @@ object LibraryVersions { const val ANNOTATIONS = "1.3.0" } -class AbiVariant (val flavor: String, vararg val filters: String = Config.SUPPORTED_ABI, val displayName: String = filters[0], val sideLoadOnly: Boolean = false) { +class AbiVariant (val flavor: String, vararg val filters: String = arrayOf(), val displayName: String = filters[0], val sideLoadOnly: Boolean = false) { init { if (filters.isEmpty()) error("Empty filters passed") @@ -37,16 +38,21 @@ class AbiVariant (val flavor: String, vararg val filters: String = Config.SUPPOR } } - val minSdkVersion: Int + val is64Bit: Boolean get() { - var minSdkVersion = maxOf(21, Config.MIN_SDK_VERSION) for (filter in filters) { if (filter != "arm64-v8a" && filter != "x86_64") { - minSdkVersion = Config.MIN_SDK_VERSION - break + return false } } - return minSdkVersion + return true + } + + val minSdkVersion: Int + get() = if (is64Bit) { + Config.PRIMARY_SDK_VERSION + } else { + Config.MIN_SDK_VERSION } } @@ -54,14 +60,14 @@ object Abi { const val UNIVERSAL = 0 const val ARMEABI_V7A = 1 const val ARM64_V8A = 2 - const val X86 = 3 + // const val X86 = 3 const val X64 = 4 val VARIANTS = mapOf( - Pair(UNIVERSAL, AbiVariant("universal", displayName = "universal", sideLoadOnly = true)), + Pair(UNIVERSAL, AbiVariant("universal", displayName = "universal", filters = arrayOf("arm64-v8a", "x86_64"))), Pair(ARMEABI_V7A, AbiVariant("arm32", "armeabi-v7a")), Pair(ARM64_V8A, AbiVariant("arm64", "arm64-v8a")), - Pair(X86, AbiVariant("x86", "x86")), + // Pair(X86, AbiVariant("x86", "x86")), Pair(X64, AbiVariant("x64", "x86_64", displayName = "x64")) ) } diff --git a/buildSrc/src/main/kotlin/me/vkryl/plugin/ModulePlugin.kt b/buildSrc/src/main/kotlin/me/vkryl/plugin/ModulePlugin.kt index 3c8148f9df..5ebf9915ad 100644 --- a/buildSrc/src/main/kotlin/me/vkryl/plugin/ModulePlugin.kt +++ b/buildSrc/src/main/kotlin/me/vkryl/plugin/ModulePlugin.kt @@ -88,6 +88,10 @@ open class ModulePlugin : Plugin { val appId = getOrSample("app.id") val appName = getOrSample("app.name") val appDownloadUrl = getOrSample("app.download_url") + val googlePlayUrl = properties.getProperty("app.google_download_url", null) + val galaxyStoreUrl = properties.getProperty("app.galaxy_download_url", null) + val huaweiAppGalleryUrl = properties.getProperty("app.huawei_download_url", null) + val amazonAppStoreUrl = properties.getProperty("app.amazon_download_url", null) val isExampleBuild = appId.startsWith("com.example.") || appId.startsWith("org.example.") val isExperimentalBuild = isExampleBuild || keystore == null || properties.getProperty("app.experimental", "false") == "true" val dontObfuscate = isExampleBuild || properties.getProperty("app.dontobfuscate", "false") == "true" @@ -112,7 +116,10 @@ open class ModulePlugin : Plugin { androidExt.apply { compileSdkVersion(versions.getOrThrow("version.sdk_compile").toInt()) buildToolsVersion(versions.getOrThrow("version.build_tools")) - ndkVersion = versions.getOrThrow("version.ndk") + + // TODO: investigate why AGP 8.1.2 forces default ndkVersion, + // despite having it properly set in productFlavors.${flavor} + ndkVersion = versions.getOrThrow("version.ndk_legacy") ndkPath = File(sdkDirectory, "ndk/$ndkVersion").absolutePath compileOptions { @@ -273,11 +280,14 @@ open class ModulePlugin : Plugin { applicationId = appId buildConfigString("PROJECT_NAME", appName) - buildConfigString("MARKET_URL", "https://play.google.com/store/apps/details?id=${appId}") buildConfigString("SAFETYNET_API_KEY", safetyNetToken) buildConfigString("DOWNLOAD_URL", appDownloadUrl) + buildConfigString("GOOGLE_PLAY_URL", googlePlayUrl) + buildConfigString("GALAXY_STORE_URL", galaxyStoreUrl) + buildConfigString("HUAWEI_APPGALLERY_URL", huaweiAppGalleryUrl) + buildConfigString("AMAZON_APPSTORE_URL", amazonAppStoreUrl) buildConfigString("OPENSSL_VERSION", openSslVersion) buildConfigString("OPENSSL_VERSION_FULL", openSslVersionFull) diff --git a/buildSrc/src/main/kotlin/me/vkryl/task/CheckEmojiKeyboardTask.kt b/buildSrc/src/main/kotlin/me/vkryl/task/CheckEmojiKeyboardTask.kt index f1bc9f48c3..6da52e4f5a 100644 --- a/buildSrc/src/main/kotlin/me/vkryl/task/CheckEmojiKeyboardTask.kt +++ b/buildSrc/src/main/kotlin/me/vkryl/task/CheckEmojiKeyboardTask.kt @@ -177,8 +177,11 @@ open class CheckEmojiKeyboardTask : BaseTask() { val supportedSet = mutableSetOf() + var maxEmojiLength = 0 + for (chunk in supported) { for (emoji in chunk) { + maxEmojiLength = maxOf(maxEmojiLength, emoji.length) val toned = findTones(emoji/*, defaultSkinTone*/) val originalEmoji = tone2dAliases[toned.first] ?: toned.first val tones = toned.second @@ -331,6 +334,8 @@ open class CheckEmojiKeyboardTask : BaseTask() { package org.thunderdog.challegram.tool import me.vkryl.annotation.Autogenerated + + const val MAX_EMOJI_LENGTH = ${maxEmojiLength} @Autogenerated fun colored1dSet () = setOf( ${tonedEmoji1d.keys.joinToString(",\n ") { javaWrap(it) }} diff --git a/buildSrc/src/main/kotlin/me/vkryl/task/GenerateResourcesAndThemesTask.kt b/buildSrc/src/main/kotlin/me/vkryl/task/GenerateResourcesAndThemesTask.kt index eb626a6a63..f8f2423664 100644 --- a/buildSrc/src/main/kotlin/me/vkryl/task/GenerateResourcesAndThemesTask.kt +++ b/buildSrc/src/main/kotlin/me/vkryl/task/GenerateResourcesAndThemesTask.kt @@ -16,9 +16,11 @@ import App import groovy.util.Node import groovy.util.NodeList import groovy.xml.XmlParser +import okhttp3.internal.toHexString import org.gradle.api.tasks.TaskAction import java.io.File import java.util.* +import java.util.zip.CRC32 import kotlin.contracts.ExperimentalContracts class Theme (file: File) { @@ -548,12 +550,14 @@ open class GenerateResourcesAndThemesTask : BaseTask() { @Autogenerated @Retention(RetentionPolicy.SOURCE) @IntDef({ + PropertyId.NONE, ${themeProperties.joinToString(",\n ") { "PropertyId.${it.camelCaseToUpperCase()}" }} }) public @interface PropertyId { int + NONE = 0, ${ themeProperties.mapIndexed { index, propertyId -> "${propertyId.camelCaseToUpperCase()} = ${index + 1}" @@ -646,6 +650,7 @@ open class GenerateResourcesAndThemesTask : BaseTask() { @Autogenerated @Retention(RetentionPolicy.SOURCE) @IntDef({ + ColorId.NONE, ${tintedColors.joinToString(",\n ") { "ColorId.$it" }} @@ -683,6 +688,7 @@ open class GenerateResourcesAndThemesTask : BaseTask() { @Autogenerated @Retention(RetentionPolicy.SOURCE) @IntDef({ + ColorId.NONE, ${porterDuffColors.joinToString(",\n ") { "ColorId.$it" }} @@ -1014,5 +1020,107 @@ open class GenerateResourcesAndThemesTask : BaseTask() { } } } + + // TdCompileAssert.kt & TdUnsupported.kt + // TODO: move file generation to vkryl/td + + val typesRegex = Regex("public abstract static class ([a-zA-Z]+) extends Object \\{[^@]+[^\\n]+\\s+@IntDef\\(\\{([^}]+)") + val tdApi = File("tdlib/src/main/java/org/drinkless/tdlib/TdApi.java").readText() + var matchResult: MatchResult? + val hashCodes = mutableSetOf() + matchResult = typesRegex.find(tdApi) + val tdTypes = mutableListOf, String>>>() + while (matchResult != null) { + val (typeName, constructorsRaw) = matchResult.destructured + val constructors = constructorsRaw.split(",").map { + it.trim().replace(".CONSTRUCTOR", "").replace(Regex("^${typeName}"), "") + }.toSortedSet() + val hashSrc = "${typeName}_${constructors.joinToString("_")}" + val crc32 = CRC32() + crc32.update(hashSrc.toByteArray()) + + val hashCode = crc32.value.toHexString() + if (!hashCodes.add(hashCode)) + error("hashCode collision for ${typeName}") + val stubMethodName = "assert${typeName}_${hashCode}"; + if (stubMethodName.length > 65535) { + error("Too long (${stubMethodName.length}) method name for type: ${typeName}") + } + tdTypes.add(Pair(typeName, Pair(constructors, stubMethodName))) + matchResult = matchResult.next() + } + tdTypes.sortBy { it.first } + + writeToFile("vkryl/td/src/main/kotlin/me/vkryl/td/TdCompileAssert.kt") { kt -> + kt.append(""" + /* + * This file is a part of tdlib-utils + * Copyright © Vyacheslav Krylov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + @file:JvmName("Td") + @file:JvmMultifileClass + + package me.vkryl.td + + import me.vkryl.annotation.Autogenerated + import org.drinkless.tdlib.TdApi.* + + // Сause compilation error whenever new constructor is added to the TDLib type + // by calling one of the corresponding stub methods below in places, where you expect to support all of them. + + ${tdTypes.joinToString("\n ") { + "@Autogenerated fun ${it.second.second} (): ${it.first}? = null // ${it.second.first.joinToString(", ")}" + }} + """.trimIndent()) + } + + writeToFile("vkryl/td/src/main/kotlin/me/vkryl/td/TdUnsupported.kt") { kt -> + kt.append(""" + /* + * This file is a part of tdlib-utils + * Copyright © Vyacheslav Krylov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + @file:JvmName("Td") + @file:JvmMultifileClass + + package me.vkryl.td + + import me.vkryl.annotation.Autogenerated + import org.drinkless.tdlib.TdApi.* + + // Use throw unsupported(object) to group multiple crashes of the same kind into one report, + // and access critical places via Find Usages for the methods below when any new constructors are added + + ${tdTypes.joinToString("\n ") { + "@Autogenerated fun unsupported (data: ${it.first}): NotImplementedError = NotImplementedError(data.toString())" + }} + """.trimIndent()) + } } } \ No newline at end of file diff --git a/scripts/private/build-ffmpeg-impl.sh b/scripts/private/build-ffmpeg-impl.sh index 79047ee9e7..63c3651963 100755 --- a/scripts/private/build-ffmpeg-impl.sh +++ b/scripts/private/build-ffmpeg-impl.sh @@ -2,6 +2,12 @@ set -e function build_one { + if [ -z "$ANDROID_NDK_ROOT" -a "$ANDROID_NDK_ROOT" == "" ]; then + echo -e "${STYLE_ERROR}Failed! NDK is empty. Run 'export ANDROID_NDK_ROOT=[PATH_TO_NDK]'${STYLE_END}" + exit + fi + validate_dir "$ANDROID_NDK_ROOT" + LIBVPX_INCLUDE_DIR="$THIRDPARTY_LIBRARIES/libvpx/build/$CPU/include" LIBVPX_LIB_DIR="$THIRDPARTY_LIBRARIES/libvpx/build/$CPU/lib" @@ -25,14 +31,11 @@ function build_one { validate_file "$RANLIB" validate_dir "$LINK" - LIBS=${PREBUILT}/lib64/clang/12.0.9/lib/linux - validate_dir "$LIBS" - echo "Cleaning..." rm -f config.h make clean || true - echo "Configuring... ${NDK}" + echo "Configuring ffmpeg... CPU: ${CPU}, NDK: ${ANDROID_NDK_ROOT}" ./configure \ --nm="${NM}" \ @@ -58,7 +61,7 @@ function build_one { --sysroot="$SYSROOT" \ --extra-cflags="-fvisibility=hidden -ffunction-sections -fdata-sections -g -fno-omit-frame-pointer -w -Werror -Wl,-Bsymbolic -Os -DCONFIG_LINUX_PERF=0 -DANDROID $OPTIMIZE_CFLAGS -I$LIBVPX_INCLUDE_DIR --static -fPIC" \ --extra-ldflags="-L$LIBVPX_LIB_DIR $EXTRA_LDFLAGS -L -lvpx -Wl,-Bsymbolic -nostdlib -lc -lm -ldl -fPIC" \ - --extra-libs="-lunwind $EXTRA_LIBS" \ + --extra-libs="$EXTRA_LIBS" \ \ --enable-version3 \ --enable-gpl \ @@ -117,12 +120,6 @@ function checkPreRequisites { exit fi - if [ -z "$ANDROID_NDK" -a "$ANDROID_NDK" == "" ]; then - echo -e "${STYLE_ERROR}Failed! NDK is empty. Run 'export NDK=[PATH_TO_NDK]'${STYLE_END}" - exit - fi - - validate_dir "$ANDROID_NDK" test "$CPU_COUNT" } @@ -133,81 +130,110 @@ popd > /dev/null ## common -PREBUILT=$ANDROID_NDK/toolchains/llvm/prebuilt/$BUILD_PLATFORM -SYSROOT=$PREBUILT/sysroot +pushd "$THIRDPARTY_LIBRARIES/ffmpeg" +# 64-bit, minSdk 21 +ANDROID_NDK_VERSION=$ANDROID_NDK_VERSION_PRIMARY +ANDROID_NDK_ROOT="$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION" +PREBUILT="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$BUILD_PLATFORM" +SYSROOT="$PREBUILT/sysroot" + +validate_dir "$ANDROID_NDK_ROOT" validate_dir "$PREBUILT" validate_dir "$SYSROOT" -pushd "$THIRDPARTY_LIBRARIES/ffmpeg" - -#x86_64 +# arm64-v8a ANDROID_API=21 -LINK=$SYSROOT/usr/lib/x86_64-linux-android/$ANDROID_API -CROSS_PREFIX=$PREBUILT/bin/x86_64-linux-android +LINK=$SYSROOT/usr/lib/aarch64-linux-android/$ANDROID_API +CROSS_PREFIX=$PREBUILT/bin/aarch64-linux-android CC=${CROSS_PREFIX}${ANDROID_API}-clang CXX=${CROSS_PREFIX}${ANDROID_API}-clang++ LD=$CC AS=$CC -ARCH=x86_64 -CPU=x86_64 +ARCH=arm64 +CPU=arm64-v8a PREFIX=./build/$CPU -ADDITIONAL_CONFIGURE_FLAG="--disable-asm" +ADDITIONAL_CONFIGURE_FLAG="--disable-asm --enable-optimizations" OPTIMIZE_CFLAGS="" -EXTRA_LIBS="" +EXTRA_LIBS="-lunwind" EXTRA_LDFLAGS="" +# FIXME ADDITIONAL_CONFIGURE_FLAG="--enable-neon --enable-optimizations" build_one -#arm64-v8a +# x86_64 ANDROID_API=21 -LINK=$SYSROOT/usr/lib/aarch64-linux-android/$ANDROID_API -CROSS_PREFIX=$PREBUILT/bin/aarch64-linux-android +LINK=$SYSROOT/usr/lib/x86_64-linux-android/$ANDROID_API +CROSS_PREFIX=$PREBUILT/bin/x86_64-linux-android CC=${CROSS_PREFIX}${ANDROID_API}-clang CXX=${CROSS_PREFIX}${ANDROID_API}-clang++ LD=$CC AS=$CC -ARCH=arm64 -CPU=arm64-v8a +ARCH=x86_64 +CPU=x86_64 PREFIX=./build/$CPU -ADDITIONAL_CONFIGURE_FLAG="--disable-asm --enable-optimizations" +ADDITIONAL_CONFIGURE_FLAG="--disable-asm" OPTIMIZE_CFLAGS="" -EXTRA_LIBS="" +EXTRA_LIBS="-lunwind" EXTRA_LDFLAGS="" -# FIXME ADDITIONAL_CONFIGURE_FLAG="--enable-neon --enable-optimizations" build_one -#arm v7n +# 32-bit, minSdk 16 +ANDROID_NDK_VERSION=$ANDROID_NDK_VERSION_LEGACY +ANDROID_NDK_ROOT="$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION" +PREBUILT="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$BUILD_PLATFORM" +SYSROOT="$PREBUILT/sysroot" + +validate_dir "$PREBUILT" +validate_dir "$SYSROOT" + +# armeabi-v7a ANDROID_API=16 LINK=$SYSROOT/usr/lib/arm-linux-androideabi/$ANDROID_API CROSS_PREFIX=$PREBUILT/bin/arm-linux-androideabi CC=$PREBUILT/bin/armv7a-linux-androideabi${ANDROID_API}-clang CXX=$PREBUILT/bin/armv7a-linux-androideabi${ANDROID_API}-clang++ -LD=$CC AS=$CC ARCH=arm CPU=armv7-a PREFIX=./build/$CPU ADDITIONAL_CONFIGURE_FLAG="--enable-neon" OPTIMIZE_CFLAGS="-marm -march=$CPU -mfloat-abi=softfp" -EXTRA_LDFLAGS=-L${PREBUILT}/lib64/clang/12.0.9/lib/linux -EXTRA_LIBS=-lclang_rt.builtins-arm-android +if [[ ${ANDROID_NDK_VERSION%%.*} -ge 23 ]]; then + LD=$CC + LIBS_DIR="${PREBUILT}/lib64/clang/12.0.9/lib/linux" + validate_dir "$LIBS_DIR" + EXTRA_LDFLAGS="-L${LIBS_DIR}" + EXTRA_LIBS="-lunwind -lclang_rt.builtins-arm-android" +else + LD="${PREBUILT}/arm-linux-androideabi/bin/ld.gold" + EXTRA_LDFLAGS="" + EXTRA_LIBS="-lgcc" +fi build_one -#x86 platform +# x86 ANDROID_API=16 LINK=$SYSROOT/usr/lib/i686-linux-android/$ANDROID_API CROSS_PREFIX=$PREBUILT/bin/i686-linux-android CC=${CROSS_PREFIX}${ANDROID_API}-clang CXX=${CROSS_PREFIX}${ANDROID_API}-clang++ -LD=$CC AS=$CC ARCH=x86 CPU=i686 PREFIX=./build/$CPU ADDITIONAL_CONFIGURE_FLAG="--disable-x86asm --disable-inline-asm --disable-asm" OPTIMIZE_CFLAGS="-march=$CPU" -EXTRA_LDFLAGS=-L${PREBUILT}/lib64/clang/12.0.9/lib/linux -EXTRA_LIBS=-lclang_rt.builtins-i686-android +if [[ ${ANDROID_NDK_VERSION%%.*} -ge 23 ]]; then + LD=$CC + LIBS_DIR="${PREBUILT}/lib64/clang/12.0.9/lib/linux" + validate_dir "$LIBS_DIR" + EXTRA_LDFLAGS="-L${LIBS_DIR}" + EXTRA_LIBS=-lclang_rt.builtins-i686-android +else + LD="${PREBUILT}/i686-linux-android/bin/ld.gold" + EXTRA_LDFLAGS="" + EXTRA_LIBS="-lgcc" +fi build_one # Copy headers to all platform-specific folders diff --git a/scripts/private/build-vpx-impl.sh b/scripts/private/build-vpx-impl.sh index 62cc4b3c83..228b81779a 100755 --- a/scripts/private/build-vpx-impl.sh +++ b/scripts/private/build-vpx-impl.sh @@ -11,12 +11,12 @@ function checkPreRequisites { exit fi - if [ -z "$ANDROID_NDK" -a "$ANDROID_NDK" == "" ]; then - echo -e "${STYLE_ERROR}Failed! NDK is empty. Run 'export NDK=[PATH_TO_NDK]'${STYLE_END}" + if [ -z "$ANDROID_SDK_ROOT" -a "$ANDROID_SDK_ROOT" == "" ]; then + echo -e "${STYLE_ERROR}Failed! ANDROID_SDK_ROOT is empty. Run 'export ANDROID_SDK_ROOT=[PATH_TO_SDK]'${STYLE_END}" exit fi - validate_dir "$ANDROID_NDK" + validate_dir "$ANDROID_SDK_ROOT" test "$CPU_COUNT" } @@ -29,28 +29,14 @@ popd > /dev/null pushd "$THIRDPARTY_LIBRARIES/libvpx" -# configuration - -PREBUILT=$ANDROID_NDK/toolchains/llvm/prebuilt/$BUILD_PLATFORM -SYSROOT=$PREBUILT/sysroot -ABIS=("armeabi-v7a" "arm64-v8a" "x86" "x86_64") -CFLAGS_="-DANDROID -fpic -fpie" -LDFLAGS_="" - # the function itself configure_abi() { + CFLAGS_="-DANDROID -fpic -fpie" + LDFLAGS_="" case ${ABI} in - armeabi-v7a) - ANDROID_API=16 - TARGET="armv7-android-gcc --enable-neon --disable-neon-asm" - NDK_ABIARCH="armv7a-linux-androideabi" - CFLAGS="${CFLAGS_} -Os -march=armv7-a -marm -mfloat-abi=softfp -mfpu=neon -mthumb -D__thumb__" - LDFLAGS="${LDFLAGS_}" - ASFLAGS="" - CPU=armv7-a - ;; arm64-v8a) + ANDROID_NDK_VERSION=$ANDROID_NDK_VERSION_PRIMARY ANDROID_API=21 TARGET="arm64-android-gcc" NDK_ABIARCH="aarch64-linux-android" @@ -59,16 +45,8 @@ configure_abi() { ASFLAGS="" CPU=arm64-v8a ;; - x86) - ANDROID_API=16 - TARGET="x86-android-gcc" - NDK_ABIARCH="i686-linux-android" - CFLAGS="${CFLAGS_} -O3 -march=i686 -msse3 -mfpmath=sse -m32 -fPIC" - LDFLAGS="-m32" - ASFLAGS="-D__ANDROID__" - CPU=i686 - ;; x86_64) + ANDROID_NDK_VERSION=$ANDROID_NDK_VERSION_PRIMARY ANDROID_API=21 TARGET="x86_64-android-gcc" NDK_ABIARCH="x86_64-linux-android" @@ -76,9 +54,36 @@ configure_abi() { LDFLAGS="" ASFLAGS="-D__ANDROID__" CPU=x86_64 + ;; + armeabi-v7a) + ANDROID_NDK_VERSION=$ANDROID_NDK_VERSION_LEGACY + ANDROID_API=16 + TARGET="armv7-android-gcc --enable-neon --disable-neon-asm" + NDK_ABIARCH="armv7a-linux-androideabi" + CFLAGS="${CFLAGS_} -Os -march=armv7-a -marm -mfloat-abi=softfp -mfpu=neon -mthumb -D__thumb__" + LDFLAGS="${LDFLAGS_}" + ASFLAGS="" + CPU=armv7-a + ;; + x86) + ANDROID_NDK_VERSION=$ANDROID_NDK_VERSION_LEGACY + ANDROID_API=16 + TARGET="x86-android-gcc" + NDK_ABIARCH="i686-linux-android" + CFLAGS="${CFLAGS_} -O3 -march=i686 -msse3 -mfpmath=sse -m32 -fPIC" + LDFLAGS="-m32" + ASFLAGS="-D__ANDROID__" + CPU=i686 ;; esac + ANDROID_NDK_ROOT="$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION" + PREBUILT="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$BUILD_PLATFORM" + SYSROOT="$PREBUILT/sysroot" + + validate_dir "$ANDROID_NDK_ROOT" + echo "${STYLE_INFO}- Using NDK ${ANDROID_NDK_ROOT}${STYLE_END}" + export CFLAGS="${CFLAGS}" export CPPFLAGS="${CFLAGS}" export CXXFLAGS="${CFLAGS} -std=c++11" @@ -149,10 +154,9 @@ configure_make() { make -j"$CPU_COUNT" install } -for ((i=0; i < ${#ABIS[@]}; i++)) -do - configure_make "${ABIS[i]}" - echo -e "${STYLE_INFO}- libvpx build ended for ${ABIS[i]}${STYLE_END}" +for ABI in x86 armeabi-v7a x86_64 arm64-v8a ; do + configure_make "$ABI" + echo -e "${STYLE_INFO}- libvpx build ended for ${ABI}${STYLE_END}" done popd diff --git a/scripts/private/run-cmake-impl.sh b/scripts/private/run-cmake-impl.sh index 2b32d54b5e..3c52fc2197 100755 --- a/scripts/private/run-cmake-impl.sh +++ b/scripts/private/run-cmake-impl.sh @@ -1,14 +1,14 @@ #!/bin/bash set -e -NDK_CMAKE_DIR="$ANDROID_SDK_ROOT/cmake/3.18.1" -NDK_CMAKE_BIN="$NDK_CMAKE_DIR/bin/cmake" -NDK_NINJA_BIN="$NDK_CMAKE_DIR/bin/ninja" +CMAKE_DIR="$ANDROID_SDK_ROOT/cmake/${CMAKE_VERSION}" +CMAKE_BIN="$CMAKE_DIR/bin/cmake" +NINJA_BIN="$CMAKE_DIR/bin/ninja" TARGET_DIR="$1" CMAKE_ARGS=${@:2} -validate_file "$NDK_CMAKE_BIN" -validate_file "$NDK_NINJA_BIN" +validate_file "$CMAKE_BIN" +validate_file "$NINJA_BIN" validate_dir "$TARGET_DIR" function run_cmake { @@ -17,12 +17,19 @@ function run_cmake { test -d "$ARG_ABI" || mkdir "$ARG_ABI" pushd "$ARG_ABI" > /dev/null - $NDK_CMAKE_BIN -DANDROID_ABI="${ARG_ABI}" \ - -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK}/build/cmake/android.toolchain.cmake" \ + if (( $ARG_API_LEVEL >= 21 )); then + ANDROID_NDK_ROOT="$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION_PRIMARY" + else + ANDROID_NDK_ROOT="$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION_LEGACY" + fi + validate_dir "$ANDROID_NDK_ROOT" + + $CMAKE_BIN -DANDROID_ABI="${ARG_ABI}" \ + -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake" \ -DANDROID_NATIVE_API_LEVEL="${ARG_API_LEVEL}" \ ${CMAKE_ARGS} \ -GNinja ../.. - $NDK_NINJA_BIN + $NINJA_BIN popd > /dev/null } @@ -33,8 +40,8 @@ test -d build || mkdir build pushd build > /dev/null run_cmake arm64-v8a 21 -run_cmake armeabi-v7a 16 run_cmake x86_64 21 +run_cmake armeabi-v7a 16 run_cmake x86 16 popd > /dev/null diff --git a/scripts/set-env.sh b/scripts/set-env.sh index 5b16c7e01f..0b2e91a896 100755 --- a/scripts/set-env.sh +++ b/scripts/set-env.sh @@ -54,22 +54,21 @@ if [ ! "$IGNORE_SDK" ]; then fi fi - NDK_VERSION=$(scripts/./read-property.sh version.properties version.ndk) CMAKE_VERSION=$(scripts/./read-property.sh version.properties version.cmake) - if [[ ! "$NDK_VERSION" =~ ^[_0-9\.]+$ ]]; then - echo "${STYLE_ERROR}Invalid NDK version: $NDK_VERSION!${STYLE_END}" + if [[ ! "$CMAKE_VERSION" =~ ^[_0-9\.]+$ ]]; then + echo "${STYLE_ERROR}Invalid CMake version: $CMAKE_VERSION!${STYLE_END}" exit 1 fi - ANDROID_NDK="$ANDROID_SDK_ROOT/ndk/$NDK_VERSION" + ANDROID_NDK_VERSION_PRIMARY=$(scripts/./read-property.sh version.properties version.ndk_primary) + ANDROID_NDK_VERSION_LEGACY=$(scripts/./read-property.sh version.properties version.ndk_legacy) - CC="$ANDROID_NDK/toolchains/llvm/prebuilt/$BUILD_PLATFORM/bin/clang" - CXX="$ANDROID_NDK/toolchains/llvm/prebuilt/$BUILD_PLATFORM/bin/clang++" - LD="$ANDROID_NDK/toolchains/llvm/prebuilt/$BUILD_PLATFORM/bin/ld.lld" + test -d "$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION_PRIMARY" || echo -e "${STYLE_WARN}Android NDK $ANDROID_NDK_VERSION_PRIMARY is not installed.${STYLE_END}" + test -d "$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION_LEGACY" || echo -e "${STYLE_WARN}Android NDK $ANDROID_NDK_VERSION_LEGACY is not installed.${STYLE_END}" - PATH="$ANDROID_NDK/prebuilt/$BUILD_PLATFORM/bin:$ANDROID_SDK_ROOT/cmake/$CMAKE_VERSION/bin:$ANDROID_NDK:$ANDROID_SDK_ROOT/tools/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH" + PATH="$ANDROID_SDK_ROOT/cmake/$CMAKE_VERSION/bin:$ANDROID_SDK_ROOT/tools/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH" - (test -d "$ANDROID_SDK_ROOT" && (test -d "$ANDROID_NDK" || echo -e "${STYLE_WARN}Android NDK $NDK_VERSION is not installed.${STYLE_END}")) || echo -e "${STYLE_WARN}Android SDK is not installed.${STYLE_END}" + (test -d "$ANDROID_SDK_ROOT") || echo -e "${STYLE_WARN}Android SDK is not installed.${STYLE_END}" fi # Export @@ -78,13 +77,10 @@ export CPU_COUNT export BUILD_PLATFORM if [ ! "$IGNORE_SDK" ]; then - export NDK_VERSION export CMAKE_VERSION - export ANDROID_NDK + export ANDROID_NDK_VERSION_LEGACY + export ANDROID_NDK_VERSION_PRIMARY export ANDROID_SDK_ROOT - export CC - export CXX - export LD fi export STYLE_END diff --git a/scripts/setup-sdk.sh b/scripts/setup-sdk.sh index e905078fe1..73106a55b9 100755 --- a/scripts/setup-sdk.sh +++ b/scripts/setup-sdk.sh @@ -20,19 +20,23 @@ popd # Downloading packages BUILD_TOOLS_VERSION=$(read-property.sh version.properties version.build_tools) -NDK_VERSION=$(read-property.sh version.properties version.ndk) COMPILE_SDK_VERSION=$(read-property.sh version.properties version.sdk_compile) CMAKE_VERSION=$(read-property.sh version.properties version.cmake) +ANDROID_NDK_VERSION_PRIMARY=$(read-property.sh version.properties version.ndk_primary) +ANDROID_NDK_VERSION_LEGACY=$(read-property.sh version.properties version.ndk_legacy) yes | "$ANDROID_SDK_ROOT"/cmdline-tools/latest/bin/sdkmanager --licenses yes | "$ANDROID_SDK_ROOT"/cmdline-tools/latest/bin/sdkmanager --update yes | "$ANDROID_SDK_ROOT"/cmdline-tools/latest/bin/sdkmanager --install \ "platforms;android-$COMPILE_SDK_VERSION" \ "build-tools;$BUILD_TOOLS_VERSION" \ - "ndk;$NDK_VERSION" \ + "ndk;$ANDROID_NDK_VERSION_PRIMARY" \ + "ndk;$ANDROID_NDK_VERSION_LEGACY" \ "cmake;$CMAKE_VERSION" test -d "$ANDROID_SDK_ROOT" || (echo "ANDROID_SDK_ROOT ($ANDROID_SDK_ROOT) not found!" && exit 1) -test -d "$ANDROID_NDK" || (echo "ANDROID_NDK ($ANDROID_NDK) not found!" && exit 1) +test -d "$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION_PRIMARY" || (echo "ANDROID_NDK ($ANDROID_NDK_VERSION_PRIMARY) not found!" && exit 1) +test -d "$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION_LEGACY" || (echo "ANDROID_NDK ($ANDROID_NDK_VERSION_LEGACY) not found!" && exit 1) -echo "SDK setup is now complete!" \ No newline at end of file +echo "SDK setup is now complete!" +echo "build-tools: ${BUILD_TOOLS_VERSION}, ndk_primary: ${ANDROID_NDK_VERSION_PRIMARY}, ndk_legacy: ${ANDROID_NDK_VERSION_LEGACY}" \ No newline at end of file diff --git a/tdlib b/tdlib index 50c8a0abca..51e8bee36f 160000 --- a/tdlib +++ b/tdlib @@ -1 +1 @@ -Subproject commit 50c8a0abca49b37d2af6d8467a35755ddf32825b +Subproject commit 51e8bee36f0b453532f9b0faf4727c0fc1db105d diff --git a/thirdparty/ExoPlayer b/thirdparty/ExoPlayer index f425665582..5df25aefd9 160000 --- a/thirdparty/ExoPlayer +++ b/thirdparty/ExoPlayer @@ -1 +1 @@ -Subproject commit f42566558294c91f0c1425299b3f4e322767d90c +Subproject commit 5df25aefd9234f91716960430a4540baa8971315 diff --git a/version.properties b/version.properties index bfbbadcaac..50a5c121a2 100644 --- a/version.properties +++ b/version.properties @@ -1,12 +1,12 @@ # App -version.app=1649 +version.app=1674 version.major=0 # Anchor date point in app versioning version.creation=873642600564 # Native bundle (/app/jni) -version.jni=224.0.1 +version.jni=224.0.2 # LevelDB (/vkryl/leveldb) -version.leveldb=5.0.0 +version.leveldb=6.0.0 # Emoji (/app/src/main/assets/emoji) version.emoji=6 @@ -14,8 +14,10 @@ version.emoji=6 version.cmdline_tools=8512546 # https://developer.android.com/ndk/downloads/revision_history -version.ndk=23.2.8568313 version.cmake=3.22.1 +version.ndk_primary=23.2.8568313 +# TODO: Try switching to 22.1.7171670, but requires patching TDLib build +version.ndk_legacy=23.2.8568313 # https://developer.android.com/studio/releases/build-tools version.build_tools=34.0.0 diff --git a/vkryl/android b/vkryl/android index b9a28ab7f2..cba4608f5b 160000 --- a/vkryl/android +++ b/vkryl/android @@ -1 +1 @@ -Subproject commit b9a28ab7f24bc706147d2b8d9d21fd859aceaf39 +Subproject commit cba4608f5b7ba3556647f9fcc1d5bbb85dfa1377 diff --git a/vkryl/core b/vkryl/core index 9de4f01641..2c06947759 160000 --- a/vkryl/core +++ b/vkryl/core @@ -1 +1 @@ -Subproject commit 9de4f01641652812a9c8ab45998b95c7aac135f7 +Subproject commit 2c06947759e986a9ad24d805ad19d39aa0a30023 diff --git a/vkryl/leveldb b/vkryl/leveldb index 0043bcaea2..97bda2b641 160000 --- a/vkryl/leveldb +++ b/vkryl/leveldb @@ -1 +1 @@ -Subproject commit 0043bcaea258da9e5bfdcc5b63f2ad76c355dabe +Subproject commit 97bda2b64138afe33fd4588e82de4e47fea108c4 diff --git a/vkryl/td b/vkryl/td index 568406398b..38402319ee 160000 --- a/vkryl/td +++ b/vkryl/td @@ -1 +1 @@ -Subproject commit 568406398b4befe8ab5e30a87890b9f2a82351de +Subproject commit 38402319ee40715b7d271ecda27feca42c4f75d3