From 1e4c8bc29120cd649dc582b54f3021fc3e0c527a Mon Sep 17 00:00:00 2001 From: adbenitez Date: Tue, 27 Jan 2026 19:05:38 +0100 Subject: [PATCH 1/5] use event emmiter before DcAccounts creation --- jni/dc_wrapper.c | 40 +++++++++++++++++- .../java/com/b44t/messenger/DcAccounts.java | 6 +-- .../com/b44t/messenger/DcEventChannel.java | 27 ++++++++++++ .../securesms/ApplicationContext.java | 41 +++++++++++-------- .../securesms/connect/DcEventCenter.java | 32 ++++++++------- 5 files changed, 110 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/b44t/messenger/DcEventChannel.java diff --git a/jni/dc_wrapper.c b/jni/dc_wrapper.c index b51257bdb..eccca6f8a 100644 --- a/jni/dc_wrapper.c +++ b/jni/dc_wrapper.c @@ -153,6 +153,42 @@ static uint32_t* jintArray2uint32Pointer(JNIEnv* env, jintArray ja, int* ret_icn } +/************************************************************ + * DcEventChannel + ************************************************************/ + +static dc_event_channel_t* get_dc_event_channel(JNIEnv *env, jobject obj) +{ + static jfieldID fid = 0; + if (fid==0) { + jclass cls = (*env)->GetObjectClass(env, obj); + fid = (*env)->GetFieldID(env, cls, "eventChannelCPtr", "J" /*Signature, J=long*/); + } + if (fid) { + return (dc_event_channel_t*)(*env)->GetLongField(env, obj, fid); + } + return NULL; +} + + +JNIEXPORT jlong Java_com_b44t_messenger_DcEventChannel_createEventChannelCPtr(JNIEnv *env, jobject obj) +{ + return (jlong)dc_event_channel_new(); +} + + +JNIEXPORT void Java_com_b44t_messenger_DcEventChannel_unrefEventChannelCPtr(JNIEnv *env, jobject obj) +{ + dc_event_channel_unref(get_dc_event_channel(env, obj)); +} + + +JNIEXPORT jlong Java_com_b44t_messenger_DcEventChannel_getEventEmitterCPtr(JNIEnv *env, jobject obj) +{ + return (jlong)dc_event_channel_get_event_emitter(get_dc_event_channel(env, obj)); +} + + /******************************************************************************* * DcAccounts ******************************************************************************/ @@ -172,11 +208,11 @@ static dc_accounts_t* get_dc_accounts(JNIEnv *env, jobject obj) } -JNIEXPORT jlong Java_com_b44t_messenger_DcAccounts_createAccountsCPtr(JNIEnv *env, jobject obj, jstring dir) +JNIEXPORT jlong Java_com_b44t_messenger_DcAccounts_createAccountsCPtr(JNIEnv *env, jobject obj, jstring dir, jobject chanObj) { CHAR_REF(dir); int writable = 1; - jlong accountsCPtr = (jlong)dc_accounts_new(dirPtr, writable); + jlong accountsCPtr = (jlong)dc_accounts_new_with_event_channel(dirPtr, writable, get_dc_event_channel(env, chanObj)); CHAR_UNREF(dir); return accountsCPtr; } diff --git a/src/main/java/com/b44t/messenger/DcAccounts.java b/src/main/java/com/b44t/messenger/DcAccounts.java index 669048b12..8a08984d3 100644 --- a/src/main/java/com/b44t/messenger/DcAccounts.java +++ b/src/main/java/com/b44t/messenger/DcAccounts.java @@ -2,8 +2,8 @@ package com.b44t.messenger; public class DcAccounts { - public DcAccounts(String dir) { - accountsCPtr = createAccountsCPtr(dir); + public DcAccounts(String dir, DcEventChannel channel) { + accountsCPtr = createAccountsCPtr(dir, channel); if (accountsCPtr == 0) throw new RuntimeException("createAccountsCPtr() returned null pointer"); } @@ -38,7 +38,7 @@ public class DcAccounts { // working with raw c-data private long accountsCPtr; // CAVE: the name is referenced in the JNI - private native long createAccountsCPtr (String dir); + private native long createAccountsCPtr (String dir, DcEventChannel channel); private native void unrefAccountsCPtr (); private native long getEventEmitterCPtr (); private native long getJsonrpcInstanceCPtr (); diff --git a/src/main/java/com/b44t/messenger/DcEventChannel.java b/src/main/java/com/b44t/messenger/DcEventChannel.java new file mode 100644 index 000000000..2c3ece1dd --- /dev/null +++ b/src/main/java/com/b44t/messenger/DcEventChannel.java @@ -0,0 +1,27 @@ +package com.b44t.messenger; + +public class DcEventChannel { + + public DcEventChannel() { + eventChannelCPtr = createEventChannelCPtr(); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + if (eventChannelCPtr != 0) { + unrefEventChannelCPtr(); + eventChannelCPtr = 0; + } + } + + public DcEventEmitter getEventEmitter() { + return new DcEventEmitter(getEventEmitterCPtr()); + } + + // working with raw c-data + private long eventChannelCPtr; // CAVE: the name is referenced in the JNI + private native long createEventChannelCPtr (); + private native void unrefEventChannelCPtr (); + private native long getEventEmitterCPtr (); +} diff --git a/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 6476842a3..37ade8c06 100644 --- a/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -23,6 +23,7 @@ import androidx.work.WorkManager; import com.b44t.messenger.DcAccounts; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; +import com.b44t.messenger.DcEventChannel; import com.b44t.messenger.DcEventEmitter; import com.b44t.messenger.FFITransport; @@ -173,7 +174,29 @@ public class ApplicationContext extends MultiDexApplication { Util.runOnBackground(() -> { synchronized (initLock) { try { - dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath()); + DcEventChannel eventChannel = new DcEventChannel(); + DcEventEmitter emitter = eventChannel.getEventEmitter(); + eventCenter = new DcEventCenter(this); + + new Thread(() -> { + Log.i(TAG, "Starting event loop"); + while (true) { + DcEvent event = emitter.getNextEvent(); + if (event == null) { + break; + } + if (isInitialized) { + eventCenter.handleEvent(event); + } else { + // not fully initialized, only handle logging events, + // ex. account migrations during DcAccounts initialization + eventCenter.handleLogging(event); + } + } + Log.i("DeltaChat", "shutting down event handler"); + }, "eventThread").start(); + + dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath(), eventChannel); Log.i(TAG, "DcAccounts created"); rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance())); Log.i(TAG, "Rpc created"); @@ -213,28 +236,12 @@ public class ApplicationContext extends MultiDexApplication { } dcContext = dcAccounts.getSelectedAccount(); notificationCenter = new NotificationCenter(this); - eventCenter = new DcEventCenter(this); dcLocationManager = new DcLocationManager(this, dcContext); - // Mark as initialized before starting threads that depend on it isInitialized = true; initLock.notifyAll(); Log.i(TAG, "DcAccounts initialization complete"); - new Thread(() -> { - Log.i(TAG, "Starting event loop"); - DcEventEmitter emitter = dcAccounts.getEventEmitter(); - Log.i(TAG, "DcEventEmitter obtained"); - while (true) { - DcEvent event = emitter.getNextEvent(); - if (event==null) { - break; - } - eventCenter.handleEvent(event); - } - Log.i("DeltaChat", "shutting down event handler"); - }, "eventThread").start(); - // set translations before starting I/O to avoid sending untranslated MDNs (issue #2288) DcHelper.setStockTranslations(this); diff --git a/src/main/java/org/thoughtcrime/securesms/connect/DcEventCenter.java b/src/main/java/org/thoughtcrime/securesms/connect/DcEventCenter.java index 560f67888..f766a2e53 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/DcEventCenter.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/DcEventCenter.java @@ -164,6 +164,23 @@ public class DcEventCenter { }); } + public void handleLogging(@NonNull DcEvent event) { + final String logPrefix = "[accId="+event.getAccountId()+ "] "; + switch (event.getId()) { + case DcContext.DC_EVENT_INFO: + Log.i("DeltaChat", logPrefix + event.getData2Str()); + break; + + case DcContext.DC_EVENT_WARNING: + Log.w("DeltaChat", logPrefix + event.getData2Str()); + break; + + case DcContext.DC_EVENT_ERROR: + Log.e("DeltaChat", logPrefix + event.getData2Str()); + break; + } + } + public long handleEvent(@NonNull DcEvent event) { int accountId = event.getAccountId(); int id = event.getId(); @@ -205,20 +222,7 @@ public class DcEventCenter { return 0; } - final String logPrefix = "[accId="+accountId + "] "; - switch (id) { - case DcContext.DC_EVENT_INFO: - Log.i("DeltaChat", logPrefix + event.getData2Str()); - break; - - case DcContext.DC_EVENT_WARNING: - Log.w("DeltaChat", logPrefix + event.getData2Str()); - break; - - case DcContext.DC_EVENT_ERROR: - Log.e("DeltaChat", logPrefix + event.getData2Str()); - break; - } + handleLogging(event); if (accountId != context.getDcContext().getAccountId()) { return 0; From 18e145faafbc0f6b579a1f0388cb8d2f7236f1d3 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 29 Jan 2026 02:17:06 +0100 Subject: [PATCH 2/5] avoid custom constructor in AccountSelectionListFragment --- .../AccountSelectionListFragment.java | 35 ++++++++++++------- .../securesms/connect/AccountManager.java | 2 +- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java index 7e4e2adff..014ce8308 100644 --- a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java @@ -47,15 +47,20 @@ import chat.delta.rpc.RpcException; public class AccountSelectionListFragment extends DialogFragment implements DcEventCenter.DcEventDelegate { private static final String TAG = AccountSelectionListFragment.class.getSimpleName(); - private final ConversationListActivity activity; + private static final String ARG_SELECT_ONLY = "select_only"; private RecyclerView recyclerView; private AccountSelectionListAdapter adapter; - private final boolean selectOnly; - public AccountSelectionListFragment(ConversationListActivity activity, boolean selectOnly) { - super(); - this.activity = activity; - this.selectOnly = selectOnly; + public static AccountSelectionListFragment newInstance(boolean selectOnly) { + AccountSelectionListFragment fragment = new AccountSelectionListFragment(); + Bundle args = new Bundle(); + args.putBoolean(ARG_SELECT_ONLY, selectOnly); + fragment.setArguments(args); + return fragment; + } + + private boolean selectOnly() { + return getArguments() != null && getArguments().getBoolean(ARG_SELECT_ONLY, false); } @NonNull @@ -64,7 +69,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()) .setTitle(R.string.switch_account) .setNegativeButton(R.string.cancel, null); - if (!selectOnly) { + if (!selectOnly()) { builder.setNeutralButton(R.string.connectivity, ((dialog, which) -> { startActivity(new Intent(getActivity(), ConnectivityActivity.class)); })); @@ -99,23 +104,24 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv private void refreshData() { if (adapter == null) return; + boolean selOnly = selectOnly(); DcAccounts accounts = DcHelper.getAccounts(getActivity()); int[] accountIds = accounts.getAll(); - int[] ids = new int[(selectOnly? 0 : 1) + accountIds.length]; + int[] ids = new int[(selOnly? 0 : 1) + accountIds.length]; int j = 0; for (int accountId : accountIds) { ids[j++] = accountId; } - if (!selectOnly) ids[j] = DC_CONTACT_ID_ADD_ACCOUNT; + if (!selOnly) ids[j] = DC_CONTACT_ID_ADD_ACCOUNT; adapter.changeData(ids, accounts.getSelectedAccount().getAccountId()); } @Override public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - if (selectOnly) return; + if (selectOnly()) return; requireActivity().getMenuInflater().inflate(R.menu.account_item_context, menu); @@ -179,6 +185,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv } private void onSetTag(int accountId) { + ConversationListActivity activity = (ConversationListActivity)requireActivity(); AccountSelectionListFragment.this.dismiss(); DcContext dcContext = DcHelper.getAccounts(activity).getAccount(accountId); @@ -194,14 +201,15 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv .setPositiveButton(android.R.string.ok, (d, b) -> { String newTag = inputField.getText().toString().trim(); dcContext.setConfig(CONFIG_PRIVATE_TAG, newTag); - AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly); + AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly()); }) - .setNegativeButton(R.string.cancel, (d, b) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly)) + .setNegativeButton(R.string.cancel, (d, b) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly())) .show(); } private void onDeleteProfile(int accountId) { AccountSelectionListFragment.this.dismiss(); + ConversationListActivity activity = (ConversationListActivity)requireActivity(); DcAccounts accounts = DcHelper.getAccounts(activity); Rpc rpc = DcHelper.getRpc(activity); @@ -236,7 +244,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv AlertDialog dialog = new AlertDialog.Builder(activity) .setTitle(R.string.delete_account) .setView(dialogView) - .setNegativeButton(R.string.cancel, (d, which) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly)) + .setNegativeButton(R.string.cancel, (d, which) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly())) .setPositiveButton(R.string.delete, (d2, w2) -> activity.onDeleteProfile(accountId)) .show(); Util.redPositiveButton(dialog); @@ -254,6 +262,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv @Override public void onItemClick(AccountSelectionListItem contact) { AccountSelectionListFragment.this.dismiss(); + ConversationListActivity activity = (ConversationListActivity)requireActivity(); int accountId = contact.getAccountId(); if (accountId == DC_CONTACT_ID_ADD_ACCOUNT) { AccountManager.getInstance().switchAccountAndStartActivity(activity, 0); diff --git a/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java b/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java index 2bdebe7fe..de8901b52 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java @@ -138,7 +138,7 @@ public class AccountManager { // ui public void showSwitchAccountMenu(ConversationListActivity activity, boolean selectOnly) { - AccountSelectionListFragment dialog = new AccountSelectionListFragment(activity, selectOnly); + AccountSelectionListFragment dialog = AccountSelectionListFragment.newInstance(selectOnly); dialog.show(((FragmentActivity) activity).getSupportFragmentManager(), null); } From 1f9264225bbff41b548f5908246e582f4c376952 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 29 Jan 2026 03:30:17 +0100 Subject: [PATCH 3/5] remove custom constructor in ReactionsDetailsFragment --- .../securesms/ConversationFragment.java | 2 +- .../reactions/ReactionsDetailsFragment.java | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java index d28040736..30832749d 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java @@ -869,7 +869,7 @@ public class ConversationFragment extends MessageSelectorFragment @Override public void onReactionClicked(DcMsg messageRecord) { - ReactionsDetailsFragment dialog = new ReactionsDetailsFragment(messageRecord.getId()); + ReactionsDetailsFragment dialog = ReactionsDetailsFragment.newInstance(messageRecord.getId()); dialog.show(getActivity().getSupportFragmentManager(), null); } } diff --git a/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDetailsFragment.java b/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDetailsFragment.java index 91edc2e18..1288e9afc 100644 --- a/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDetailsFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDetailsFragment.java @@ -36,19 +36,24 @@ import chat.delta.rpc.types.Reactions; public class ReactionsDetailsFragment extends DialogFragment implements DcEventCenter.DcEventDelegate { private static final String TAG = ReactionsDetailsFragment.class.getSimpleName(); + private static final String ARG_MSG_ID = "msg_id"; private RecyclerView recyclerView; private ReactionRecipientsAdapter adapter; - private final int msgId; + private int msgId; - public ReactionsDetailsFragment(int msgId) { - super(); - this.msgId = msgId; + public static ReactionsDetailsFragment newInstance(int msgId) { + ReactionsDetailsFragment fragment = new ReactionsDetailsFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_MSG_ID, msgId); + fragment.setArguments(args); + return fragment; } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { + msgId = getArguments() != null? getArguments().getInt(ARG_MSG_ID, 0) : 0; adapter = new ReactionRecipientsAdapter(requireActivity(), GlideApp.with(requireActivity()), new ListClickListener()); LayoutInflater inflater = requireActivity().getLayoutInflater(); From 2e9aa79b02bb503b746b89da56fe4bc6b0377cc0 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 29 Jan 2026 03:44:42 +0100 Subject: [PATCH 4/5] simplify changes in AccountSelectionListFragment --- .../AccountSelectionListFragment.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java index 014ce8308..1506980cf 100644 --- a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java @@ -50,6 +50,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv private static final String ARG_SELECT_ONLY = "select_only"; private RecyclerView recyclerView; private AccountSelectionListAdapter adapter; + private boolean selectOnly; public static AccountSelectionListFragment newInstance(boolean selectOnly) { AccountSelectionListFragment fragment = new AccountSelectionListFragment(); @@ -59,17 +60,14 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv return fragment; } - private boolean selectOnly() { - return getArguments() != null && getArguments().getBoolean(ARG_SELECT_ONLY, false); - } - @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { + selectOnly = getArguments() != null && getArguments().getBoolean(ARG_SELECT_ONLY, false); AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()) .setTitle(R.string.switch_account) .setNegativeButton(R.string.cancel, null); - if (!selectOnly()) { + if (!selectOnly) { builder.setNeutralButton(R.string.connectivity, ((dialog, which) -> { startActivity(new Intent(getActivity(), ConnectivityActivity.class)); })); @@ -104,24 +102,23 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv private void refreshData() { if (adapter == null) return; - boolean selOnly = selectOnly(); DcAccounts accounts = DcHelper.getAccounts(getActivity()); int[] accountIds = accounts.getAll(); - int[] ids = new int[(selOnly? 0 : 1) + accountIds.length]; + int[] ids = new int[(selectOnly? 0 : 1) + accountIds.length]; int j = 0; for (int accountId : accountIds) { ids[j++] = accountId; } - if (!selOnly) ids[j] = DC_CONTACT_ID_ADD_ACCOUNT; + if (!selectOnly) ids[j] = DC_CONTACT_ID_ADD_ACCOUNT; adapter.changeData(ids, accounts.getSelectedAccount().getAccountId()); } @Override public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - if (selectOnly()) return; + if (selectOnly) return; requireActivity().getMenuInflater().inflate(R.menu.account_item_context, menu); @@ -201,9 +198,9 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv .setPositiveButton(android.R.string.ok, (d, b) -> { String newTag = inputField.getText().toString().trim(); dcContext.setConfig(CONFIG_PRIVATE_TAG, newTag); - AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly()); + AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly); }) - .setNegativeButton(R.string.cancel, (d, b) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly())) + .setNegativeButton(R.string.cancel, (d, b) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly)) .show(); } @@ -244,7 +241,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv AlertDialog dialog = new AlertDialog.Builder(activity) .setTitle(R.string.delete_account) .setView(dialogView) - .setNegativeButton(R.string.cancel, (d, which) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly())) + .setNegativeButton(R.string.cancel, (d, which) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly)) .setPositiveButton(R.string.delete, (d2, w2) -> activity.onDeleteProfile(accountId)) .show(); Util.redPositiveButton(dialog); From 8d5a55c24c35039bdd57dfab48321ed28d5126f8 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 29 Jan 2026 03:47:53 +0100 Subject: [PATCH 5/5] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e98854105..97aa1dc23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * Fix: Update menu when using "select all" in contact selection * Fix: Avoid empty profiles after using "add as second device" from welcome screen * Fix multi-device seen messages synchronization when using multiple relays +* Avoid crash when the app is minimized with profile switcher or reactions dialogs open * Update to core 2.39.0 ## v2.35.0