diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java b/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java index a06052825..5025868cd 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java @@ -1345,41 +1345,21 @@ public class SharedConfig { } public static boolean isAppUpdateAvailable() { - if (pendingAppUpdate == null || pendingAppUpdate.document == null || !AndroidUtilities.isStandaloneApp()) { + if (pendingAppUpdate == null || pendingAppUpdate.document == null) { return false; } - int currentVersion; - try { - PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0); - currentVersion = pInfo.versionCode; - } catch (Exception e) { - FileLog.e(e); - currentVersion = BuildVars.BUILD_VERSION; - } - return pendingAppUpdateBuildVersion == currentVersion; + return pendingAppUpdateBuildVersion == BuildVars.BUILD_VERSION; } public static boolean setNewAppVersionAvailable(TLRPC.TL_help_appUpdate update) { - String updateVersionString = null; - int versionCode = 0; - try { - PackageInfo packageInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0); - versionCode = packageInfo.versionCode; - updateVersionString = packageInfo.versionName; - } catch (Exception e) { - FileLog.e(e); - } - if (versionCode == 0) { - versionCode = BuildVars.BUILD_VERSION; - } - if (updateVersionString == null) { - updateVersionString = BuildVars.BUILD_VERSION_STRING; - } - if (update.version == null || updateVersionString.compareTo(update.version) >= 0) { + if (update == null) { + pendingAppUpdate = null; + pendingAppUpdateBuildVersion = 0; + saveConfig(); return false; } pendingAppUpdate = update; - pendingAppUpdateBuildVersion = versionCode; + pendingAppUpdateBuildVersion = BuildConfig.VERSION_CODE; saveConfig(); return true; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/BlockingUpdateView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/BlockingUpdateView.java index 8f414e579..6a635ec83 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/BlockingUpdateView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/BlockingUpdateView.java @@ -233,7 +233,7 @@ public class BlockingUpdateView extends FrameLayout implements NotificationCente public static boolean checkApkInstallPermissions(final Context context) { if (Build.VERSION.SDK_INT >= 26 && !ApplicationLoader.applicationContext.getPackageManager().canRequestPackageInstalls()) { AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setTitle(LocaleController.getString("NekoX", R.string.NekoX)); builder.setMessage(LocaleController.getString("ApkRestricted", R.string.ApkRestricted)); builder.setPositiveButton(LocaleController.getString("PermissionOpenSettings", R.string.PermissionOpenSettings), (dialogInterface, i) -> { try { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/UpdateAppAlertDialog.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/UpdateAppAlertDialog.java index b1c2630ac..246e2ad2c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/UpdateAppAlertDialog.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/UpdateAppAlertDialog.java @@ -32,6 +32,7 @@ import org.telegram.messenger.SvgHelper; import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.ActionBar.Theme; +import tw.nekomimi.nekogram.parts.DialogTransKt; public class UpdateAppAlertDialog extends BottomSheet { @@ -252,7 +253,7 @@ public class UpdateAppAlertDialog extends BottomSheet { textView.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); textView.setSingleLine(true); textView.setEllipsize(TextUtils.TruncateAt.END); - textView.setText(LocaleController.getString("AppUpdate", R.string.AppUpdate)); + textView.setText(LocaleController.getString("AppUpdateNekoX", R.string.AppUpdateNekoX)); linearLayout.addView(textView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 23, 16, 23, 0)); TextView messageTextView = new TextView(getContext()); @@ -262,7 +263,16 @@ public class UpdateAppAlertDialog extends BottomSheet { messageTextView.setLinkTextColor(Theme.getColor(Theme.key_dialogTextLink)); messageTextView.setText(LocaleController.formatString("AppUpdateVersionAndSize", R.string.AppUpdateVersionAndSize, appUpdate.version, AndroidUtilities.formatFileSize(appUpdate.document.size))); messageTextView.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.TOP); - linearLayout.addView(messageTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 23, 0, 23, 5)); + linearLayout.addView(messageTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 23, 5, 23, 5)); + + TextView translationHintTextView = new TextView(getContext()); + translationHintTextView.setTextColor(Theme.getColor(Theme.key_dialogTextGray2)); + translationHintTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + translationHintTextView.setMovementMethod(new AndroidUtilities.LinkMovementMethodMy()); + translationHintTextView.setLinkTextColor(Theme.getColor(Theme.key_dialogTextLink)); + translationHintTextView.setText(LocaleController.getString("NekoXUpdateTranslationHint", R.string.NekoXUpdateTranslationHint)); + translationHintTextView.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.TOP); + linearLayout.addView(translationHintTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 23, 0, 23, 5)); TextView changelogTextView = new TextView(getContext()); changelogTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); @@ -277,6 +287,11 @@ public class UpdateAppAlertDialog extends BottomSheet { changelogTextView.setText(builder); } changelogTextView.setGravity(Gravity.LEFT | Gravity.TOP); + changelogTextView.setOnLongClickListener(v -> { + if (TextUtils.isEmpty(appUpdate.text)) return false; + DialogTransKt.startTrans(v.getContext(), appUpdate.text); + return true; + }); linearLayout.addView(changelogTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 23, 15, 23, 0)); FrameLayout.LayoutParams frameLayoutParams = new FrameLayout.LayoutParams(LayoutHelper.MATCH_PARENT, AndroidUtilities.getShadowHeight(), Gravity.BOTTOM | Gravity.LEFT); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java index 06c4ed650..a2cddff5e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java @@ -351,10 +351,10 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. private boolean showSetPasswordConfirm; private int otherwiseReloginDays; - private FrameLayout updateLayout; - private AnimatorSet updateLayoutAnimator; - private RadialProgress2 updateLayoutIcon; - private TextView updateTextView; +// private FrameLayout updateLayout; +// private AnimatorSet updateLayoutAnimator; +// private RadialProgress2 updateLayoutIcon; +// private TextView updateTextView; private DialogsActivityDelegate delegate; @@ -3415,77 +3415,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } if (searchString == null && initialDialogsType == 0) { - updateLayout = new FrameLayout(context) { - - private Paint paint = new Paint(); - private Matrix matrix = new Matrix(); - private LinearGradient updateGradient; - private int lastGradientWidth; - - @Override - protected void onDraw(Canvas canvas) { - if (updateGradient == null) { - return; - } - paint.setColor(0xffffffff); - paint.setShader(updateGradient); - updateGradient.setLocalMatrix(matrix); - canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint); - updateLayoutIcon.setBackgroundGradientDrawable(updateGradient); - updateLayoutIcon.draw(canvas); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int width = MeasureSpec.getSize(widthMeasureSpec); - if (lastGradientWidth != width) { - updateGradient = new LinearGradient(0, 0, width, 0, new int[]{0xff69BF72, 0xff53B3AD}, new float[]{0.0f, 1.0f}, Shader.TileMode.CLAMP); - lastGradientWidth = width; - } - int x = (getMeasuredWidth() - updateTextView.getMeasuredWidth()) / 2; - updateLayoutIcon.setProgressRect(x, AndroidUtilities.dp(13), x + AndroidUtilities.dp(22), AndroidUtilities.dp(13 + 22)); - } - - @Override - public void setTranslationY(float translationY) { - super.setTranslationY(translationY); - additionalFloatingTranslation2 = AndroidUtilities.dp(48) - translationY; - if (additionalFloatingTranslation2 < 0) { - additionalFloatingTranslation2 = 0; - } - if (!floatingHidden) { - updateFloatingButtonOffset(); - } - } - }; - updateLayout.setWillNotDraw(false); - updateLayout.setVisibility(View.INVISIBLE); - updateLayout.setTranslationY(AndroidUtilities.dp(48)); - if (Build.VERSION.SDK_INT >= 21) { - updateLayout.setBackground(Theme.getSelectorDrawable(Theme.getColor(Theme.key_listSelector), null)); - } - contentView.addView(updateLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.BOTTOM)); - updateLayout.setOnClickListener(v -> { - if (!SharedConfig.isAppUpdateAvailable()) { - return; - } - AndroidUtilities.openForView(SharedConfig.pendingAppUpdate.document, true, getParentActivity()); - }); - - updateLayoutIcon = new RadialProgress2(updateLayout); - updateLayoutIcon.setColors(0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff); - updateLayoutIcon.setCircleRadius(AndroidUtilities.dp(11)); - updateLayoutIcon.setAsMini(); - updateLayoutIcon.setIcon(MediaActionDrawable.ICON_UPDATE, true, false); - - updateTextView = new TextView(context); - updateTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); - updateTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - updateTextView.setText(LocaleController.getString("AppUpdateNow", R.string.AppUpdateNow).toUpperCase()); - updateTextView.setTextColor(0xffffffff); - updateTextView.setPadding(AndroidUtilities.dp(30), 0, 0, 0); - updateLayout.addView(updateTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER, 0, 0, 0, 0)); + // NekoX: Remove UPDATE NOW Bottom View in DialogsActivity } for (int a = 0; a < 2; a++) { @@ -3615,8 +3545,6 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. PrivacyUtil.postCheckAll(getParentActivity(), currentAccount); if (new Random().nextInt(100) < 20) UpdateUtil.postCheckFollowChannel(getParentActivity(), currentAccount); - if (!BuildVars.isFdroid && !BuildVars.isPlay && NekoXConfig.autoUpdateReleaseChannel != 0 && System.currentTimeMillis() / 1000 > NekoXConfig.nextUpdateCheck) - UIUtil.runOnIoDispatcher(() -> InternalUpdater.checkUpdate(getParentActivity(), true), 6000); if (NekoXConfig.developerMode && !NekoXConfig.isDeveloper()) NekoXConfig.toggleDeveloperMode(); @@ -3624,66 +3552,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } private void updateAppUpdateViews(boolean animated) { - if (updateLayout == null) { - return; - } - boolean show; - if (SharedConfig.isAppUpdateAvailable()) { - String fileName = FileLoader.getAttachFileName(SharedConfig.pendingAppUpdate.document); - File path = FileLoader.getPathToAttach(SharedConfig.pendingAppUpdate.document, true); - show = path.exists(); - } else { - show = false; - } - if (show) { - if (updateLayout.getTag() != null) { - return; - } - if (updateLayoutAnimator != null) { - updateLayoutAnimator.cancel(); - } - updateLayout.setVisibility(View.VISIBLE); - updateLayout.setTag(1); - if (animated) { - updateLayoutAnimator = new AnimatorSet(); - updateLayoutAnimator.setDuration(180); - updateLayoutAnimator.setInterpolator(CubicBezierInterpolator.EASE_OUT); - updateLayoutAnimator.playTogether(ObjectAnimator.ofFloat(updateLayout, View.TRANSLATION_Y, 0)); - updateLayoutAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - updateLayoutAnimator = null; - } - }); - updateLayoutAnimator.start(); - } else { - updateLayout.setTranslationY(0); - } - } else { - if (updateLayout.getTag() == null) { - return; - } - updateLayout.setTag(null); - if (animated) { - updateLayoutAnimator = new AnimatorSet(); - updateLayoutAnimator.setDuration(180); - updateLayoutAnimator.setInterpolator(CubicBezierInterpolator.EASE_OUT); - updateLayoutAnimator.playTogether(ObjectAnimator.ofFloat(updateLayout, View.TRANSLATION_Y, AndroidUtilities.dp(48))); - updateLayoutAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (updateLayout.getTag() == null) { - updateLayout.setVisibility(View.INVISIBLE); - } - updateLayoutAnimator = null; - } - }); - updateLayoutAnimator.start(); - } else { - updateLayout.setTranslationY(AndroidUtilities.dp(48)); - updateLayout.setVisibility(View.INVISIBLE); - } - } + // NekoX: Remove UPDATE NOW Bottom View in DialogsActivity } private void updateContextViewPosition() { @@ -6728,7 +6597,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. View databaseMigrationHint; private void updateMenuButton(boolean animated) { - if (menuDrawable == null || updateLayout == null) { + if (menuDrawable == null) { return; } int type; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java index 4fbf81aa1..cecfe0838 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java @@ -22,7 +22,11 @@ import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; import android.graphics.Point; +import android.graphics.Shader; import android.location.Location; import android.location.LocationManager; import android.media.AudioManager; @@ -36,7 +40,9 @@ import android.provider.ContactsContract; import android.provider.Settings; import android.text.TextUtils; import android.util.Base64; +import android.util.TypedValue; import android.view.ActionMode; +import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MotionEvent; @@ -113,11 +119,13 @@ import org.telegram.ui.Components.AudioPlayerAlert; import org.telegram.ui.Components.BlockingUpdateView; import org.telegram.ui.Components.Bulletin; import org.telegram.ui.Components.BulletinFactory; +import org.telegram.ui.Components.CubicBezierInterpolator; import org.telegram.ui.Components.Easings; import org.telegram.ui.Components.EmbedBottomSheet; import org.telegram.ui.Components.GroupCallPip; import org.telegram.ui.Components.JoinGroupAlert; import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.MediaActionDrawable; import org.telegram.ui.Components.PasscodeView; import org.telegram.ui.Components.PhonebookShareAlert; import org.telegram.ui.Components.PipRoundVideoView; @@ -133,6 +141,7 @@ import org.telegram.ui.Components.StickersAlert; import org.telegram.ui.Components.TermsOfServiceView; import org.telegram.ui.Components.ThemeEditorView; import org.telegram.ui.Components.UndoView; +import org.telegram.ui.Components.UpdateAppAlertDialog; import org.telegram.ui.Components.voip.VoIPHelper; import org.webrtc.voiceengine.WebRtcAudioTrack; @@ -155,6 +164,7 @@ import java.util.regex.Pattern; import cn.hutool.core.util.StrUtil; import kotlin.Unit; import kotlin.text.StringsKt; +import tw.nekomimi.nekogram.InternalUpdater; import tw.nekomimi.nekogram.ui.BottomBuilder; import tw.nekomimi.nekogram.ExternalGcm; import tw.nekomimi.nekogram.NekoConfig; @@ -2085,6 +2095,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } catch (Exception e) { FileLog.e(e); } + } else if (url.startsWith("tg:upgrade") || url.startsWith("tg://upgrade") || url.startsWith("tg:update") || url.startsWith("tg://update")) { + checkAppUpdate(true); } else if ((url.startsWith("tg:search") || url.startsWith("tg://search"))) { url = url.replace("tg:search", "tg://telegram.org").replace("tg://search", "tg://telegram.org"); data = Uri.parse(url); @@ -3652,6 +3664,201 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa return foundContacts; } + private void createUpdateUI() { + if (sideMenuContainer == null) { + return; + } + updateLayout = new FrameLayout(this) { + + private Paint paint = new Paint(); + private Matrix matrix = new Matrix(); + private LinearGradient updateGradient; + private int lastGradientWidth; + + @Override + protected void onDraw(Canvas canvas) { + if (updateGradient == null) { + return; + } + paint.setColor(0xffffffff); + paint.setShader(updateGradient); + updateGradient.setLocalMatrix(matrix); + canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint); + updateLayoutIcon.setBackgroundGradientDrawable(updateGradient); + updateLayoutIcon.draw(canvas); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + if (lastGradientWidth != width) { + updateGradient = new LinearGradient(0, 0, width, 0, new int[]{0xff69BF72, 0xff53B3AD}, new float[]{0.0f, 1.0f}, Shader.TileMode.CLAMP); + lastGradientWidth = width; + } + } + }; + updateLayout.setWillNotDraw(false); + updateLayout.setVisibility(View.INVISIBLE); + updateLayout.setTranslationY(AndroidUtilities.dp(44)); + if (Build.VERSION.SDK_INT >= 21) { + updateLayout.setBackground(Theme.getSelectorDrawable(Theme.getColor(Theme.key_listSelector), null)); + } + sideMenuContainer.addView(updateLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 44, Gravity.LEFT | Gravity.BOTTOM)); + updateLayout.setOnClickListener(v -> { + if (!SharedConfig.isAppUpdateAvailable()) { + return; + } + if (updateLayoutIcon.getIcon() == MediaActionDrawable.ICON_DOWNLOAD) { + FileLoader.getInstance(currentAccount).loadFile(SharedConfig.pendingAppUpdate.document, "update", 1, 1); + updateAppUpdateViews(true); + } else if (updateLayoutIcon.getIcon() == MediaActionDrawable.ICON_CANCEL) { + FileLoader.getInstance(currentAccount).cancelLoadFile(SharedConfig.pendingAppUpdate.document); + updateAppUpdateViews(true); + } else { + AndroidUtilities.openForView(SharedConfig.pendingAppUpdate.document, true, this); + } + }); + updateLayoutIcon = new RadialProgress2(updateLayout); + updateLayoutIcon.setColors(0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff); + updateLayoutIcon.setProgressRect(AndroidUtilities.dp(22), AndroidUtilities.dp(11), AndroidUtilities.dp(22 + 22), AndroidUtilities.dp(11 + 22)); + updateLayoutIcon.setCircleRadius(AndroidUtilities.dp(11)); + updateLayoutIcon.setAsMini(); + + updateTextView = new SimpleTextView(this); + updateTextView.setTextSize(15); + updateTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + updateTextView.setText(LocaleController.getString("AppUpdate", R.string.AppUpdate)); + updateTextView.setTextColor(0xffffffff); + updateTextView.setGravity(Gravity.LEFT); + updateLayout.addView(updateTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_VERTICAL, 74, 0, 0, 0)); + + updateSizeTextView = new TextView(this); + updateSizeTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + updateSizeTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + updateSizeTextView.setGravity(Gravity.RIGHT); + updateSizeTextView.setTextColor(0xffffffff); + updateLayout.addView(updateSizeTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, 0, 0, 17, 0)); + } + + private void updateAppUpdateViews(boolean animated) { + if (sideMenuContainer == null) { + return; + } + if (SharedConfig.isAppUpdateAvailable()) { + createUpdateUI(); + updateSizeTextView.setText(AndroidUtilities.formatFileSize(SharedConfig.pendingAppUpdate.document.size)); + String fileName = FileLoader.getAttachFileName(SharedConfig.pendingAppUpdate.document); + File path = FileLoader.getPathToAttach(SharedConfig.pendingAppUpdate.document, true); + boolean showSize; + if (path.exists()) { + updateLayoutIcon.setIcon(MediaActionDrawable.ICON_UPDATE, true, animated); + updateTextView.setText(LocaleController.getString("AppUpdateNow", R.string.AppUpdateNow)); + showSize = false; + } else { + if (FileLoader.getInstance(currentAccount).isLoadingFile(fileName)) { + updateLayoutIcon.setIcon(MediaActionDrawable.ICON_CANCEL, true, animated); + Float p = ImageLoader.getInstance().getFileProgress(fileName); + updateTextView.setText(LocaleController.formatString("AppUpdateDownloading", R.string.AppUpdateDownloading, (int) ((p != null ? p : 0.0f) * 100))); + showSize = false; + } else { + updateLayoutIcon.setIcon(MediaActionDrawable.ICON_DOWNLOAD, true, animated); + updateTextView.setText(LocaleController.getString("AppUpdate", R.string.AppUpdate)); + showSize = true; + } + } + if (showSize) { + if (updateSizeTextView.getTag() != null) { + if (animated) { + updateSizeTextView.setTag(null); + updateSizeTextView.animate().alpha(1.0f).scaleX(1.0f).scaleY(1.0f).setDuration(180).start(); + } else { + updateSizeTextView.setAlpha(1.0f); + updateSizeTextView.setScaleX(1.0f); + updateSizeTextView.setScaleY(1.0f); + } + } + } else { + if (updateSizeTextView.getTag() == null) { + if (animated) { + updateSizeTextView.setTag(1); + updateSizeTextView.animate().alpha(0.0f).scaleX(0.0f).scaleY(0.0f).setDuration(180).start(); + } else { + updateSizeTextView.setAlpha(0.0f); + updateSizeTextView.setScaleX(0.0f); + updateSizeTextView.setScaleY(0.0f); + } + } + } + if (updateLayout.getTag() != null) { + return; + } + updateLayout.setVisibility(View.VISIBLE); + updateLayout.setTag(1); + if (animated) { + updateLayout.animate().translationY(0).setInterpolator(CubicBezierInterpolator.EASE_OUT).setListener(null).setDuration(180).start(); + } else { + updateLayout.setTranslationY(0); + } + sideMenu.setPadding(0, 0, 0, AndroidUtilities.dp(44)); + } else { + if (updateLayout == null || updateLayout.getTag() == null) { + return; + } + updateLayout.setTag(null); + if (animated) { + updateLayout.animate().translationY(AndroidUtilities.dp(44)).setInterpolator(CubicBezierInterpolator.EASE_OUT).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (updateLayout.getTag() == null) { + updateLayout.setVisibility(View.INVISIBLE); + } + } + }).setDuration(180).start(); + } else { + updateLayout.setTranslationY(AndroidUtilities.dp(44)); + updateLayout.setVisibility(View.INVISIBLE); + } + sideMenu.setPadding(0, 0, 0, 0); + } + } + + public void checkAppUpdate(boolean force) { + if (BuildVars.isFdroid || BuildVars.isPlay) return; + if (NekoXConfig.autoUpdateReleaseChannel == 0) return; + if (!force && System.currentTimeMillis() < SharedConfig.lastUpdateCheckTime + 1000L * 60 * 60) return; + SharedConfig.lastUpdateCheckTime = System.currentTimeMillis(); + SharedConfig.saveConfig(); + FileLog.d("checking update"); + + final int accountNum = currentAccount; + InternalUpdater.checkUpdate((res, error) -> AndroidUtilities.runOnUIThread(() -> { + if (res != null) { + SharedConfig.setNewAppVersionAvailable(res); + if (res.can_not_skip) { + showUpdateActivity(accountNum, res, false); + } else { + drawerLayoutAdapter.notifyDataSetChanged(); + try { + (new UpdateAppAlertDialog(LaunchActivity.this, res, accountNum)).show(); + } catch (Exception e) { + FileLog.e(e); + } + } + } else { + if (force) { + if (error) + Toast.makeText(LaunchActivity.this, LocaleController.getString("ErrorOccurred", R.string.ErrorOccurred), Toast.LENGTH_SHORT).show(); + else + Toast.makeText(LaunchActivity.this, LocaleController.getString("VersionUpdateNoUpdate", R.string.VersionUpdateNoUpdate), Toast.LENGTH_SHORT).show(); + } + SharedConfig.setNewAppVersionAvailable(null); + drawerLayoutAdapter.notifyDataSetChanged(); + } + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.appUpdateAvailable); + })); + } + public AlertDialog showAlertDialog(AlertDialog.Builder builder) { try { if (visibleDialog != null) { @@ -4282,6 +4489,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } else if (SharedConfig.pendingAppUpdate != null && SharedConfig.pendingAppUpdate.can_not_skip) { showUpdateActivity(UserConfig.selectedAccount, SharedConfig.pendingAppUpdate, true); } + checkAppUpdate(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { ApplicationLoader.canDrawOverlays = Settings.canDrawOverlays(this); @@ -4597,6 +4805,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if (SharedConfig.isAppUpdateAvailable()) { String name = FileLoader.getAttachFileName(SharedConfig.pendingAppUpdate.document); if (name.equals(path)) { + updateAppUpdateViews(true); } } if (loadingThemeFileName != null) { @@ -4667,6 +4876,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if (SharedConfig.isAppUpdateAvailable()) { String name = FileLoader.getAttachFileName(SharedConfig.pendingAppUpdate.document); if (name.equals(path)) { + updateAppUpdateViews(true); } } } else if (id == NotificationCenter.screenStateChanged) { @@ -4741,6 +4951,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } } else if (id == NotificationCenter.appUpdateAvailable) { + updateAppUpdateViews(mainFragmentsStack.size() == 1); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java index dafb1a80a..d899ba6fb 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java @@ -3007,7 +3007,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (!BuildVars.isFdroid && !BuildVars.isPlay) { builder.addItem(LocaleController.getString("CheckUpdate", R.string.CheckUpdate), R.drawable.baseline_search_24, (it) -> { - UIUtil.runOnIoDispatcher(() -> InternalUpdater.checkUpdate(getParentActivity(), false)); + Browser.openUrl(context, "tg://update"); return Unit.INSTANCE; }); @@ -3022,6 +3022,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. case 2: currentChannel += LocaleController.getString("AutoCheckUpdateRc", R.string.AutoCheckUpdateRc); break; + case 3: + currentChannel += LocaleController.getString("AutoCheckUpdatePreview", R.string.AutoCheckUpdatePreview); + break; } builder.addItem(LocaleController.getString("AutoCheckUpdateSwitch", R.string.AutoCheckUpdateSwitch) + currentChannel, R.drawable.update_black_24, (it) -> { @@ -3042,6 +3045,11 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. switchBuilder.doRadioCheck(radioButtonCell); return Unit.INSTANCE; }); + switchBuilder.addRadioItem(LocaleController.getString("AutoCheckUpdatePreview", R.string.AutoCheckUpdatePreview), NekoXConfig.autoUpdateReleaseChannel == 3, (radioButtonCell) -> { + NekoXConfig.setAutoUpdateReleaseChannel(3); + switchBuilder.doRadioCheck(radioButtonCell); + return Unit.INSTANCE; + }); showDialog(switchBuilder.create()); return Unit.INSTANCE; }); diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/InternalUpdater.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/InternalUpdater.java index ed85f6905..50271959f 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/InternalUpdater.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/InternalUpdater.java @@ -1,186 +1,216 @@ package tw.nekomimi.nekogram; -import static org.telegram.ui.Components.BlockingUpdateView.checkApkInstallPermissions; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.widget.Toast; - -import androidx.core.content.FileProvider; - -import com.google.gson.Gson; - -import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.AccountInstance; import org.telegram.messenger.BuildConfig; import org.telegram.messenger.BuildVars; import org.telegram.messenger.FileLog; -import org.telegram.messenger.LocaleController; -import org.telegram.messenger.R; -import org.telegram.messenger.browser.Browser; -import org.telegram.ui.ActionBar.AlertDialog; +import org.telegram.messenger.UserConfig; +import org.telegram.tgnet.TLRPC; +import org.webrtc.EglBase; -import java.io.File; -import java.text.SimpleDateFormat; -import java.util.Locale; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; -import cn.hutool.http.HttpRequest; import tw.nekomimi.nekogram.utils.FileUtil; //TODO use UpdateAppAlertDialog / BlockingUpdateView? public class InternalUpdater { - private static final String API_URL_RELEASE = "https://api.github.com/repos/NekoX-Dev/NekoX/releases?per_page=10"; - private static class ReleaseMetadata { - String name; - String body; - String published_at; - String html_url; - ApkMetadata[] assets; - } + static final int UPDATE_METADATA_START_FROM = 0; + static final int MAX_READ_COUNT = 20; + static final long CHANNEL_METADATA_ID = 1359638116; + static final String CHANNEL_METADATA_NAME = "nekox_update_metadata"; + static final long CHANNEL_APKS_ID = 1137038259; + static final String CHANNEL_APKS_NAME = "NekoXApks"; - private static class ApkMetadata { - String name; - String browser_download_url; - } - - private static class GithubApiContents { - String content; - } - - // as a base64 encoded json - private static class NekoXReleaseNote { - NekoXAPK[] apks; - } - - private static class NekoXAPK { - String name; - String sha1; - String[] urls; // https://t.me/xxx or bdex://xxx, bdex removed - } - - private static ApkMetadata matchBuild(ApkMetadata[] apks) { - String target = BuildConfig.FLAVOR + "-" + FileUtil.getAbi() + "-" + BuildConfig.BUILD_TYPE + ".apk"; - FileLog.e(target); - for (ApkMetadata apk : apks) { - if (apk.name.contains(target)) - return apk; - } - return null; - } - - public static void checkUpdate(Context ctx, boolean isAutoCheck) { - if (BuildVars.isFdroid) - return; - - //cleanup - File f = new File(ApplicationLoader.getDataDirFixed(), "cache/new.apk"); - if (f.exists()) f.delete(); - - try { - NekoXConfig.setNextUpdateCheck(System.currentTimeMillis() / 1000 + 24 * 3600); - - //TODO update URL when api.github.com get banned. - String ret = HttpRequest.get(API_URL_RELEASE).header("accept", "application/vnd.github.v3+json").execute().body(); - ReleaseMetadata[] releases = new Gson().fromJson(ret, ReleaseMetadata[].class); - ReleaseMetadata release = null; - - // Not now. - String releaseChannel = "stable"; - switch (NekoXConfig.autoUpdateReleaseChannel) { - case 2: - releaseChannel = "rc"; - break; - case 3: - releaseChannel = "preview"; - break; + static void retrieveUpdateMetadata(retrieveUpdateMetadataCallback callback) { + final int localVersionCode = BuildVars.BUILD_VERSION; + AccountInstance accountInstance = AccountInstance.getInstance(UserConfig.selectedAccount); + TLRPC.TL_messages_getHistory req = new TLRPC.TL_messages_getHistory(); + req.peer = accountInstance.getMessagesController().getInputPeer(-CHANNEL_METADATA_ID); + req.offset_id = 0; + req.limit = MAX_READ_COUNT; + Runnable sendReq = () -> accountInstance.getConnectionsManager().sendRequest(req, (response, error) -> { + if (error != null) { + FileLog.e("Error when retrieving update metadata from channel " + error); + callback.apply(null, true); + return; } - - for (ReleaseMetadata rel : releases) { - if (rel.name.equals("v" + BuildConfig.VERSION_NAME)) - break; - if (rel.name.contains("rc") && NekoXConfig.autoUpdateReleaseChannel < 2 || rel.name.contains("preview") && NekoXConfig.autoUpdateReleaseChannel < 3) + TLRPC.messages_Messages res = (TLRPC.messages_Messages) response; + List metas = new ArrayList<>(); + for (TLRPC.Message message : res.messages) { + if (!(message instanceof TLRPC.TL_message)) continue; + if (!message.message.startsWith("v")) continue; + String[] split = message.message.split(","); + if (split.length < 4) continue; + UpdateMetadata metaData = new UpdateMetadata(message.id, split); + metas.add(metaData); + } + Collections.sort(metas, (o1, o2) -> o2.versionCode - o1.versionCode); // versionCode Desc + UpdateMetadata found = null; + for (UpdateMetadata metaData : metas) { + if (metaData.versionCode <= localVersionCode) break; + if (NekoXConfig.autoUpdateReleaseChannel < 3 && metaData.versionName.contains("preview")) continue; - release = rel; + if (NekoXConfig.autoUpdateReleaseChannel < 2 && metaData.versionName.contains("rc")) + continue; + found = metaData; break; } - if (release == null) { - FileLog.d("no update"); - if (!isAutoCheck) - AndroidUtilities.runOnUIThread(() -> Toast.makeText(ctx, LocaleController.getString("VersionUpdateNoUpdate", R.string.VersionUpdateNoUpdate), Toast.LENGTH_SHORT).show()); - return; - } else if (release.name.equals(NekoXConfig.ignoredUpdateTag) && isAutoCheck) { - FileLog.d("ignored tag " + release.name); + if (found != null) { + for (TLRPC.Message message : res.messages) { + if (!(message instanceof TLRPC.TL_message)) continue; + if (message.id == found.UpdateLogMessageID) { + found.updateLog = message.message; + found.updateLogEntities = message.entities; + break; + } + } + } + if (found == null) { + FileLog.d("Cannot find Update Metadata"); + callback.apply(null, false); return; } - - // match release apk urls - final ApkMetadata apk = matchBuild(release.assets); - - // match apk urls. these can be empty. - - ReleaseMetadata finalRelease = release; - - AndroidUtilities.runOnUIThread(() -> { - AlertDialog.Builder builder = new AlertDialog.Builder(ctx); - builder.setTitle(LocaleController.getString("VersionUpdateTitle", R.string.VersionUpdateTitle)); - - String message = null; - try { - message = finalRelease.name + " " + LocaleController.formatDateChat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).parse(finalRelease.published_at).getTime() / 1000) + "\n\n"; - } catch (Exception e) { - FileLog.e(e); + FileLog.w("Found Update Metadata " + found.versionName + " " + found.versionCode); + callback.apply(found, false); + }); + if (req.peer.access_hash != 0) sendReq.run(); + else { + TLRPC.TL_contacts_resolveUsername resolve = new TLRPC.TL_contacts_resolveUsername(); + resolve.username = CHANNEL_METADATA_NAME; + accountInstance.getConnectionsManager().sendRequest(resolve, (response1, error1) -> { + if (error1 != null) { + FileLog.e("Error when checking update, unable to resolve metadata channel " + error1.text); + callback.apply(null, true); + return; } - if (apk == null) - message += LocaleController.getString("VersionUpdateVariantNotMatch", R.string.VersionUpdateVariantNotMatch); - else - message += apk.name.replace(".apk", ""); - builder.setMessage(message); - - builder.setPositiveButton(LocaleController.getString("VersionUpdateConfirm", R.string.VersionUpdateConfirm), (dialog, which) -> { - Browser.openUrl(ctx, apk.browser_download_url); - }); - builder.setNeutralButton(LocaleController.getString("VersionUpdateIgnore", R.string.VersionUpdateIgnore), (dialog, which) -> NekoXConfig.setIgnoredUpdateTag(finalRelease.name)); - builder.setNegativeButton(LocaleController.getString("VersionUpdateNotNow", R.string.VersionUpdateNotNow), (dialog, which) -> NekoXConfig.setNextUpdateCheck(System.currentTimeMillis() / 1000 + 3 * 24 * 3600)); - builder.show(); + if (!(response1 instanceof TLRPC.TL_contacts_resolvedPeer)) { + FileLog.e("Error when checking update, unable to resolve metadata channel, unexpected responseType " + response1.getClass().getName()); + callback.apply(null, true); + return; + } + TLRPC.TL_contacts_resolvedPeer resolvedPeer = (TLRPC.TL_contacts_resolvedPeer) response1; + accountInstance.getMessagesController().putUsers(resolvedPeer.users, false); + accountInstance.getMessagesController().putChats(resolvedPeer.chats, false); + accountInstance.getMessagesStorage().putUsersAndChats(resolvedPeer.users, resolvedPeer.chats, false, true); + if ((resolvedPeer.chats == null || resolvedPeer.chats.size() == 0)) { + FileLog.e("Error when checking update, unable to resolve metadata channel, unexpected resolvedChat "); + callback.apply(null, true); + return; + } + req.peer = new TLRPC.TL_inputPeerChannel(); + req.peer.channel_id = resolvedPeer.chats.get(0).id; + req.peer.access_hash = resolvedPeer.chats.get(0).access_hash; + sendReq.run(); }); - } catch (Exception e) { - FileLog.e(e); - if (!isAutoCheck) - AndroidUtilities.runOnUIThread(() -> Toast.makeText(ctx, "An exception occurred during checking updates.", Toast.LENGTH_SHORT).show()); } } - public static boolean openApkInstall(Activity activity, File f) { - if (!checkApkInstallPermissions(activity)) { - return false; - } - - boolean exists = false; - try { - if (exists = f.exists()) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - - if (Build.VERSION.SDK_INT >= 24) { - intent.setDataAndType(FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", f), "application/vnd.android.package-archive"); - } else { - intent.setDataAndType(Uri.fromFile(f), "application/vnd.android.package-archive"); - } - try { - activity.startActivityForResult(intent, 500); - } catch (Exception e) { - FileLog.e(e); - } + public static void checkUpdate(checkUpdateCallback callback) { + AccountInstance accountInstance = AccountInstance.getInstance(UserConfig.selectedAccount); + retrieveUpdateMetadata((metadata, err) -> { + if (metadata == null) { + callback.apply(null, err); + return; } - } catch (Exception e) { - FileLog.e(e); + TLRPC.TL_messages_getHistory req = new TLRPC.TL_messages_getHistory(); + req.peer = accountInstance.getMessagesController().getInputPeer(-CHANNEL_APKS_ID); + req.offset_id = metadata.apkChannelMessageID; + req.limit = MAX_READ_COUNT; + + Runnable sendReq = () -> accountInstance.getConnectionsManager().sendRequest(req, (response, error) -> { + if (error != null) { + FileLog.e("Error when getting update document " + error.text); + callback.apply(null, true); + return; + } + TLRPC.messages_Messages res = (TLRPC.messages_Messages) response; + FileLog.d("Retrieve update messages, size:" + res.messages.size()); + final String target = BuildConfig.FLAVOR + "-" + FileUtil.getAbi() + "-" + ("debug".equals(BuildConfig.BUILD_TYPE) ? "release" : BuildConfig.BUILD_TYPE) + ".apk"; + for (int i = 0; i < res.messages.size(); i++) { + if (res.messages.get(i).media == null) continue; + + TLRPC.Document apkDocument = res.messages.get(i).media.document; + String fileName = apkDocument.attributes.size() == 0 ? "" : apkDocument.attributes.get(0).file_name; + if (!(fileName.contains(target) && fileName.contains(metadata.versionName) )) + continue; + TLRPC.TL_help_appUpdate update = new TLRPC.TL_help_appUpdate(); + update.version = metadata.versionName; + update.document = apkDocument; + update.can_not_skip = false; + update.flags |= 2; + if (metadata.updateLog != null) { + update.text = metadata.updateLog; + update.entities = metadata.updateLogEntities; + } + callback.apply(update, false); + return; + } + callback.apply(null, false); + }); + if (req.peer.access_hash != 0) sendReq.run(); + else { + TLRPC.TL_contacts_resolveUsername resolve = new TLRPC.TL_contacts_resolveUsername(); + resolve.username = CHANNEL_APKS_NAME; + accountInstance.getConnectionsManager().sendRequest(resolve, (response1, error1) -> { + if (error1 != null) { + FileLog.e("Error when checking update, unable to resolve metadata channel " + error1); + callback.apply(null, true); + return; + } + if (!(response1 instanceof TLRPC.TL_contacts_resolvedPeer)) { + FileLog.e("Error when checking update, unable to resolve metadata channel, unexpected responseType " + response1.getClass().getName()); + callback.apply(null, true); + return; + } + TLRPC.TL_contacts_resolvedPeer resolvedPeer = (TLRPC.TL_contacts_resolvedPeer) response1; + accountInstance.getMessagesController().putUsers(resolvedPeer.users, false); + accountInstance.getMessagesController().putChats(resolvedPeer.chats, false); + accountInstance.getMessagesStorage().putUsersAndChats(resolvedPeer.users, resolvedPeer.chats, false, true); + if ((resolvedPeer.chats == null || resolvedPeer.chats.size() == 0)) { + FileLog.e("Error when checking update, unable to resolve metadata channel, unexpected resolvedChat "); + callback.apply(null, true); + return; + } + req.peer = new TLRPC.TL_inputPeerChannel(); + req.peer.channel_id = resolvedPeer.chats.get(0).id; + req.peer.access_hash = resolvedPeer.chats.get(0).access_hash; + sendReq.run(); + }); + } + }); + + + } + + public interface retrieveUpdateMetadataCallback { + void apply(UpdateMetadata metadata, boolean error); + } + + public interface checkUpdateCallback { + void apply(TLRPC.TL_help_appUpdate resp, boolean error); + } + + static class UpdateMetadata { + int messageID; + String versionName; + int versionCode; + int apkChannelMessageID; + int UpdateLogMessageID; + String updateLog = null; + ArrayList updateLogEntities = null; + + UpdateMetadata(int messageID, String[] split) { + this.messageID = messageID; + versionName = split[0]; + versionCode = Integer.parseInt(split[1]); + apkChannelMessageID = Integer.parseInt(split[2]); + UpdateLogMessageID = Integer.parseInt(split[3]); } - return exists; } } \ No newline at end of file diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoXConfig.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoXConfig.java index eda7ffe3e..49eaa7fd1 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoXConfig.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoXConfig.java @@ -82,7 +82,7 @@ public class NekoXConfig { public static int autoUpdateReleaseChannel = preferences.getInt("autoUpdateReleaseChannel", 2); public static String ignoredUpdateTag = preferences.getString("ignoredUpdateTag", ""); - public static long nextUpdateCheck = preferences.getLong("nextUpdateCheckTimestamp", 0); +// public static long nextUpdateCheck = preferences.getLong("nextUpdateCheckTimestamp", 0); // public static int customApi = preferences.getInt("custom_api", 0); // public static int customAppId = preferences.getInt("custom_app_id", 0); @@ -135,9 +135,9 @@ public class NekoXConfig { return BuildConfig.APP_HASH; } - public static void setNextUpdateCheck(long timestamp) { - preferences.edit().putLong("nextUpdateCheckTimestamp", nextUpdateCheck = timestamp).apply(); - } +// public static void setNextUpdateCheck(long timestamp) { +// preferences.edit().putLong("nextUpdateCheckTimestamp", nextUpdateCheck = timestamp).apply(); +// } public static boolean isDeveloper() { if (hasDeveloper != null) diff --git a/TMessagesProj/src/main/res/values/strings_nekox.xml b/TMessagesProj/src/main/res/values/strings_nekox.xml index 7cd6a3988..d26d9bbf6 100644 --- a/TMessagesProj/src/main/res/values/strings_nekox.xml +++ b/TMessagesProj/src/main/res/values/strings_nekox.xml @@ -247,6 +247,7 @@ Automatic Update Stable Release Candidate + Preview (Unstable) Unread badge on back button Set IP of public proxy In some areas, specifying the IP of public proxy CDN(eg. Cloudflare) can speed up access. @@ -263,4 +264,6 @@ Remember all clicked replies Disable instant camera Show timestamp in seconds + Update Nekogram X + Press to translate the change log \ No newline at end of file