Compare commits

..

73 Commits

Author SHA1 Message Date
adbenitez 93427ba412 Merge remote-tracking branch 'upstream/wch423/audio-background-play' into adb/test-bg-audio 2026-02-06 19:04:39 +01:00
adbenitez eb997eca00 fix bug introduced by upstream merge 2026-02-06 18:35:30 +01:00
adb 30124de2a8 Merge branch 'main' into wch423/audio-background-play 2026-02-06 18:34:56 +01:00
B. Petersen 9ffc904ae5 more detailed call strings 2026-02-06 11:45:56 +01:00
wchen342 2704749b44 Merge pull request #4198 from deltachat/wch423/edge-to-edge-device-channel
Make device messages and subscribed channels edge to edge
2026-02-05 15:15:40 -05:00
wch423 1c174b5b70 Stop draft audio playback when (1) the conversation exits; or (2) the message is sent 2026-02-05 21:12:09 +01:00
adbenitez 8b5dd70d75 Merge remote-tracking branch 'upstream/main' 2026-02-05 20:25:35 +01:00
adb f022316ad7 Merge branch 'main' into wch423/audio-background-play 2026-02-05 19:44:50 +01:00
wch423 3386f5c5f7 Make !CanSend -> CanSend still trigger Inset changes 2026-02-05 19:07:43 +01:00
adb 57eead3a34 Merge branch 'main' into wch423/edge-to-edge-device-channel 2026-02-05 18:33:52 +01:00
adb 2868b51835 Merge pull request #4212 from deltachat/prep-2.40.0
prepare 2.40.0
2026-02-05 17:44:28 +01:00
wch423 82118db71b Make inset changes only happen on initialization of activity 2026-02-05 17:27:21 +01:00
adbenitez 14f55ca6b1 prepare 2.40.0 2026-02-05 17:25:20 +01:00
adb cebfa12142 Merge branch 'main' into wch423/edge-to-edge-device-channel 2026-02-05 16:55:30 +01:00
adb 96c8c21b78 Merge pull request #4209 from deltachat/adb/issue-4206
hide delete-contact icon in ContactSelectionListFragment
2026-02-05 16:55:07 +01:00
adb 9ab1b1f3a7 Merge branch 'main' into adb/issue-4206 2026-02-05 16:54:55 +01:00
adb 94a0e426f8 Merge pull request #4210 from deltachat/adb/allow-to-pick-call-mode
show menu to select audio/video call modes
2026-02-05 16:48:27 +01:00
adbenitez 006f8ae826 use hasVideo instead of audioOnly 2026-02-05 16:37:42 +01:00
wch423 2889266522 Force dispatch all inset changes immediately in ConversationFragment 2026-02-05 16:28:38 +01:00
adb 5e6fccf143 Merge branch 'main' into wch423/edge-to-edge-device-channel 2026-02-05 15:41:17 +01:00
adbenitez 3847e20d18 set tint for icons in the call submenu 2026-02-05 02:31:38 +01:00
adbenitez fc69212a51 update changelog 2026-02-05 01:41:29 +01:00
adbenitez 8999f54ba2 upgrade calls web app 2026-02-05 01:40:27 +01:00
adbenitez f470e92300 show different icon depending on voice/video call modes 2026-02-05 01:35:20 +01:00
adbenitez 3ac49e3e58 set the call mode when calling Rpc.placeOutgoingCall 2026-02-05 01:20:11 +01:00
adb 8dd9cfec5b Merge branch 'main' into adb/allow-to-pick-call-mode 2026-02-05 01:17:12 +01:00
adb 0588214ee7 Merge pull request #4211 from deltachat/update-core-and-stuff-2026-02-04
Update core to 2.40.0
2026-02-05 00:37:53 +01:00
adbenitez 4ec49a031c adapt to new Rpc.placeOutgoingCall API 2026-02-05 00:24:51 +01:00
adbenitez a31d7d6d3e update the RPC bindings 2026-02-04 23:39:04 +01:00
adbenitez 64bbe9866d update translations 2026-02-04 23:38:33 +01:00
adbenitez 5d7ab84efc update changelog 2026-02-04 23:35:14 +01:00
adbenitez 11f73a88e8 update deltachat-core-rust to 'chore(release): prepare for 2.40.0' of 'v2.40.0' 2026-02-04 23:31:30 +01:00
adbenitez dd0e847976 take into consideration if the call is audio/video call when accepting it 2026-02-04 23:17:42 +01:00
wch423 71ed333468 Stop playback when attachment preview is removed;
Stop playback when message containing audio is removed
2026-02-04 22:49:18 +01:00
adbenitez 41d94ae3ee if it is an audio-only call start with video disabled 2026-02-04 22:14:50 +01:00
wch423 b66bc1f863 Handle specific case when leaving group; also move floating button to avoid overaapping with system bar 2026-02-04 19:36:51 +01:00
adbenitez df7d80319c show menu to select audio/video call modes 2026-02-04 19:36:34 +01:00
adbenitez 134145d166 update changelog 2026-02-04 17:13:48 +01:00
adbenitez 688a103c10 hide delete-contact icon in ContactSelectionListFragment 2026-02-04 16:45:29 +01:00
adb e2efa1f913 Merge pull request #4201 from deltachat/adb/issue-4195
fix mailto handling and remove unused code
2026-02-03 23:27:48 +01:00
adb aebd5c66f7 Merge branch 'main' into wch423/audio-background-play 2026-02-03 21:51:02 +01:00
adbenitez 15c60c6b12 fix mailto handling 2026-02-03 20:57:47 +01:00
wch423 f319ba2b83 Allow list scrolling to extend to edge 2026-02-03 19:40:02 +01:00
wch423 93f12e7367 Make message list respect bottom bar height 2026-02-03 19:40:00 +01:00
wch423 10acb07f82 Make device messages and subscribed channels edge to edge 2026-02-03 19:39:56 +01:00
adb ee9a8dd53a Merge branch 'main' into adb/issue-4195 2026-02-03 18:22:39 +01:00
adb 0a55023bdb Merge pull request #4205 from deltachat/adb/issue-4202
remove deselected members
2026-02-03 18:22:02 +01:00
adb fec9f8b4d3 Merge pull request #4204 from deltachat/adb/issue-4203
Fix layout problems inside in-chat apps
2026-02-03 17:53:22 +01:00
wch423 13374df709 Bug fixes and minor changes 2026-02-03 17:41:31 +01:00
adbenitez 7fa04dc3c0 update changelog 2026-02-03 01:03:05 +01:00
adbenitez 9d4e0e4e21 remove deselected members 2026-02-03 00:40:00 +01:00
adbenitez 473c28ab07 Fix layout problems inside in-chat apps 2026-02-02 20:31:42 +01:00
adbenitez de47feac40 update changelog 2026-01-30 20:21:48 +01:00
adbenitez 84894ff538 fix mailto handling and remove unused code 2026-01-30 20:17:04 +01:00
adb f254c35749 Merge pull request #4199 from deltachat/r10s/improve-details-strings
message info: title-case the send/received titles
2026-01-30 17:48:37 +01:00
adb 1ff4e069ea Merge branch 'main' into wch423/audio-background-play 2026-01-29 22:17:24 +01:00
B. Petersen d574d33596 title-case the message-info send/received titles
we nowhere in the app use lower case field names
2026-01-29 18:33:57 +01:00
wchen342 f6756fc34b Merge pull request #4191 from deltachat/adb/issue-4157
use event emmiter before DcAccounts creation
2026-01-29 10:34:57 -05:00
wchen342 7c27eb47fc Merge pull request #4196 from deltachat/adb/issue-4194
avoid crash caused by custom Fragment constructors
2026-01-29 10:19:15 -05:00
adbenitez 8d5a55c24c update changelog 2026-01-29 03:47:53 +01:00
adbenitez 2e9aa79b02 simplify changes in AccountSelectionListFragment 2026-01-29 03:44:42 +01:00
adbenitez 1f9264225b remove custom constructor in ReactionsDetailsFragment 2026-01-29 03:30:17 +01:00
adbenitez 18e145faaf avoid custom constructor in AccountSelectionListFragment 2026-01-29 02:17:06 +01:00
adbenitez 47cf70120a add logcat to crash report 2026-01-28 23:21:37 +01:00
wch423 34be7aab17 Cleanup imports 2026-01-28 21:30:51 +01:00
wch423 59139ed242 Add support for attachment draft; Distinguish between different messages; Prevent unrelated activities from changing the pending intent 2026-01-28 21:28:43 +01:00
wch423 11f3964bdc Add support for attachment draft; Distinguish between different messages 2026-01-28 21:28:42 +01:00
wch423 7683408d18 Allow going back to previous activity from the notification; Support AllMediaActivity 2026-01-28 21:28:40 +01:00
wch423 b74e793654 Finish ViewModel for audio playback; Refine UI set up for AudioView 2026-01-28 21:28:38 +01:00
wch423 4bd74324d2 Try to decouple view and business logics 2026-01-28 21:28:36 +01:00
wch423 9a121b3039 Fix UI update problems;
Make notification clickable
2026-01-28 21:28:33 +01:00
wch423 a9832c9c53 Make audio view use Media3 for playback 2026-01-28 21:28:31 +01:00
adbenitez 1e4c8bc291 use event emmiter before DcAccounts creation 2026-01-27 19:05:38 +01:00
67 changed files with 1735 additions and 1094 deletions
+10 -2
View File
@@ -1,6 +1,7 @@
# Delta Chat Android Changelog
## Unreleased
## v2.40.0
2026-02
* Improve switch speed when changing profiles
* Allow to switch profile when sharing or forwarding
@@ -12,13 +13,20 @@
* Mark external links with " ↗" to make them clear
* Make QR code larger on "Add Second Device" screen
* Add indication for blocked contacts in user profile
* Allow to start calls with video disabled
* Show hint for empty contact search results
* Add background playing for voice messages and other audio files
* Fix: Show dialog if pasted QR codes are invalid
* Fix: Refresh chat list when returning from conversation if selected profile changed
* Fix: Update menu when using "select all" in contact selection
* Fix: Avoid empty profiles after using "add as second device" from welcome screen
* Fix: Remove from group deselected members in the contact selection list
* Fix multi-device seen messages synchronization when using multiple relays
* Update to core 2.39.0
* Fix mailto handling
* Fix layout problems inside in-chat apps
* Avoid crash when the app is minimized with profile switcher or reactions dialogs open
* Remove "trash icon" option from contact selection list when adding members to group
* Update to core 2.40.0
## v2.35.0
2026-01
+11 -3
View File
@@ -33,8 +33,8 @@ android {
useLibrary 'org.apache.http.legacy'
defaultConfig {
versionCode 30000736
versionName "2.36.0"
versionCode 30000737
versionName "2.40.0"
applicationId "chat.delta.lite"
multiDexEnabled true
@@ -68,6 +68,7 @@ android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}
packagingOptions {
jniLibs {
@@ -211,6 +212,10 @@ dependencies {
implementation "io.noties.markwon:inline-parser:$markwon_version"
implementation 'com.airbnb.android:lottie:4.2.2' // Lottie animations support.
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
def media3_version = "1.8.0" // 1.9.0 need minSdkVersion 23
implementation 'androidx.concurrent:concurrent-futures:1.3.0'
implementation 'androidx.sharetarget:sharetarget:1.2.0'
implementation 'androidx.webkit:webkit:1.14.0'
@@ -231,8 +236,11 @@ dependencies {
implementation 'androidx.work:work-runtime:2.9.1'
implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0'
implementation 'com.google.guava:guava:31.1-android'
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // plays video and audio
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // FIXME: exoplayer dependencies kept for Video, but we shall migrate them at some point
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
implementation "androidx.media3:media3-exoplayer:$media3_version"
implementation "androidx.media3:media3-session:$media3_version"
implementation "androidx.media3:media3-ui:$media3_version"
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'com.google.zxing:core:3.3.0' // fixed version to support SDK<24
implementation ('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } // QR Code scanner
+38 -2
View File
@@ -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;
}
+11
View File
@@ -38,6 +38,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- force compiling emojipicker on sdk<21; runtime checks are required then -->
<uses-sdk tools:overrideLibrary="androidx.emoji2.emojipicker"/>
@@ -92,6 +93,7 @@
<intent-filter>
<data android:scheme="mailto"/>
<action android:name="android.intent.action.SENDTO"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>
@@ -390,6 +392,15 @@
android:name=".service.FetchForegroundService"
android:foregroundServiceType="dataSync" />
<service
android:name=".service.AudioPlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
<receiver android:name=".notifications.MarkReadReceiver"
android:enabled="true"
android:exported="false">
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1315,8 +1315,8 @@ public class Rpc {
}
/** Starts an outgoing call. */
public Integer placeOutgoingCall(Integer accountId, Integer chatId, String placeCallInfo) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "place_outgoing_call", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(placeCallInfo));
public Integer placeOutgoingCall(Integer accountId, Integer chatId, String placeCallInfo, Boolean hasVideo) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "place_outgoing_call", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(placeCallInfo), mapper.valueToTree(hasVideo));
}
/** Accepts an incoming call. */
@@ -2,7 +2,7 @@
package chat.delta.rpc.types;
public class CallInfo {
/** True if SDP offer has a video. */
/** True if the call is started as a video call. */
public Boolean hasVideo;
/**
* SDP offer.
@@ -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");
}
@@ -46,7 +46,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 ();
@@ -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 ();
}
@@ -1,16 +1,24 @@
package org.thoughtcrime.securesms;
import android.content.ComponentName;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import androidx.viewpager.widget.ViewPager;
import com.b44t.messenger.DcChat;
@@ -18,11 +26,13 @@ import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.google.android.material.tabs.TabLayout;
import com.google.common.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.service.AudioPlaybackService;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
@@ -30,6 +40,7 @@ import java.util.ArrayList;
public class AllMediaActivity extends PassphraseRequiredActionBarActivity
implements DcEventCenter.DcEventDelegate
{
private static final String TAG = AllMediaActivity.class.getSimpleName();
public static final String CHAT_ID_EXTRA = "chat_id";
public static final String CONTACT_ID_EXTRA = "contact_id";
@@ -57,6 +68,10 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
private TabLayout tabLayout;
private ViewPager viewPager;
private @Nullable MediaController mediaController;
private ListenableFuture<MediaController> mediaControllerFuture;
private AudioPlaybackViewModel playbackViewModel;
@Override
protected void onPreCreate() {
dynamicTheme = new DynamicNoActionBarTheme();
@@ -91,11 +106,19 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
DcEventCenter eventCenter = DcHelper.getEventCenter(this);
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
initializeMediaController();
}
@Override
public void onDestroy() {
DcHelper.getEventCenter(this).removeObservers(this);
if (mediaController != null) {
MediaController.releaseFuture(mediaControllerFuture);
mediaController = null;
playbackViewModel.setMediaController(null);
}
super.onDestroy();
}
@@ -124,6 +147,40 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
this.tabLayout = ViewUtil.findById(this, R.id.tab_layout);
}
private void initializeMediaController() {
SessionToken sessionToken = new SessionToken(this,
new ComponentName(this, AudioPlaybackService.class));
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
.buildAsync();
mediaControllerFuture.addListener(() -> {
try {
mediaController = mediaControllerFuture.get();
addActivityContext(
this.getIntent().getExtras(),
this.getClass().getName()
);
playbackViewModel.setMediaController(mediaController);
} catch (Exception e) {
Log.e(TAG, "Error connecting to audio playback service", e);
}
}, ContextCompat.getMainExecutor(this));
}
private void addActivityContext(Bundle extras, String activityClassName) {
if (mediaController == null) return;
Bundle commandArgs = new Bundle();
commandArgs.putString("activity_class", activityClassName);
if (extras != null) {
commandArgs.putAll(extras);
}
SessionCommand updateContextCommand =
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
}
private boolean isGlobalGallery() {
return contactId==0 && chatId==0;
}
@@ -11,9 +11,10 @@ import androidx.annotation.NonNull;
import com.b44t.messenger.DcMsg;
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DocumentSlide;
@@ -31,7 +32,8 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
private final ItemClickListener itemClickListener;
private final Set<DcMsg> selected;
private BucketedThreadMedia media;
private BucketedThreadMedia media;
private AudioPlaybackViewModel playbackViewModel;
private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
private final DocumentView documentView;
@@ -71,6 +73,10 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
this.media = media;
}
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
this.playbackViewModel = playbackViewModel;
}
@Override
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false));
@@ -97,6 +103,7 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
viewHolder.webxdcView.setVisibility(View.GONE);
viewHolder.audioView.setVisibility(View.VISIBLE);
viewHolder.audioView.setPlaybackViewModel(playbackViewModel);
viewHolder.audioView.setAudio((AudioSlide)slide, dcMsg.getDuration());
viewHolder.audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
viewHolder.audioView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
@@ -16,6 +16,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.RecyclerView;
@@ -25,6 +26,7 @@ import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
@@ -72,9 +74,11 @@ public class AllMediaDocumentsFragment
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(recyclerView, true, false, true, true);
this.recyclerView.setAdapter(new AllMediaDocumentsAdapter(getContext(),
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
this));
AllMediaDocumentsAdapter adapter = new AllMediaDocumentsAdapter(getContext(),
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
this);
this.recyclerView.setAdapter(adapter);
adapter.setPlaybackViewModel(new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class));
this.recyclerView.setLayoutManager(gridManager);
this.recyclerView.setHasFixedSize(true);
@@ -239,12 +243,13 @@ public class AllMediaDocumentsFragment
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
int itemId = menuItem.getItemId();
AudioPlaybackViewModel playbackViewModel = new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
if (itemId == R.id.details) {
handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedMedia()));
mode.finish();
return true;
} else if (itemId == R.id.delete) {
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia());
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
mode.finish();
return true;
} else if (itemId == R.id.share) {
@@ -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;
@@ -164,7 +165,8 @@ public class ApplicationContext extends MultiDexApplication {
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter, true));
String errorMsg = "Android " + Build.VERSION.RELEASE +":\n" + stringWriter.getBuffer().toString();
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + " Crash Report";
errorMsg += "\n" + LogViewFragment.grabLogcat();
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR + " Crash Report";
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
@@ -195,7 +197,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");
@@ -235,28 +259,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);
@@ -50,11 +50,11 @@ public abstract class BaseConversationItem extends LinearLayout
this.rpc = DcHelper.getRpc(context);
}
protected void bind(@NonNull DcMsg messageRecord,
@NonNull DcChat dcChat,
@NonNull Set<DcMsg> batchSelected,
boolean pulseHighlight,
@NonNull Recipient conversationRecipient)
protected void bindPartial(@NonNull DcMsg messageRecord,
@NonNull DcChat dcChat,
@NonNull Set<DcMsg> batchSelected,
boolean pulseHighlight,
@NonNull Recipient conversationRecipient)
{
this.messageRecord = messageRecord;
this.dcChat = dcChat;
@@ -126,6 +126,8 @@ public abstract class BaseConversationItem extends LinearLayout
public void onClick(View v) {
if (!shouldInterceptClicks(messageRecord) && parent != null) {
// The click workaround on ConversationItem shall be revised.
// In fact, it is probably better rethinking accessibility approach for the items.
if (batchSelected.isEmpty() && Util.isTouchExplorationEnabled(context)) {
BaseConversationItem.this.onAccessibilityClick();
}
@@ -6,6 +6,8 @@ import androidx.annotation.Nullable;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -17,7 +19,9 @@ public interface BindableConversationItem extends Unbindable {
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient recipients,
boolean pulseHighlight);
boolean pulseHighlight,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener);
DcMsg getMessageRecord();
@@ -23,9 +23,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Activity container for selecting a list of contacts.
@@ -36,9 +34,7 @@ import java.util.Set;
public class ContactMultiSelectionActivity extends ContactSelectionActivity {
public static final String CONTACTS_EXTRA = "contacts_extra";
public static final String REMOVED_CONTACTS_EXTRA = "removed_contacts_extra";
private ArrayList<Integer> preselectedContacts;
public static final String DESELECTED_CONTACTS_EXTRA = "deselected_contacts_extra";
@Override
protected void onCreate(Bundle icicle, boolean ready) {
@@ -49,9 +45,6 @@ public class ContactMultiSelectionActivity extends ContactSelectionActivity {
// it's a bit confusing having one "X" button on the left and one on the right -
// and the "clear search" button is not that important.
getToolbar().setUseClearButton(false);
// Store preselected contacts to track which ones were removed
preselectedContacts = getIntent().getIntegerArrayListExtra(ContactSelectionListFragment.PRESELECTED_CONTACTS);
}
@Override
@@ -79,20 +72,9 @@ public class ContactMultiSelectionActivity extends ContactSelectionActivity {
private void saveSelection() {
Intent resultIntent = getIntent();
List<Integer> selectedContacts = contactsFragment.getSelectedContacts();
List<Integer> deselectedContacts = contactsFragment.getDeselectedContacts();
resultIntent.putIntegerArrayListExtra(CONTACTS_EXTRA, new ArrayList<>(selectedContacts));
// Calculate which contacts were removed (preselected but not in final selection)
if (preselectedContacts != null) {
Set<Integer> selectedSet = new HashSet<>(selectedContacts);
ArrayList<Integer> removedContacts = new ArrayList<>();
for (Integer preselectedId : preselectedContacts) {
if (!selectedSet.contains(preselectedId)) {
removedContacts.add(preselectedId);
}
}
resultIntent.putIntegerArrayListExtra(REMOVED_CONTACTS_EXTRA, removedContacts);
}
resultIntent.putIntegerArrayListExtra(DESELECTED_CONTACTS_EXTRA, new ArrayList<>(deselectedContacts));
setResult(RESULT_OK, resultIntent);
}
}
@@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -83,6 +84,7 @@ public class ContactSelectionListFragment extends Fragment
private DcContext dcContext;
private Set<Integer> selectedContacts;
private Set<Integer> deselectedContacts;
private OnContactSelectedListener onContactSelectedListener;
private String cursorFilter;
private RecyclerView recyclerView;
@@ -127,6 +129,7 @@ public class ContactSelectionListFragment extends Fragment
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.contact_list, menu);
menu.findItem(R.id.menu_delete_selected).setVisible(!isMulti());
updateActionModeState(actionMode);
return true;
}
@@ -226,6 +229,15 @@ public class ContactSelectionListFragment extends Fragment
return selected;
}
public @NonNull List<Integer> getDeselectedContacts() {
List<Integer> deselected = new LinkedList<>();
if (deselectedContacts != null) {
deselected.addAll(deselectedContacts);
}
return deselected;
}
private boolean isMulti() {
return getActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
}
@@ -241,6 +253,7 @@ public class ContactSelectionListFragment extends Fragment
isMulti(),
true);
selectedContacts = adapter.getSelectedContacts();
deselectedContacts = new HashSet<>();
ArrayList<Integer> preselectedContacts = getActivity().getIntent().getIntegerArrayListExtra(PRESELECTED_CONTACTS);
if(preselectedContacts!=null) {
selectedContacts.addAll(preselectedContacts);
@@ -309,12 +322,14 @@ public class ContactSelectionListFragment extends Fragment
}
selectedContacts.add(contactId);
deselectedContacts.remove(contactId);
contact.setChecked(true);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactSelected(contactId);
}
} else {
selectedContacts.remove(contactId);
deselectedContacts.add(contactId);
contact.setChecked(false);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(contactId);
@@ -355,6 +370,7 @@ public class ContactSelectionListFragment extends Fragment
int contactId = data.getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0);
if (contactId != 0) {
selectedContacts.add(contactId);
deselectedContacts.remove(contactId);
}
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
}
@@ -25,6 +25,7 @@ import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
@@ -65,7 +66,12 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.view.WindowCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
@@ -76,7 +82,7 @@ import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.calls.CallUtil;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
import org.thoughtcrime.securesms.components.ComposeText;
@@ -86,6 +92,8 @@ import org.thoughtcrime.securesms.components.InputPanel;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
import org.thoughtcrime.securesms.components.ScaleStableImageView;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
@@ -104,19 +112,19 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.service.AudioPlaybackService;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.guava.Optional;
import org.thoughtcrime.securesms.util.views.ProgressDialog;
import org.thoughtcrime.securesms.video.recode.VideoRecoder;
import org.thoughtcrime.securesms.calls.CallUtil;
import java.io.File;
import java.util.ArrayList;
@@ -138,14 +146,13 @@ import chat.delta.util.SettableFuture;
*/
@SuppressLint("StaticFieldLeak")
public class ConversationActivity extends PassphraseRequiredActionBarActivity
implements ConversationFragment.ConversationFragmentListener,
AttachmentManager.AttachmentListener,
SearchView.OnQueryTextListener,
DcEventCenter.DcEventDelegate,
OnKeyboardShownListener,
InputPanel.Listener,
InputPanel.MediaListener
{
implements ConversationFragment.ConversationFragmentListener,
AttachmentManager.AttachmentListener,
SearchView.OnQueryTextListener,
DcEventCenter.DcEventDelegate,
OnKeyboardShownListener,
InputPanel.Listener,
InputPanel.MediaListener, AudioView.OnActionListener {
private static final String TAG = ConversationActivity.class.getSimpleName();
public static final String ACCOUNT_ID_EXTRA = "account_id";
@@ -185,6 +192,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private MediaKeyboard emojiPicker;
protected HidingLinearLayout quickAttachmentToggle;
private InputPanel inputPanel;
private @Nullable MediaController mediaController;
private com.google.common.util.concurrent.ListenableFuture<MediaController> mediaControllerFuture;
private AudioPlaybackViewModel playbackViewModel;
private ApplicationContext context;
private Recipient recipient;
@@ -217,6 +227,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeActionBar();
initializeViews();
initializeResources();
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
initializeMediaController();
initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
@@ -267,6 +281,36 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void initializeMediaController() {
SessionToken sessionToken = new SessionToken(this,
new ComponentName(this, AudioPlaybackService.class));
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
.buildAsync();
mediaControllerFuture.addListener(() -> {
try {
mediaController = mediaControllerFuture.get();
playbackViewModel.setMediaController(mediaController);
} catch (Exception e) {
Log.e(TAG, "Error connecting to audio playback service", e);
}
}, ContextCompat.getMainExecutor(this));
}
private void addActivityContext(Bundle extras, String activityClassName) {
if (mediaController == null) return;
Bundle commandArgs = new Bundle();
commandArgs.putString("activity_class", activityClassName);
if (extras != null) {
commandArgs.putAll(extras);
}
SessionCommand updateContextCommand =
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
@@ -337,7 +381,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
DcHelper.getNotificationCenter(this).clearVisibleChat();
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
inputPanel.onPause();
AudioSlidePlayer.stopAll();
}
@Override
@@ -357,6 +400,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
protected void onDestroy() {
DcHelper.getEventCenter(this).removeObservers(this);
if (mediaController != null) {
MediaController.releaseFuture(mediaControllerFuture);
mediaController = null;
playbackViewModel.setMediaController(null);
}
super.onDestroy();
}
@@ -569,8 +617,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else if (itemId == R.id.menu_show_map) {
WebxdcActivity.openMaps(this, chatId);
return true;
} else if (itemId == R.id.menu_start_call) {
CallUtil.startCall(this, chatId);
} else if (itemId == R.id.menu_start_audio_call) {
CallUtil.startCall(this, chatId, false);
return true;
} else if (itemId == R.id.menu_start_video_call) {
CallUtil.startCall(this, chatId, true);
return true;
} else if (itemId == R.id.menu_all_media) {
handleAllMedia();
@@ -631,6 +682,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
extras.putInt(ConversationListFragment.RELOAD_LIST, 1);
}
playbackViewModel.stopNonMessageAudioPlayback();
boolean archived = getIntent().getBooleanExtra(FROM_ARCHIVED_CHATS_EXTRA, false);
Intent intent = new Intent(this, (archived ? ConversationListArchiveActivity.class : ConversationListActivity.class));
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -1004,20 +1057,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
recipient = new Recipient(this, dcChat);
glideRequests = GlideApp.with(this);
setComposePanelVisibility();
setComposePanelVisibility(true);
initializeContactRequest();
}
private void setComposePanelVisibility() {
private void setComposePanelVisibility(boolean isInitialization) {
if (dcChat.canSend()) {
composePanel.setVisibility(View.VISIBLE);
attachmentManager.setHidden(false);
inputPanel.setSubjectVisible(!dcChat.isEncrypted());
ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
fragment.handleRemoveBottomInsets();
} else {
composePanel.setVisibility(View.GONE);
attachmentManager.setHidden(true);
hideSoftKeyboard();
inputPanel.setSubjectVisible(false);
if (isInitialization) {
ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, false);
fragment.handleAddBottomInsets();
}
}
}
@@ -1054,11 +1113,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return new SettableFuture<>(false);
}
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId);
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId, playbackViewModel);
}
private ListenableFuture<Boolean> setMedia(DcMsg msg, @NonNull MediaType mediaType) {
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId);
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId, playbackViewModel);
}
private void addAttachmentContactInfo(int contactId) {
@@ -1110,6 +1169,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
inputPanel.clearSubject();
}
// Stop draft audio playback regardless, since it is unlikely
// we will need background playback for drafts
playbackViewModel.stopNonMessageAudioPlayback();
DcContext dcContext = DcHelper.getContext(context);
Util.runOnAnyBackgroundThread(() -> {
DcMsg msg = null;
@@ -1422,6 +1485,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// Listeners
@Override
public void onPlayPauseButtonClicked(View view) {
addActivityContext(
this.getIntent().getExtras(),
this.getClass().getName()
);
}
private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener {
@Override
public void onClick(int type) {
@@ -1590,7 +1661,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
dcChat = dcContext.getChat(chatId);
titleView.setTitle(glideRequests, dcChat);
initializeSecurity(isSecureText, isDefaultSms);
setComposePanelVisibility();
setComposePanelVisibility(false);
initializeContactRequest();
} else if ((eventId == DcContext.DC_EVENT_INCOMING_MSG
|| eventId == DcContext.DC_EVENT_MSG_READ)
@@ -34,6 +34,8 @@ import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -59,6 +61,7 @@ import java.util.Set;
* @author Moxie Marlinspike
*
*/
// FIXME: this breaks type checks, that is why there are so many casts.
public class ConversationAdapter <V extends View & BindableConversationItem>
extends RecyclerView.Adapter
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
@@ -97,6 +100,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private long pulseHighlightingSince = -1;
private int lastSeenPosition = -1;
private long lastSeen = -1;
private AudioPlaybackViewModel playbackViewModel;
private AudioView.OnActionListener audioPlayPauseListener;
protected static class ViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
@@ -170,6 +175,14 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
return fromDb;
}
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
this.playbackViewModel = playbackViewModel;
}
public void setAudioPlayPauseListener(AudioView.OnActionListener audioPlayPauseListener) {
this.audioPlayPauseListener = audioPlayPauseListener;
}
/**
* Returns the position of the message with msgId in the chat list, counted from the top
*/
@@ -237,7 +250,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
long elapsed = now - pulseHighlightingSince;
boolean pulseHighlight = (positionCurrentlyPulseHighlighting == position && elapsed < PULSE_HIGHLIGHT_MILLIS);
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight);
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight, playbackViewModel, audioPlayPauseListener);
}
@Override
@@ -41,6 +41,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
@@ -52,6 +53,7 @@ import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
@@ -101,6 +103,7 @@ public class ConversationFragment extends MessageSelectorFragment
private StickyHeaderDecoration dateDecoration;
private View scrollToBottomButton;
private View floatingLocationButton;
private View bottomDivider;
private AddReactionView addReactionView;
private TextView noMessageTextView;
private Timer reloadTimer;
@@ -108,6 +111,8 @@ public class ConversationFragment extends MessageSelectorFragment
public boolean isPaused;
private Debouncer markseenDebouncer;
private Rpc rpc;
private boolean pendingAddBottomInsets;
private boolean pendingRemoveBottomInsets;
@Override
public void onCreate(Bundle icicle) {
@@ -141,6 +146,7 @@ public class ConversationFragment extends MessageSelectorFragment
floatingLocationButton = ViewUtil.findById(view, R.id.floating_location_button);
addReactionView = ViewUtil.findById(view, R.id.add_reaction_view);
noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view);
bottomDivider = ViewUtil.findById(view, R.id.bottom_divider);
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
@@ -159,18 +165,32 @@ public class ConversationFragment extends MessageSelectorFragment
// with hardware layers, drawing may result in errors as "OpenGLRenderer: Path too large to be rendered into a texture"
list.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
if (pendingAddBottomInsets) {
bottomDivider.setVisibility(View.GONE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, true);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, true);
pendingAddBottomInsets = false;
}
if (pendingRemoveBottomInsets) {
bottomDivider.setVisibility(View.VISIBLE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, false);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, false);
pendingRemoveBottomInsets = false;
}
return view;
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initializeResources();
initializeListAdapter();
}
private void setNoMessageText() {
private void setNoMessageText() {
DcChat dcChat = getListAdapter().getChat();
if(dcChat.isMultiUser()){
if (dcChat.isInBroadcast() || dcChat.isOutBroadcast()) {
@@ -197,6 +217,28 @@ public class ConversationFragment extends MessageSelectorFragment
}
}
public void handleAddBottomInsets() {
if (bottomDivider != null) {
bottomDivider.setVisibility(View.GONE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, true);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, true);
pendingAddBottomInsets = false;
} else {
pendingAddBottomInsets = true;
}
}
public void handleRemoveBottomInsets() {
if (bottomDivider != null) {
bottomDivider.setVisibility(View.VISIBLE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, false);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, false);
pendingRemoveBottomInsets = false;
} else {
pendingRemoveBottomInsets = true;
}
}
@Override
public void onDestroy() {
DcHelper.getEventCenter(getContext()).removeObservers(this);
@@ -291,6 +333,10 @@ public class ConversationFragment extends MessageSelectorFragment
if (this.recipient != null && this.chatId != -1) {
ConversationAdapter adapter = new ConversationAdapter(getActivity(), this.recipient.getChat(), GlideApp.with(this), selectionClickListener, this.recipient);
list.setAdapter(adapter);
AudioPlaybackViewModel playbackViewModel =
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
adapter.setPlaybackViewModel(playbackViewModel);
adapter.setAudioPlayPauseListener(((ConversationActivity) requireActivity()));
if (dateDecoration != null) {
list.removeItemDecoration(dateDecoration);
@@ -407,7 +453,14 @@ public class ConversationFragment extends MessageSelectorFragment
}
public void handleClearChat() {
handleDeleteMessages((int) chatId, getListAdapter().getMessageIds());
AudioPlaybackViewModel playbackViewModel =
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
handleDeleteMessages(
(int) chatId,
getListAdapter().getMessageIds(),
playbackViewModel::stopByIds,
playbackViewModel::stopByIds);
}
private ConversationAdapter getListAdapter() {
@@ -891,7 +944,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);
}
}
@@ -940,12 +993,15 @@ public class ConversationFragment extends MessageSelectorFragment
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
hideAddReactionView();
int itemId = item.getItemId();
AudioPlaybackViewModel playbackViewModel =
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
if (itemId == R.id.menu_context_copy) {
handleCopyMessage(getListAdapter().getSelectedItems());
actionMode.finish();
return true;
} else if (itemId == R.id.menu_context_delete_message) {
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems());
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
return true;
} else if (itemId == R.id.menu_context_share) {
DcHelper.openForViewOrShare(getContext(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), Intent.ACTION_SEND);
@@ -41,8 +41,7 @@ import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.calls.CallUtil;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.BorderlessImageView;
import org.thoughtcrime.securesms.components.CallItemView;
@@ -52,6 +51,8 @@ import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.VcardView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DocumentSlide;
@@ -71,7 +72,6 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.calls.CallUtil;
import java.util.List;
import java.util.Set;
@@ -181,9 +181,11 @@ public class ConversationItem extends BaseConversationItem
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient recipients,
boolean pulseHighlight)
boolean pulseHighlight,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener)
{
bind(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
bindPartial(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
this.glideRequests = glideRequests;
this.showSender = ((dcChat.isMultiUser() || dcChat.isSelfTalk()) && !messageRecord.isOutgoing()) || messageRecord.getOverrideSenderName() != null;
@@ -204,7 +206,7 @@ public class ConversationItem extends BaseConversationItem
setGutterSizes(messageRecord, showSender);
setMessageShape(messageRecord);
setMediaAttributes(messageRecord, showSender);
setMediaAttributes(messageRecord, showSender, playbackViewModel, audioPlayPauseListener);
setBodyText(messageRecord);
setBubbleState(messageRecord);
setContactPhoto();
@@ -482,24 +484,10 @@ public class ConversationItem extends BaseConversationItem
}
private void setMediaAttributes(@NonNull DcMsg messageRecord,
boolean showSender)
boolean showSender,
AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener)
{
class SetDurationListener implements AudioSlidePlayer.Listener {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {}
@Override
public void onReceivedDuration(int millis) {
messageRecord.lateFilingMediaSize(0,0, millis);
audioViewStub.get().setDuration(millis);
}
}
if (hasAudio(messageRecord)) {
audioViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
@@ -511,12 +499,9 @@ public class ConversationItem extends BaseConversationItem
//noinspection ConstantConditions
int duration = messageRecord.getDuration();
if (duration == 0) {
AudioSlide audio = new AudioSlide(context, messageRecord);
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, new SetDurationListener());
audioSlidePlayer.requestDuration();
}
audioViewStub.get().setPlaybackViewModel(playbackViewModel);
audioViewStub.get().setOnActionListener(audioPlayPauseListener);
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord), duration);
audioViewStub.get().setOnClickListener(passthroughClickListener);
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
@@ -1000,9 +985,9 @@ public class ConversationItem extends BaseConversationItem
int chatId = messageRecord.getChatId();
if (!messageRecord.isOutgoing() && callInfo.state instanceof CallState.Alerting) {
int callId = messageRecord.getId();
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer);
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer, callInfo.hasVideo);
} else {
CallUtil.startCall(getContext(), accId, chatId);
CallUtil.startCall(getContext(), accId, chatId, callInfo.hasVideo);
}
}
}
@@ -15,6 +15,8 @@ import com.b44t.messenger.DcMsg;
import org.json.JSONObject;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.JsonUtils;
@@ -61,9 +63,11 @@ public class ConversationUpdateItem extends BaseConversationItem
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient conversationRecipient,
boolean pulseUpdate)
boolean pulseUpdate,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener)
{
bind(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
bindPartial(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
setGenericInfoRecord(messageRecord);
}
@@ -129,7 +129,7 @@ public class LogViewFragment extends Fragment {
return logFile;
}
private static String grabLogcat(LogViewFragment fragment) {
public static String grabLogcat() {
String command = "logcat -v threadtime -d -t 10000 *:I";
try {
final Process process = Runtime.getRuntime().exec(command);
@@ -165,7 +165,7 @@ public class LogViewFragment extends Fragment {
if (fragment == null) return null;
return "**This log may contain sensitive information. If you want to post it publicly you may examine and edit it beforehand.**\n\n" +
buildDescription(fragment) + "\n" + grabLogcat(fragment);
buildDescription(fragment) + "\n" + grabLogcat();
}
@Override
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
@@ -26,6 +27,7 @@ import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.Util;
import java.util.Set;
import java.util.function.Consumer;
public abstract class MessageSelectorFragment
extends Fragment
@@ -57,10 +59,14 @@ public abstract class MessageSelectorFragment
}
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords) {
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords));
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), null, null);
}
protected void handleDeleteMessages(int chatId, final int[] messageIds) {
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), deleteForMeListenerExtra, deleteForAllListenerExtra);
}
protected void handleDeleteMessages(int chatId, final int[] messageIds, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
DcContext dcContext = DcHelper.getContext(getContext());
DcChat dcChat = dcContext.getChat(chatId);
boolean canDeleteForAll = true;
@@ -79,20 +85,24 @@ public abstract class MessageSelectorFragment
String text = getActivity().getResources().getQuantityString(R.plurals.ask_delete_messages, messageIds.length, messageIds.length);
int positiveBtnLabel = dcChat.isSelfTalk() ? R.string.delete : R.string.delete_for_me;
DialogInterface.OnClickListener deleteForMeListener = (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
if (actionMode != null) actionMode.finish();
if (deleteForMeListenerExtra != null) deleteForMeListenerExtra.accept(messageIds);
};
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity())
.setMessage(text)
.setCancelable(true)
.setNeutralButton(android.R.string.cancel, null)
.setPositiveButton(positiveBtnLabel, (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
if (actionMode != null) actionMode.finish();
});
.setPositiveButton(positiveBtnLabel, deleteForMeListener);
if(canDeleteForAll) {
builder.setNegativeButton(R.string.delete_for_everyone, (d, which) -> {
DialogInterface.OnClickListener deleteForAllListener = (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds));
if (actionMode != null) actionMode.finish();
});
if (deleteForAllListenerExtra != null) deleteForAllListenerExtra.accept(messageIds);
};
builder.setNegativeButton(R.string.delete_for_everyone, deleteForAllListener);
AlertDialog dialog = builder.show();
Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE);
Util.redPositiveButton(dialog);
@@ -22,10 +22,7 @@ import static org.thoughtcrime.securesms.util.ShareUtil.acquireRelayMessageConte
import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
@@ -37,7 +34,6 @@ import com.google.zxing.integration.android.IntentResult;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.qr.QrActivity;
import org.thoughtcrime.securesms.qr.QrCodeHandler;
import org.thoughtcrime.securesms.util.MailtoUtil;
import chat.delta.rpc.types.SecurejoinSource;
import chat.delta.rpc.types.SecurejoinUiPath;
@@ -57,53 +53,6 @@ public class NewConversationActivity extends ContactSelectionActivity {
super.onCreate(bundle, ready);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
handleIntent();
}
private void handleIntent() {
Intent intent = getIntent();
String action = intent.getAction();
if(Intent.ACTION_VIEW.equals(action) || Intent.ACTION_SENDTO.equals(action)) {
try {
Uri uri = intent.getData();
if(uri != null) {
String scheme = uri.getScheme();
if(MailtoUtil.isMailto(uri)) {
String textToShare = MailtoUtil.getText(uri);
String[] recipientsArray = MailtoUtil.getRecipients(uri);
if (recipientsArray.length >= 1) {
if (!textToShare.isEmpty()) {
getIntent().putExtra(TEXT_EXTRA, textToShare);
}
final String addr = recipientsArray[0];
final DcContext dcContext = DcHelper.getContext(this);
int contactId = dcContext.lookupContactIdByAddr(addr);
if (contactId == 0 && dcContext.mayBeValidAddr(addr)) {
contactId = dcContext.createContact(null, recipientsArray[0]);
}
if (contactId == 0) {
Toast.makeText(this, R.string.bad_email_address, Toast.LENGTH_LONG).show();
} else {
onContactSelected(contactId);
}
} else {
Intent shareIntent = new Intent(this, ShareActivity.class);
shareIntent.putExtra(Intent.EXTRA_TEXT, textToShare);
startActivity(shareIntent);
finish();
}
} else if(scheme != null && scheme.startsWith("http")) {
Intent shareIntent = new Intent(this, ShareActivity.class);
shareIntent.putExtra(Intent.EXTRA_TEXT, uri.toString());
startActivity(shareIntent);
finish();
}
}
}
catch(Exception e) {
Log.e(TAG, "start activity from external 'mailto:' link failed", e);
}
}
}
@Override
@@ -310,21 +310,26 @@ public class ProfileFragment extends Fragment
super.onActivityResult(requestCode, resultCode, data);
if (requestCode==REQUEST_CODE_PICK_CONTACT && resultCode==Activity.RESULT_OK && data!=null) {
List<Integer> selected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA);
List<Integer> removed = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.REMOVED_CONTACTS_EXTRA);
if(selected == null) return;
List<Integer> deselected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.DESELECTED_CONTACTS_EXTRA);
Util.runOnAnyBackgroundThread(() -> {
// Add new members
for (Integer contactId : selected) {
if (contactId != null) {
dcContext.addContactToChat(chatId, contactId);
if (deselected != null) {
// Remove members that were deselected
int[] members = dcContext.getChatContacts(chatId);
for (int contactId : deselected) {
for (int memberId : members) {
if (memberId == contactId) {
dcContext.removeContactFromChat(chatId, memberId);
break;
}
}
}
}
// Remove members that were explicitly unchecked
if (removed != null) {
for (Integer contactId : removed) {
if (selected != null) {
// Add new members
for (Integer contactId : selected) {
if (contactId != null) {
dcContext.removeContactFromChat(chatId, contactId);
dcContext.addContactToChat(chatId, contactId);
}
}
}
@@ -104,26 +104,25 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
resolvedExtras = new ArrayList<>();
List<Uri> streamExtras = new ArrayList<>();
if (MailtoUtil.isMailto(getIntent().getData())) {
String[] extraEmail = getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL);
if (extraEmail == null || extraEmail.length == 0) {
getIntent().putExtra(Intent.EXTRA_EMAIL, MailtoUtil.getRecipients(getIntent().getData()));
}
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
if (text == null || text.isEmpty()) {
getIntent().putExtra(Intent.EXTRA_TEXT, MailtoUtil.getText(getIntent().getData()));
}
}
if (Intent.ACTION_SEND.equals(getIntent().getAction()) &&
getIntent().getParcelableExtra(Intent.EXTRA_STREAM) != null) {
Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
streamExtras.add(uri);
} else if (getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM) != null) {
streamExtras = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM);
} else {
Uri uri = getIntent().getData();
if (MailtoUtil.isMailto(uri)) {
String[] extraEmail = getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL);
if (extraEmail == null || extraEmail.length == 0) {
getIntent().putExtra(Intent.EXTRA_EMAIL, MailtoUtil.getRecipients(uri));
}
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
if (text == null || text.isEmpty()) {
getIntent().putExtra(Intent.EXTRA_TEXT, MailtoUtil.getText(uri));
}
} else if (uri != null) {
streamExtras.add(uri);
}
} else if (getIntent().getData() != null) {
streamExtras.add(getIntent().getData());
}
if (needsFilePermission(streamExtras)) {
@@ -265,6 +264,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
}
chatId = dcContext.createChatByContactId(contactId);
accId = dcContext.getAccountId();
}
Intent composeIntent;
if (accId != -1 && chatId != -1) {
@@ -77,7 +77,7 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
findViewById(R.id.status_bar_background).setBackgroundResource(R.drawable.search_toolbar_shadow);
} else {
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(findViewById(R.id.content_container));
ViewUtil.applyWindowInsets(findViewById(R.id.content_container), true, true, true, true, true, false);
}
webView.setWebViewClient(new WebViewClient() {
@@ -47,21 +47,24 @@ 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;
private 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;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
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) {
@@ -179,6 +182,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);
@@ -202,6 +206,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
private void onDeleteProfile(int accountId) {
AccountSelectionListFragment.this.dismiss();
ConversationListActivity activity = (ConversationListActivity)requireActivity();
DcAccounts accounts = DcHelper.getAccounts(activity);
Rpc rpc = DcHelper.getRpc(activity);
@@ -254,6 +259,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);
@@ -1,356 +0,0 @@
package org.thoughtcrime.securesms.audio;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.guava.Optional;
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
import java.io.IOException;
import java.lang.ref.WeakReference;
public class AudioSlidePlayer {
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
private final @NonNull Context context;
private final @NonNull AudioSlide slide;
private final @NonNull Handler progressEventHandler;
private @NonNull WeakReference<Listener> listener;
private @Nullable SimpleExoPlayer mediaPlayer;
private @Nullable SimpleExoPlayer durationCalculator;
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
@NonNull AudioSlide slide,
@NonNull Listener listener)
{
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
playing.get().setListener(listener);
return playing.get();
} else {
return new AudioSlidePlayer(context, slide, listener);
}
}
private AudioSlidePlayer(@NonNull Context context,
@NonNull AudioSlide slide,
@NonNull Listener listener)
{
this.context = context;
this.slide = slide;
this.listener = new WeakReference<>(listener);
this.progressEventHandler = new ProgressEventHandler(this);
}
public void requestDuration() {
try {
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build();
durationCalculator = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
.setTrackSelector(new DefaultTrackSelector(context))
.setLoadControl(loadControl)
.build();
durationCalculator.setPlayWhenReady(false);
durationCalculator.addListener(new Player.Listener() {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == Player.STATE_READY) {
Util.runOnMain(() -> {
synchronized (AudioSlidePlayer.this) {
if (durationCalculator == null) return;
Log.d(TAG, "request duration " + durationCalculator.getDuration());
getListener().onReceivedDuration(Long.valueOf(durationCalculator.getDuration()).intValue());
durationCalculator.release();
durationCalculator.removeListener(this);
durationCalculator = null;
}
});
}
}
});
durationCalculator.prepare(createMediaSource(slide.getUri()));
} catch (Exception e) {
Log.w(TAG, e);
getListener().onReceivedDuration(0);
}
}
public void play(final double progress) throws IOException {
play(progress, false);
}
private void play(final double progress, boolean earpiece) throws IOException {
if (this.mediaPlayer != null) {
return;
}
if (slide.getUri() == null) {
throw new IOException("Slide has no URI!");
}
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build();
this.mediaPlayer = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
.setTrackSelector(new DefaultTrackSelector(context))
.setLoadControl(loadControl)
.build();
mediaPlayer.prepare(createMediaSource(slide.getUri()));
mediaPlayer.setPlayWhenReady(true);
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
.setContentType(earpiece ? C.AUDIO_CONTENT_TYPE_SPEECH : C.AUDIO_CONTENT_TYPE_MUSIC)
.setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
.build(), false);
mediaPlayer.addListener(new Player.Listener() {
boolean started = false;
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")");
switch (playbackState) {
case Player.STATE_READY:
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
synchronized (AudioSlidePlayer.this) {
if (mediaPlayer == null) return;
Log.d(TAG, "DURATION: " + mediaPlayer.getDuration());
if (started) {
Log.d(TAG, "Already started. Ignoring.");
return;
}
started = true;
if (progress > 0) {
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
}
setPlaying(AudioSlidePlayer.this);
}
keepScreenOn(true);
notifyOnStart();
progressEventHandler.sendEmptyMessage(0);
break;
case Player.STATE_ENDED:
Log.i(TAG, "onComplete");
synchronized (AudioSlidePlayer.this) {
getListener().onReceivedDuration(Long.valueOf(mediaPlayer.getDuration()).intValue());
mediaPlayer.release();
mediaPlayer = null;
}
keepScreenOn(false);
notifyOnStop();
progressEventHandler.removeMessages(0);
}
}
@Override
public void onPlayerError(PlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error);
synchronized (AudioSlidePlayer.this) {
mediaPlayer.release();
mediaPlayer = null;
}
notifyOnStop();
progressEventHandler.removeMessages(0);
// Failed to play media file, maybe another app can handle it
int msgId = getAudioSlide().getDcMsgId();
DcHelper.openForViewOrShare(context, msgId, Intent.ACTION_VIEW);
}
});
}
private MediaSource createMediaSource(@NonNull Uri uri) {
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(defaultDataSourceFactory);
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
return new ProgressiveMediaSource.Factory(attachmentDataSourceFactory, extractorsFactory)
.createMediaSource(MediaItem.fromUri(uri));
}
public synchronized void stop() {
Log.i(TAG, "Stop called!");
keepScreenOn(false);
removePlaying(this);
if (this.mediaPlayer != null) {
this.mediaPlayer.stop();
this.mediaPlayer.release();
}
this.mediaPlayer = null;
}
public static void stopAll() {
if (playing.isPresent()) {
synchronized (AudioSlidePlayer.class) {
if (playing.isPresent()) {
playing.get().stop();
}
}
}
}
public void setListener(@NonNull Listener listener) {
this.listener = new WeakReference<>(listener);
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
notifyOnStart();
}
}
public @NonNull AudioSlide getAudioSlide() {
return slide;
}
private Pair<Double, Integer> getProgress() {
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
return new Pair<>(0D, 0);
} else {
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
(int) mediaPlayer.getCurrentPosition());
}
}
private void notifyOnStart() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onStart();
}
});
}
private void notifyOnStop() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onStop();
}
});
}
private void notifyOnProgress(final double progress, final long millis) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onProgress(slide, progress, millis);
}
});
}
private @NonNull Listener getListener() {
Listener listener = this.listener.get();
if (listener != null) return listener;
else return new Listener() {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {}
@Override
public void onReceivedDuration(int millis) {}
};
}
public void keepScreenOn(boolean keepOn) {
if (context instanceof Activity) {
if (keepOn) {
((Activity) context).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
((Activity) context).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
}
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
if (playing.isPresent() && playing.get() != player) {
playing.get().notifyOnStop();
playing.get().stop();
}
playing = Optional.of(player);
}
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
if (playing.isPresent() && playing.get() == player) {
playing = Optional.absent();
}
}
public interface Listener {
void onStart();
void onStop();
void onProgress(AudioSlide slide, double progress, long millis);
void onReceivedDuration(int millis);
}
private static class ProgressEventHandler extends Handler {
private final WeakReference<AudioSlidePlayer> playerReference;
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
this.playerReference = new WeakReference<>(player);
}
@Override
public void handleMessage(@NonNull Message msg) {
AudioSlidePlayer player = playerReference.get();
if (player == null || player.mediaPlayer == null || !isPlayerActive(player.mediaPlayer)) {
return;
}
Pair<Double, Integer> progress = player.getProgress();
player.notifyOnProgress(progress.first, progress.second);
sendEmptyMessageDelayed(0, 50);
}
private boolean isPlayerActive(@NonNull SimpleExoPlayer player) {
return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
}
}
}
@@ -43,12 +43,14 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
public static final String EXTRA_CHAT_ID = "chat_id";
public static final String EXTRA_CALL_ID = "call_id";
public static final String EXTRA_HASH = "hash";
public static final String EXTRA_HAS_VIDEO = "has_video";
private DcContext dcContext;
private Rpc rpc;
private int accId;
private int chatId;
private int callId;
private boolean hasVideo;
private boolean ended = false;
@SuppressLint("SetJavaScriptEnabled")
@@ -58,7 +60,9 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
Bundle bundle = getIntent().getExtras();
assert bundle != null;
hasVideo = bundle.getBoolean(EXTRA_HAS_VIDEO, true);
String hash = bundle.getString(EXTRA_HASH, "");
String query = hasVideo? "" : "?noOutgoingVideoInitially";
accId = bundle.getInt(EXTRA_ACCOUNT_ID, -1);
chatId = bundle.getInt(EXTRA_CHAT_ID, 0);
callId = bundle.getInt(EXTRA_CALL_ID, 0);
@@ -94,7 +98,7 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied))
.onAllGranted(() -> {
String url = "file:///android_asset/calls/index.html";
webView.loadUrl(url + hash);
webView.loadUrl(url + query + hash);
}).onAnyDenied(this::finish)
.execute();
}
@@ -168,7 +172,7 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
@JavascriptInterface
public void startCall(String payload) {
try {
callId = rpc.placeOutgoingCall(accId, chatId, payload);
callId = rpc.placeOutgoingCall(accId, chatId, payload, hasVideo);
} catch (RpcException e) {
Log.e(TAG, "Error", e);
}
@@ -18,28 +18,29 @@ import java.nio.charset.StandardCharsets;
public class CallUtil {
private static final String TAG = CallUtil.class.getSimpleName();
public static void startCall(Activity activity, int chatId) {
public static void startCall(Activity activity, int chatId, boolean hasVideo) {
Permissions.with(activity)
.request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_camera_denied))
.onAllGranted(() -> {
int accId = DcHelper.getContext(activity).getAccountId();
startCall(activity, accId, chatId);
startCall(activity, accId, chatId, hasVideo);
})
.execute();
}
public static void startCall(Context context, int accId, int chatId) {
public static void startCall(Context context, int accId, int chatId, boolean hasVideo) {
Intent intent = new Intent(context, CallActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, accId);
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatId);
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
intent.putExtra(CallActivity.EXTRA_HASH, "#startCall");
context.startActivity(intent);
}
public static void openCall(Context context, int accId, int chatId, int callId, String payload) {
public static void openCall(Context context, int accId, int chatId, int callId, String payload, boolean hasVideo) {
String base64 = Base64.encodeToString(payload.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
String hash = "";
try {
@@ -53,6 +54,7 @@ public class CallUtil {
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, accId);
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatId);
intent.putExtra(CallActivity.EXTRA_CALL_ID, callId);
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
intent.putExtra(CallActivity.EXTRA_HASH, hash);
context.startActivity(intent);
}
@@ -1,301 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.DateUtils;
import java.io.IOException;
public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
private static final String TAG = AudioView.class.getSimpleName();
private final @NonNull AnimatingToggle controlToggle;
private final @NonNull ImageView playButton;
private final @NonNull ImageView pauseButton;
private final @NonNull SeekBar seekBar;
private final @NonNull TextView timestamp;
private final @NonNull TextView title;
private final @NonNull View mask;
private @Nullable AudioSlidePlayer audioSlidePlayer;
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
private int backwardsCounter;
public AudioView(Context context) {
this(context, null);
}
public AudioView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.audio_view, this);
this.controlToggle = (AnimatingToggle) findViewById(R.id.control_toggle);
this.playButton = (ImageView) findViewById(R.id.play);
this.pauseButton = (ImageView) findViewById(R.id.pause);
this.seekBar = (SeekBar) findViewById(R.id.seek);
this.timestamp = (TextView) findViewById(R.id.timestamp);
this.title = (TextView) findViewById(R.id.title);
this.mask = findViewById(R.id.interception_mask);
this.timestamp.setText("00:00");
this.playButton.setOnClickListener(new PlayClickedListener());
this.pauseButton.setOnClickListener(new PauseClickedListener());
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
this.playButton.setImageDrawable(context.getDrawable(R.drawable.play_icon));
this.pauseButton.setImageDrawable(context.getDrawable(R.drawable.pause_icon));
this.playButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
this.pauseButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
setTint(getContext().getResources().getColor(R.color.audio_icon));
}
public void setAudio(final @NonNull AudioSlide audio, int duration)
{
controlToggle.displayQuick(playButton);
seekBar.setEnabled(true);
seekBar.setProgress(0);
audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, this);
timestamp.setText(DateUtils.getFormatedDuration(duration));
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
title.setVisibility(View.GONE);
}
else {
title.setText(audio.getFileName().get());
title.setVisibility(View.VISIBLE);
}
}
@Override
public void setOnClickListener(OnClickListener listener) {
super.setOnClickListener(listener);
this.mask.setOnClickListener(listener);
}
@Override
public void setOnLongClickListener(OnLongClickListener listener) {
super.setOnLongClickListener(listener);
this.mask.setOnLongClickListener(listener);
this.playButton.setOnLongClickListener(listener);
this.pauseButton.setOnLongClickListener(listener);
}
public void togglePlay() {
if (this.playButton.getVisibility() == View.VISIBLE) {
playButton.performClick();
} else {
pauseButton.performClick();
}
}
public String getDescription() {
String desc;
if (this.title.getVisibility() == View.VISIBLE) {
desc = getContext().getString(R.string.audio);
} else {
desc = getContext().getString(R.string.voice_message);
}
desc += "\n" + this.timestamp.getText();
if (title.getVisibility() == View.VISIBLE) {
desc += "\n" + this.title.getText();
}
return desc;
}
public void setDuration(int duration) {
if (getProgress()==0)
this.timestamp.setText(DateUtils.getFormatedDuration(duration));
}
public void cleanup() {
if (this.audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
this.audioSlidePlayer.stop();
}
}
@Override
public void onReceivedDuration(int millis) {
this.timestamp.setText(DateUtils.getFormatedDuration(millis));
}
@Override
public void onStart() {
if (this.pauseButton.getVisibility() != View.VISIBLE) {
togglePlayToPause();
}
}
@Override
public void onStop() {
if (this.playButton.getVisibility() != View.VISIBLE) {
togglePauseToPlay();
}
if (seekBar.getProgress() + 5 >= seekBar.getMax()) {
backwardsCounter = 4;
onProgress(audioSlidePlayer.getAudioSlide(), 0.0, -1);
}
}
public void disablePlayer(boolean disable) {
this.mask.setVisibility(disable? View.VISIBLE : View.GONE);
}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {
if (!audioSlidePlayer.getAudioSlide().equals(slide)) {
return;
}
int seekProgress = (int) Math.floor(progress * this.seekBar.getMax());
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
backwardsCounter = 0;
this.seekBar.setProgress(seekProgress);
if (millis != -1) {
this.timestamp.setText(DateUtils.getFormatedDuration(millis));
}
} else {
backwardsCounter++;
}
}
public void setTint(int foregroundTint) {
this.playButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
this.pauseButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
}
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
seekBar.getGlobalVisibleRect(rect);
}
private double getProgress() {
if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
return 0;
} else {
return (double)this.seekBar.getProgress() / (double)this.seekBar.getMax();
}
}
private void togglePlayToPause() {
controlToggle.displayQuick(pauseButton);
AnimatedVectorDrawable playToPauseDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.play_to_pause_animation);
pauseButton.setImageDrawable(playToPauseDrawable);
playToPauseDrawable.start();
}
private void togglePauseToPlay() {
controlToggle.displayQuick(playButton);
AnimatedVectorDrawable pauseToPlayDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.pause_to_play_animation);
playButton.setImageDrawable(pauseToPlayDrawable);
pauseToPlayDrawable.start();
}
private class PlayClickedListener implements View.OnClickListener {
@Override
public void onClick(View v) {
try {
Log.w(TAG, "playbutton onClick");
if (audioSlidePlayer != null) {
if (Build.VERSION.SDK_INT >= 26) {
if (audioFocusChangeListener == null) {
audioFocusChangeListener = focusChange -> {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
pauseButton.performClick();
}
};
}
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(false)
.setWillPauseWhenDucked(false)
.setOnAudioFocusChangeListener(audioFocusChangeListener)
.build();
AudioManager audioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
audioManager.requestAudioFocus(focusRequest);
}
togglePlayToPause();
audioSlidePlayer.play(getProgress());
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
private class PauseClickedListener implements View.OnClickListener {
@Override
public void onClick(View v) {
Log.w(TAG, "pausebutton onClick");
if (audioSlidePlayer != null) {
togglePauseToPlay();
audioSlidePlayer.stop();
}
}
}
private class SeekBarModifiedListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
@Override
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
audioSlidePlayer.stop();
}
}
@Override
public synchronized void onStopTrackingTouch(SeekBar seekBar) {
try {
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
audioSlidePlayer.play(getProgress());
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
}
@@ -67,10 +67,14 @@ public class CallItemView extends FrameLayout {
title.setText(R.string.canceled_call);
} else if (callInfo.state instanceof CallState.Declined) {
title.setText(R.string.declined_call);
} else if (callInfo.hasVideo) {
title.setText(isOutgoing? R.string.outgoing_video_call : R.string.incoming_video_call);
} else {
title.setText(isOutgoing? R.string.outgoing_call : R.string.incoming_call);
title.setText(isOutgoing? R.string.outgoing_audio_call : R.string.incoming_audio_call);
}
icon.setImageResource(callInfo.hasVideo? R.drawable.ic_videocam_white_24dp : R.drawable.baseline_call_24);
int[] attrs;
if (isOutgoing) {
attrs = new int[]{
@@ -11,6 +11,9 @@ import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import java.util.ArrayList;
import java.util.List;
public class RemovableEditableMediaView extends FrameLayout {
private final @NonNull ImageView remove;
@@ -19,6 +22,7 @@ public class RemovableEditableMediaView extends FrameLayout {
private final int removeSize;
private @Nullable View current;
private final List<OnClickListener> removeClickListeners = new ArrayList<>();
public RemovableEditableMediaView(Context context) {
this(context, null);
@@ -72,8 +76,22 @@ public class RemovableEditableMediaView extends FrameLayout {
return current;
}
public void setRemoveClickListener(View.OnClickListener listener) {
this.remove.setOnClickListener(listener);
public void addRemoveClickListener(View.OnClickListener listener) {
removeClickListeners.add(listener);
updateRemoveClickListener();
}
public void removeRemoveClickListener(View.OnClickListener listener) {
removeClickListeners.remove(listener);
updateRemoveClickListener();
}
private void updateRemoveClickListener() {
this.remove.setOnClickListener(v -> {
for (OnClickListener listener : removeClickListeners) {
listener.onClick(v);
}
});
}
public void setEditClickListener(View.OnClickListener listener) {
@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.net.Uri;
import androidx.annotation.Nullable;
public class AudioPlaybackState {
private final int msgId;
private final @Nullable Uri audioUri;
private final PlaybackStatus status;
private final long currentPosition;
private final long duration;
public enum PlaybackStatus {
IDLE,
LOADING,
PLAYING,
PAUSED,
ERROR
}
public AudioPlaybackState(int msgId,
@Nullable Uri audioUri,
PlaybackStatus status,
long currentPosition,
long duration) {
this.msgId = msgId;
this.audioUri = audioUri;
this.status = status;
this.currentPosition = currentPosition;
this.duration = duration;
}
public static AudioPlaybackState idle() {
return new AudioPlaybackState(0, null, PlaybackStatus.IDLE, 0, 0);
}
public int getMsgId() {
return msgId;
}
@Nullable
public Uri getAudioUri() {
return audioUri;
}
public PlaybackStatus getStatus() {
return status;
}
public long getCurrentPosition() {
return currentPosition;
}
public long getDuration() {
return duration;
}
}
@@ -0,0 +1,243 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.session.MediaController;
public class AudioPlaybackViewModel extends ViewModel {
private static final String TAG = AudioPlaybackViewModel.class.getSimpleName();
private static final int NON_MESSAGE_AUDIO_MSG_ID = 0; // Audios not attached to a message doesn't have message id.
private final MutableLiveData<AudioPlaybackState> playbackState;
private @Nullable MediaController mediaController;
private final Handler handler;
private boolean isUserSeeking = false;
public AudioPlaybackViewModel() {
playbackState = new MutableLiveData<>(AudioPlaybackState.idle());
handler = new Handler(Looper.getMainLooper());
}
public LiveData<AudioPlaybackState> getPlaybackState() {
return playbackState;
}
public void setMediaController(@Nullable MediaController controller) {
this.mediaController = controller;
if (mediaController != null && mediaController.isPlaying()) {
startUpdateProgress();
}
updateCurrentState(true);
setupPlayerListener();
}
// Public methods
public void loadAudioAndPlay(int msgId, Uri audioUri) {
if (mediaController == null) return;
// Set media item if we have a different audio.
if (isDifferentAudio(msgId, audioUri)) {
updateState(msgId, audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0);
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(String.valueOf(msgId))
.setUri(audioUri)
.build();
mediaController.setMediaItem(mediaItem);
mediaController.prepare();
}
play(msgId, audioUri);
}
private boolean isSameAudio(int msgId, Uri audioUri) {
return !isDifferentAudio(msgId, audioUri);
}
private boolean isDifferentAudio(int msgId, Uri audioUri) {
AudioPlaybackState currentState = playbackState.getValue();
return currentState != null && (
msgId != currentState.getMsgId() ||
currentState.getAudioUri() == null ||
currentState.getAudioUri() != null && !currentState.getAudioUri().equals(audioUri));
}
public void pause(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.pause();
}
}
public void play(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.play();
}
}
public void seekTo(long position, int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.seekTo(position);
}
}
public void stop(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.stop();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
}
}
public void stopNonMessageAudioPlayback() {
stopByIds(NON_MESSAGE_AUDIO_MSG_ID);
}
// A special method for deleting message, where we only use message Ids
public void stopByIds(int... msgIds) {
AudioPlaybackState currentState = playbackState.getValue();
if (mediaController != null && currentState != null) {
for (int msgId : msgIds) {
if (msgId == currentState.getMsgId()) {
mediaController.stop();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
}
}
}
}
public void setUserSeeking(boolean isUserSeeking) {
this.isUserSeeking = isUserSeeking;
}
// Private methods
private void setupPlayerListener() {
if (mediaController == null) return;
mediaController.addListener(new Player.Listener() {
@Override
public void onEvents(Player player, Player.Events events) {
if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {
if (player.isPlaying()) {
startUpdateProgress();
} else {
stopUpdateProgress();
}
updateCurrentState(false);
}
if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
if (player.getPlaybackState() == Player.STATE_READY) {
updateCurrentState(false);
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
// This is to prevent automatically playing after the audio
// has been play to the end once, then user dragged the seek bar again
mediaController.setPlayWhenReady(false);
}
}
if (events.containsAny(Player.EVENT_PLAYER_ERROR)) {
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.ERROR, 0, 0);
}
}
});
}
private void updateCurrentState(boolean queryPlaying) {
if (mediaController == null) return;
AudioPlaybackState.PlaybackStatus status;
if (mediaController.isPlaying()) {
status = AudioPlaybackState.PlaybackStatus.PLAYING;
} else if (mediaController.getPlaybackState() == Player.STATE_READY
|| mediaController.getPlaybackState() == Player.STATE_ENDED) {
status = AudioPlaybackState.PlaybackStatus.PAUSED;
} else {
status = AudioPlaybackState.PlaybackStatus.IDLE;
}
Uri currentUri = null;
int currentMsgId = 0;
if (playbackState.getValue() != null) {
currentMsgId = playbackState.getValue().getMsgId();
currentUri = playbackState.getValue().getAudioUri();
}
if (queryPlaying || playbackState.getValue() == null) {
MediaItem item = mediaController.getCurrentMediaItem();
if (item != null) {
try {
currentMsgId = Integer.parseInt(item.mediaId);
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid integer", e);
}
if (item.localConfiguration != null) {
currentUri = item.localConfiguration.uri;
}
}
}
updateState(
currentMsgId,
currentUri,
status,
mediaController.getCurrentPosition(),
mediaController.getDuration());
}
private void updateState(int msgId,
Uri audioUri,
AudioPlaybackState.PlaybackStatus status,
long position,
long duration) {
playbackState.setValue(new AudioPlaybackState(
msgId, audioUri, status, position, duration
));
}
private void updateCurrentAudioState(AudioPlaybackState.PlaybackStatus status,
long position,
long duration) {
AudioPlaybackState current = playbackState.getValue();
if (current != null) {
updateState(current.getMsgId(), current.getAudioUri(), status, position, duration);
}
}
// Progress tracking
private final Runnable progressRunnable = new Runnable() {
@Override
public void run() {
if (mediaController != null && mediaController.isPlaying() && !isUserSeeking) {
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.PLAYING,
mediaController.getCurrentPosition(),
mediaController.getDuration());
handler.postDelayed(this, 100);
}
}
};
private void startUpdateProgress() {
stopUpdateProgress();
handler.post(progressRunnable);
}
private void stopUpdateProgress() {
handler.removeCallbacks(progressRunnable);
}
@Override
protected void onCleared() {
stopUpdateProgress();
super.onCleared();
}
}
@@ -0,0 +1,311 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.lifecycle.Observer;
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.DateUtils;
public class AudioView extends FrameLayout {
private static final String TAG = AudioView.class.getSimpleName();
private final @NonNull ImageView playPauseButton;
private final AnimatedVectorDrawableCompat playToPauseDrawable;
private final AnimatedVectorDrawableCompat pauseToPlayDrawable;
private final Drawable playDrawable;
private final Drawable pauseDrawable;
private final Animatable2Compat.AnimationCallback animationCallback;
private final @NonNull SeekBar seekBar;
private final @NonNull TextView timestamp;
private final @NonNull TextView title;
private final @NonNull View mask;
private OnActionListener listener;
private int msgId;
private Uri audioUri;
private AudioPlaybackViewModel viewModel;
private final Observer<AudioPlaybackState> stateObserver = this::onPlaybackStateChanged;
private boolean isPlaying;
public AudioView(Context context) {
this(context, null);
}
public AudioView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.audio_view, this);
this.playPauseButton = findViewById(R.id.play_pause);
this.seekBar = findViewById(R.id.seek);
this.timestamp = findViewById(R.id.timestamp);
this.title = findViewById(R.id.title);
this.mask = findViewById(R.id.interception_mask);
this.timestamp.setText("00:00");
// Load drawables once
this.playToPauseDrawable = AnimatedVectorDrawableCompat.create(
getContext(), R.drawable.play_to_pause_animation);
this.pauseToPlayDrawable = AnimatedVectorDrawableCompat.create(
getContext(), R.drawable.pause_to_play_animation);
this.playDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.play_icon);
this.pauseDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.pause_icon);
this.animationCallback = new Animatable2Compat.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
Drawable endState = isPlaying ? pauseDrawable : playDrawable;
playPauseButton.setImageDrawable(endState);
}
};
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setupControls();
}
private void setupControls() {
playPauseButton.setOnClickListener(v -> {
Log.w(TAG, "playPauseButton onClick");
if (viewModel == null || audioUri == null) return;
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
if (state != null && msgId == state.getMsgId() && audioUri.equals(state.getAudioUri())) {
// Same audio
if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) {
viewModel.pause(msgId, audioUri);
} else {
viewModel.play(msgId, audioUri);
}
} else {
// Different audio
// Note: they can be the same *physical* file, but in different messages
viewModel.loadAudioAndPlay(msgId, audioUri);
}
if (listener != null) {
listener.onPlayPauseButtonClicked(v);
}
});
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
timestamp.setText(DateUtils.getFormatedDuration(progress));
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
viewModel.setUserSeeking(true);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
viewModel.setUserSeeking(false);
viewModel.seekTo(seekBar.getProgress(), msgId, audioUri);
}
});
if (playToPauseDrawable != null) {
playToPauseDrawable.registerAnimationCallback(animationCallback);
}
if (pauseToPlayDrawable != null) {
pauseToPlayDrawable.registerAnimationCallback(animationCallback);
}
}
@Override
protected void onDetachedFromWindow() {
if (viewModel != null) {
viewModel.getPlaybackState().removeObserver(stateObserver);
}
if (playToPauseDrawable != null) {
playToPauseDrawable.clearAnimationCallbacks();
}
if (pauseToPlayDrawable != null) {
pauseToPlayDrawable.clearAnimationCallbacks();
}
super.onDetachedFromWindow();
}
public void setPlaybackViewModel(AudioPlaybackViewModel viewModel) {
if (this.viewModel != null) {
this.viewModel.getPlaybackState().removeObserver(stateObserver);
}
// ViewModel is used directly for simplicity, since there is no reuse yet
this.viewModel = viewModel;
if (viewModel != null) {
viewModel.getPlaybackState().observeForever(stateObserver);
}
}
public void setAudio(final @NonNull AudioSlide audio, int duration)
{
msgId = audio.getDcMsgId();
audioUri = audio.getUri();
playPauseButton.setImageDrawable(playDrawable);
seekBar.setEnabled(true);
seekBar.setProgress(0);
timestamp.setText(DateUtils.getFormatedDuration(duration));
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
title.setVisibility(View.GONE);
}
else {
title.setText(audio.getFileName().get());
title.setVisibility(View.VISIBLE);
}
}
@Override
public void setOnClickListener(OnClickListener listener) {
super.setOnClickListener(listener);
this.mask.setOnClickListener(listener);
}
@Override
public void setOnLongClickListener(OnLongClickListener listener) {
super.setOnLongClickListener(listener);
this.mask.setOnLongClickListener(listener);
this.playPauseButton.setOnLongClickListener(listener);
}
public int getMsgId() {
return msgId;
}
public Uri getAudioUri() {
return audioUri;
}
public interface OnActionListener {
void onPlayPauseButtonClicked(View view);
}
public void setOnActionListener(OnActionListener listener) {
this.listener = listener;
}
public void togglePlay() {
playPauseButton.performClick();
}
public String getDescription() {
String desc;
if (this.title.getVisibility() == View.VISIBLE) {
desc = getContext().getString(R.string.audio);
} else {
desc = getContext().getString(R.string.voice_message);
}
desc += "\n" + this.timestamp.getText();
if (title.getVisibility() == View.VISIBLE) {
desc += "\n" + this.title.getText();
}
return desc;
}
private void updateProgress(AudioPlaybackState state) {
int duration = (int) state.getDuration();
int position = (int) state.getCurrentPosition();
if (duration > 0) {
seekBar.setMax(duration);
seekBar.setProgress(position);
timestamp.setText(DateUtils.getFormatedDuration(position));
}
}
public void disablePlayer(boolean disable) {
this.mask.setVisibility(disable? View.VISIBLE : View.GONE);
}
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
seekBar.getGlobalVisibleRect(rect);
}
private void togglePlayPause(boolean expectedPlaying) {
isPlaying = expectedPlaying;
Drawable expectedDrawable = expectedPlaying ? pauseDrawable : playDrawable;
boolean isAnimating = false;
Drawable currentDrawable = playPauseButton.getDrawable();
if (currentDrawable instanceof AnimatedVectorDrawableCompat) {
isAnimating = ((AnimatedVectorDrawableCompat) currentDrawable).isRunning();
}
if (!isAnimating && playPauseButton.getDrawable() != expectedDrawable) {
AnimatedVectorDrawableCompat animDrawable = expectedPlaying ? playToPauseDrawable : pauseToPlayDrawable;
String contentDescription = getContext().getString(
expectedPlaying ? R.string.menu_pause : R.string.menu_play);
if (animDrawable != null) {
playPauseButton.setImageDrawable(animDrawable);
playPauseButton.setContentDescription(contentDescription);
animDrawable.start();
}
}
}
private void onPlaybackStateChanged(AudioPlaybackState state) {
if (audioUri == null || state == null) return;
// Check if this state is about this message
boolean isThisMessage = msgId == state.getMsgId() && audioUri.equals(state.getAudioUri());
if (isThisMessage) {
updateUIForPlaybackState(state);
} else {
togglePlayPause(false);
}
}
private void updateUIForPlaybackState(AudioPlaybackState state) {
switch (state.getStatus()) {
case PLAYING:
togglePlayPause(true);
updateProgress(state);
break;
case PAUSED:
togglePlayPause(false);
updateProgress(state);
break;
case LOADING:
case ERROR:
// No special handling yet
break;
}
}
}
@@ -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);
}
@@ -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;
@@ -210,8 +210,6 @@ public class DcHelper {
dcContext.setStockTranslation(178, context.getString(R.string.member_x_removed));
dcContext.setStockTranslation(190, context.getString(R.string.secure_join_wait));
dcContext.setStockTranslation(193, context.getString(R.string.donate_device_msg));
dcContext.setStockTranslation(194, context.getString(R.string.outgoing_call));
dcContext.setStockTranslation(195, context.getString(R.string.incoming_call));
dcContext.setStockTranslation(196, context.getString(R.string.declined_call));
dcContext.setStockTranslation(197, context.getString(R.string.canceled_call));
dcContext.setStockTranslation(198, context.getString(R.string.missed_call));
@@ -223,6 +221,10 @@ public class DcHelper {
dcContext.setStockTranslation(220, context.getString(R.string.proxy_enabled));
dcContext.setStockTranslation(221, context.getString(R.string.proxy_enabled_hint));
dcContext.setStockTranslation(230, context.getString(R.string.chat_unencrypted_explanation));
dcContext.setStockTranslation(232, context.getString(R.string.outgoing_audio_call));
dcContext.setStockTranslation(233, context.getString(R.string.outgoing_video_call));
dcContext.setStockTranslation(234, context.getString(R.string.incoming_audio_call));
dcContext.setStockTranslation(235, context.getString(R.string.incoming_video_call));
}
public static File getImexDir() {
@@ -49,13 +49,13 @@ import org.thoughtcrime.securesms.WebxdcActivity;
import org.thoughtcrime.securesms.WebxdcStoreActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.VcardView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.geolocation.DcLocationManager;
@@ -121,7 +121,7 @@ public class AttachmentManager {
//this.mapView = ViewUtil.findById(root, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
removableMediaView.setRemoveClickListener(new RemoveButtonListener());
removableMediaView.addRemoveClickListener(new RemoveButtonListener());
removableMediaView.setEditClickListener(new EditButtonListener());
thumbnail.setOnClickListener(new ThumbnailClickListener());
}
@@ -152,8 +152,6 @@ public class AttachmentManager {
markGarbage(getSlideUri());
slide = Optional.absent();
audioView.cleanup();
}
}
@@ -235,7 +233,8 @@ public class AttachmentManager {
@NonNull final MediaType mediaType,
final int width,
final int height,
final int chatId)
final int chatId,
AudioPlaybackViewModel playbackViewModel)
{
inflateStub();
@@ -285,26 +284,12 @@ public class AttachmentManager {
setAttachmentPresent(true);
if (slide.hasAudio()) {
class SetDurationListener implements AudioSlidePlayer.Listener {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {}
@Override
public void onReceivedDuration(int millis) {
((AudioView) removableMediaView.getCurrent()).setDuration(millis);
}
}
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(context, (AudioSlide) slide, new SetDurationListener());
audioSlidePlayer.requestDuration();
audioView.setPlaybackViewModel(playbackViewModel);
audioView.setAudio((AudioSlide) slide, 0);
removableMediaView.display(audioView, false);
removableMediaView.addRemoveClickListener(v -> {
playbackViewModel.stop(audioView.getMsgId(), audioView.getAudioUri());
});
result.set(true);
} else if (slide.isVcard()) {
vcardView.setVcard(glideRequests, (VcardSlide)slide, DcHelper.getRpc(context));
@@ -61,6 +61,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import chat.delta.rpc.RpcException;
public class NotificationCenter {
private static final String TAG = NotificationCenter.class.getSimpleName();
@NonNull private final ApplicationContext context;
@@ -164,7 +166,7 @@ public class NotificationCenter {
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
}
public PendingIntent getOpenCallIntent(ChatData chatData, int callId, String payload, boolean autoAccept) {
public PendingIntent getOpenCallIntent(ChatData chatData, int callId, String payload, boolean autoAccept, boolean hasVideo) {
final Intent chatIntent = new Intent(context, ConversationActivity.class)
.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, chatData.accountId)
.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatData.chatId)
@@ -184,6 +186,7 @@ public class NotificationCenter {
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatData.chatId);
intent.putExtra(CallActivity.EXTRA_CALL_ID, callId);
intent.putExtra(CallActivity.EXTRA_HASH, hash);
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
intent.setPackage(context.getPackageName());
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(chatIntent)
@@ -427,6 +430,13 @@ public class NotificationCenter {
Util.runOnAnyBackgroundThread(() -> {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
DcContext dcContext = context.getDcAccounts().getAccount(accId);
boolean hasVideo;
try {
hasVideo = context.getRpc().callInfo(accId, callId).hasVideo;
} catch (RpcException e) {
Log.e(TAG, "Rpc.callInfo() failed", e);
hasVideo = false;
}
int chatId = dcContext.getMsg(callId).getChatId();
DcChat dcChat = dcContext.getChat(chatId);
String name = dcChat.getName();
@@ -434,7 +444,7 @@ public class NotificationCenter {
String notificationChannel = getCallNotificationChannel(notificationManager, chatData, name);
PendingIntent declineCallIntent = getDeclineCallIntent(chatData, callId);
PendingIntent openCallIntent = getOpenCallIntent(chatData, callId, payload, false);
PendingIntent openCallIntent = getOpenCallIntent(chatData, callId, payload, false, hasVideo);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannel)
.setSmallIcon(R.drawable.icon_notification)
@@ -459,7 +469,7 @@ public class NotificationCenter {
new NotificationCompat.Action.Builder(
R.drawable.ic_videocam_white_24dp,
context.getString(R.string.answer_call),
getOpenCallIntent(chatData, callId, payload, true)).build());
getOpenCallIntent(chatData, callId, payload, true, hasVideo)).build());
Bitmap bitmap = getAvatar(dcChat);
if (bitmap != null) {
@@ -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();
@@ -0,0 +1,140 @@
package org.thoughtcrime.securesms.service;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.session.MediaSession;
import androidx.media3.session.MediaSessionService;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionCommands;
import androidx.media3.session.SessionResult;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.ConversationListActivity;
public class AudioPlaybackService extends MediaSessionService {
private static final String TAG = AudioPlaybackService.class.getSimpleName();
private ExoPlayer player;
private MediaSession session;
@Override
public void onCreate() {
super.onCreate();
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA) // USAGE_VOICE_COMMUNICATION is for VoIP calls
.setContentType(C.AUDIO_CONTENT_TYPE_SPEECH)
.build();
player = new ExoPlayer.Builder(this)
.setAudioAttributes(audioAttributes, true)
.setHandleAudioBecomingNoisy(true)
.build();
// This is for click on the notification to go back to app
Intent intent = new Intent(this, ConversationListActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent initialIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
session = new MediaSession.Builder(this, player)
.setSessionActivity(initialIntent)
.setCallback(new MediaSession.Callback() {
@OptIn(markerClass = UnstableApi.class)
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session,
MediaSession.ControllerInfo controller
) {
SessionCommands sessionCommands = MediaSession
.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
.add(new SessionCommand("UPDATE_ACTIVITY_CONTEXT", new Bundle()))
.build();
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(sessionCommands)
.build();
}
@NonNull
@Override
public ListenableFuture<SessionResult> onCustomCommand(
MediaSession session,
MediaSession.ControllerInfo controller,
SessionCommand customCommand,
Bundle args
) {
if ("UPDATE_ACTIVITY_CONTEXT".equals(customCommand.customAction)) {
updateSessionActivity(args);
}
return Futures.immediateFuture(
new SessionResult(SessionResult.RESULT_SUCCESS));
}
})
.build();
}
@OptIn(markerClass = UnstableApi.class)
private void updateSessionActivity(Bundle args) {
try {
// Put all the original extras back into the intent
if (args != null && !args.isEmpty()) {
String activityClassName = args.getString("activity_class");
args.remove("activity_class");
if (activityClassName != null) {
Class<?> activityClass = Class.forName(activityClassName);
Intent intent = new Intent(this, activityClass);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtras(args);
PendingIntent pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
session.setSessionActivity(pendingIntent);
}
}
} catch (ClassNotFoundException e) {
Log.e(TAG, "Activity class not found", e);
}
}
@Nullable
@Override
public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
return session;
}
@Override
public void onDestroy() {
if (session != null) {
session.release();
session = null;
}
if (player != null) {
player.release();
player = null;
}
super.onDestroy();
}
}
@@ -323,7 +323,15 @@ public class ViewUtil {
* @param view The view to apply insets to
*/
public static void applyWindowInsetsAsMargin(@NonNull View view) {
applyWindowInsetsAsMargin(view, true, true, true, true);
applyWindowInsetsAsMargin(view, true, true, true, true, false);
}
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsetsAsMargin(view, left, top, right, bottom, false);
}
public static void forceApplyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsetsAsMargin(view, left, top, right, bottom, true);
}
/**
@@ -337,8 +345,9 @@ public class ViewUtil {
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
* @param forceDispatch Whether to force application of insets
*/
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean forceDispatch) {
// Only enable on API 30+ where WindowInsets APIs work correctly
if (!isEdgeToEdgeSupported()) return;
@@ -371,10 +380,10 @@ public class ViewUtil {
ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) layoutParams;
marginParams.leftMargin = baseMarginLeft + insets.left;
marginParams.topMargin = baseMarginTop + insets.top;
marginParams.rightMargin = baseMarginRight + insets.right;
marginParams.bottomMargin = baseMarginBottom + insets.bottom;
marginParams.leftMargin = left ? baseMarginLeft + insets.left : baseMarginLeft;
marginParams.topMargin = top ? baseMarginTop + insets.top : baseMarginTop;
marginParams.rightMargin = right ? baseMarginRight + insets.right : baseMarginRight;
marginParams.bottomMargin = bottom ? baseMarginBottom + insets.bottom : baseMarginBottom;
v.setLayoutParams(marginParams);
}
@@ -383,18 +392,54 @@ public class ViewUtil {
// Request the initial insets to be dispatched if the view is attached
if (view.isAttachedToWindow()) {
ViewCompat.requestApplyInsets(view);
if (forceDispatch) {
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
if (insets != null) {
ViewCompat.dispatchApplyWindowInsets(view, insets);
}
} else {
ViewCompat.requestApplyInsets(view);
}
}
}
/**
* Apply window insets to a view by adding padding to avoid drawing elements behind system bars.
* Convenience method that applies insets to all sides.
* IME insets are propagated to child views.
*
* @param view The view to apply insets to
*/
public static void applyWindowInsets(@NonNull View view) {
applyWindowInsets(view, true, true, true, true);
applyWindowInsets(view, true, true, true, true, false, false);
}
/**
* Apply window insets to a view by adding padding to avoid drawing elements behind system bars.
*
* IME insets are propagated to child views.
*
* @param view The view to apply insets to
* @param left Whether to apply left inset
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
*/
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsets(view, left, top, right, bottom, false, false);
}
/**
* Force applying window insets to a view by adding padding to avoid drawing elements behind system bars.
*
* @param view The view to apply insets to
* @param left Whether to apply left inset
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
*/
public static void forceApplyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsets(view, left, top, right, bottom, false, true);
}
/**
@@ -408,8 +453,10 @@ public class ViewUtil {
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
* @param consumeImeInsets Whether to consume IME insets so they don't propagate to child views
* @param forceDispatch Force application of Insets, regardless if system think it shall dispatch
*/
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean consumeImeInsets, boolean forceDispatch) {
// Only enable on API 30+ where WindowInsets APIs work correctly
if (!isEdgeToEdgeSupported()) return;
@@ -442,12 +489,24 @@ public class ViewUtil {
bottom ? basePaddingBottom + insets.bottom : basePaddingBottom
);
if (consumeImeInsets) {
windowInsets = new WindowInsetsCompat.Builder(windowInsets)
.setInsets(WindowInsetsCompat.Type.ime(), Insets.NONE)
.build();
}
return windowInsets;
});
// Request the initial insets to be dispatched if the view is attached
if (view.isAttachedToWindow()) {
ViewCompat.requestApplyInsets(view);
if (forceDispatch) {
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
if (insets != null) {
ViewCompat.dispatchApplyWindowInsets(view, insets);
}
} else {
ViewCompat.requestApplyInsets(view);
}
}
}
+19 -37
View File
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.thoughtcrime.securesms.components.AudioView">
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout android:id="@+id/audio_widget_container"
android:orientation="vertical"
@@ -12,39 +10,19 @@
android:layout_height="fill_parent"
android:orientation="horizontal">
<org.thoughtcrime.securesms.components.AnimatingToggle
android:id="@+id/control_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center">
<ImageView android:id="@+id/play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_play_circle_fill_white_48dp"
android:scaleType="centerInside"
tools:visibility="gone"
android:contentDescription="@string/menu_play"/>
<ImageView android:id="@+id/pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_pause_circle_fill_white_48dp"
android:scaleType="centerInside"
android:contentDescription="@string/menu_pause"/>
</org.thoughtcrime.securesms.components.AnimatingToggle>
<ImageView android:id="@+id/play_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:focusable="true"
android:gravity="center_vertical"
android:clickable="true"
android:visibility="visible"
android:background="@drawable/ic_circle_fill_white_48dp"
android:backgroundTint="@color/audio_icon"
android:src="@drawable/play_icon"
android:scaleType="centerInside"
android:contentDescription="@string/menu_play"/>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="fill_parent"
@@ -54,7 +32,11 @@
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:layout_gravity="center_vertical"/>
android:layout_gravity="center_vertical"
android:progressTint="@color/audio_icon"
android:progressTintMode="src_in"
android:thumbTint="@color/audio_icon"
android:thumbTintMode="src_in"/>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
-1
View File
@@ -44,7 +44,6 @@
android:layout_width="30dp"
android:layout_height="30dp"
android:contentDescription="@null"
android:src="@drawable/ic_videocam_white_24dp"
/>
</LinearLayout>
@@ -37,7 +37,7 @@
app:minHeight="100dp"
app:maxHeight="300dp" />
<org.thoughtcrime.securesms.components.AudioView
<org.thoughtcrime.securesms.components.audioplay.AudioView
android:id="@+id/attachment_audio"
android:layout_width="230dp"
android:layout_height="wrap_content"
@@ -27,6 +27,7 @@
/>
<View
android:id="@+id/bottom_divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_gravity="bottom"
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.AudioView
<org.thoughtcrime.securesms.components.audioplay.AudioView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="210dp"
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.AudioView
<org.thoughtcrime.securesms.components.audioplay.AudioView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="210dp"
android:layout_height="wrap_content"
@@ -17,7 +17,7 @@
android:layout_weight="1"
android:visibility="gone"/>
<org.thoughtcrime.securesms.components.AudioView
<org.thoughtcrime.securesms.components.audioplay.AudioView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/audio_view"
+16 -3
View File
@@ -26,9 +26,22 @@
app:showAsAction="always"/>
<item android:id="@+id/menu_start_call"
android:title="@string/start_call"
android:icon="@drawable/ic_videocam_white_24dp"
app:showAsAction="always" />
android:title="@string/start_call"
app:showAsAction="always"
android:icon="@drawable/baseline_call_24">
<menu>
<item android:id="@+id/menu_start_audio_call"
android:title="@string/start_audio_call"
android:icon="@drawable/baseline_call_24"
app:iconTint="?attr/menu_icon_tint"
/>
<item android:id="@+id/menu_start_video_call"
android:title="@string/start_video_call"
android:icon="@drawable/ic_videocam_white_24dp"
app:iconTint="?attr/menu_icon_tint"
/>
</menu>
</item>
<item android:id="@+id/menu_all_media"
android:title="@string/apps_and_media"
+2 -2
View File
@@ -1089,8 +1089,8 @@
<string name="name_desktop">Name</string>
<string name="select_group_image_desktop">Gruppenbild auswählen</string>
<string name="export_backup_desktop">Backup exportieren</string>
<string name="message_detail_sent_desktop">gesendet</string>
<string name="message_detail_received_desktop">empfangen</string>
<string name="message_detail_sent_desktop">Gesendet</string>
<string name="message_detail_received_desktop">Empfangen</string>
<string name="menu.view.developer.open.log.folder">Log-Ordner öffnen</string>
<string name="menu.view.developer.open.current.log.file">Aktuelle Logdatei öffnen</string>
<string name="explain_desktop_minimized_disabled_tray_pref" tools:ignore="TypographyDashes">Benachrichtigungssymbol kann nicht deaktiviert werden, da Delta Chat mit der Option --minimized gestartet wurde.</string>
+41 -5
View File
@@ -21,10 +21,10 @@
<string name="none">هیچ‌کدام</string>
<string name="automatic">خودکار</string>
<string name="strict">سخت‌گیرانه</string>
<string name="open">گشودن</string>
<string name="open">بازکردن</string>
<string name="download">بارگیری</string>
<string name="downloading">در حال بارگیری…</string>
<string name="open_attachment">گشودن پیوست</string>
<string name="open_attachment">بازکردن پیوست</string>
<string name="join">پیوستن</string>
<string name="rejoin">پیوستن مجدد</string>
<string name="delete">حذف</string>
@@ -54,7 +54,7 @@
<string name="mute">سکوت</string>
<string name="muted">بی‌صدا</string>
<string name="ephemeral_messages">پیام‌های ناپدید شونده</string>
<string name="ephemeral_messages_hint">این روی تمام عضو‌های این گپ، اگر از دلتاچت استفاده کنند، اعمال می‌شود. ولی همچنان می‌توانند پیام‌ها را رونوشت، ذخیره و هدایت کنند.</string>
<string name="ephemeral_messages_hint">شامل همه کسانی که در این گفتگو حضور دارند می‌شود. آن‌ها همچنان می‌توانند پیام‌ها را هدایت و ذخیره کرده یا از آن‌ها رونوشت بگیرند. </string>
<string name="save">ذخیره</string>
<string name="chat">گپ</string>
<string name="media">رسانه</string>
@@ -76,9 +76,9 @@
<string name="activate">فعال کردن</string>
<string name="load_remote_content">بارگیری تصاویر از بیرون</string>
<!-- Possible answers to the question are: Never, Always, Once -->
<string name="load_remote_content_ask">تصاویر مربوط به نشانی‌های اینترنتی دیگر می‌توانند برای ردیابی شما استفاده شوند. \n\n این تنظیمها همچنین برای بارگیری فونت‌ها و دیگر محتواها استفاده می‌شوند. اگر غیرفعال باشد ممکن است همچنان تصویر‌های درون پیام و تصویر‌های بارگیری شده را ببینید. \n\n آيا تصاویر خارجی بارگیری شوند؟</string>
<string name="load_remote_content_ask">تصاویر مربوط به نشانی‌های اینترنتی دیگر می‌توانند برای ردیابی شما استفاده شوند. \n\n این تنظیمات همچنین برای بارگیری فونت‌ها و دیگر محتواها استفاده می‌شود. اگر غیرفعال باشد ممکن است همچنان تصویر‌های درون پیام و تصویر‌های بارگیری شده را ببینید. \n\n آيا تصاویر خارجی بارگیری شوند؟</string>
<string name="always">همیشه</string>
<string name="always_load_remote_images">تصاویر دوردست همیشه بار شوند</string>
<string name="always_load_remote_images">تصاویر با منبع بیرونی همیشه بارگیری شوند</string>
<string name="once">همین یک بار</string>
<string name="show_warning">نمایش هشدار</string>
<string name="not_now">اکنون نه</string>
@@ -208,6 +208,8 @@
<string name="webxdc_app">برنامه</string>
<!-- plural of "App"; used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_apps">برنامه‌ها</string>
<string name="webxdc_store_url">آدرس مخزن برنامه‌ها</string>
<string name="webxdc_store_url_explain">اگر آن را تنظیم کنید، از این آدرس برای مخزن برنامه‌ها به جای آدرس پیش‌فرض استفاده می‌شود</string>
<string name="webxdc_draft_hint">برای به اشتراک گذاری روی «ارسال» بزنید</string>
<string name="home">خانه</string>
<string name="games">بازی‌ها</string>
@@ -629,6 +631,7 @@
<string name="welcome_chat_over_email">گفتگو غیرمتمرکز و ایمن</string>
<string name="scan_invitation_code">اسکن کردن کد دعوت</string>
<string name="login_title">ورود</string>
<string name="login_advanced_hint">این برای کاربر‌های حرفه‌ای است:\n\n • از یک آدرس که در برنامه‌ای دیگر استفاده می‌کنید استفاده نکنید.\n\n • ارائه‌دهندهٔ رایانامه کلاسیک اجازه خواهد داد که گپ‌ها بدون رمزنگاری سراسری باشند. این گپ‌ها به وسیلهٔ یک نماد نامه نشانه‌گذاری می‌شوند.</string>
<string name="login_inbox">ورودی</string>
<string name="login_imap_login">IMAP Login Name</string>
<string name="login_imap_server">IMAP Server</string>
@@ -653,6 +656,19 @@
<string name="proxy_use_proxy_confirm">می‌خواهید از پروکسی «%1$s» استفاده کنید؟</string>
<string name="proxy_share_explain">دوست‌هایتان می‌توانند با اسکن کردن کد QR این پروکسی را اضافه کنند.</string>
<string name="proxy_share_link">اشتراک گذاشتن پیوند</string>
<string name="proxy_enabled">پروکسی فعال شده</string>
<string name="proxy_enabled_hint">شما از یک پروکسی استفاده می‌کنید. اگر مشکل وصل شدن داری، یک پروکسی دیگر استفاده کنید.</string>
<!-- "Relay" refers to the relay servers used; please chose a common term in your language, no need to be literal. If in doubt, stay with "Relay" -->
<string name="transports">رله‌ها</string>
<string name="add_transport">اضافه کردن رله</string>
<string name="remove_transport">حذف رله</string>
<string name="edit_transport">ویرایش رله</string>
<!-- shown if a QR code was scanned that can be used as a relay -->
<string name="confirm_add_transport">اضافه کردن این رله؟</string>
<string name="invalid_transport_qr">کیو‌آر کد اسکن شده دارای یک رلهٔ معتبر نیست.</string>
<!-- placeholder will be replaced by a relay server name -->
<string name="confirm_remove_transport">حذف کردن رلهٔ «%1$s»؟\n\n مخاطب‌های شما تنها در صورتی می‌توانند با شما ارتباط برقرار کنند که قبلا از طریق یک رلهٔ دیگر با آن‌ها تماس داشته‌اید.\n\n در صورت تردید، این رله را بعدا حذف کنید.</string>
<string name="login_certificate_checks">بررسی‌های گواهینامه</string>
<string name="login_error_mail">لطفاً یک نشانی رایانامهٔ معتبر وارد کنید</string>
<string name="login_error_server">لطفا یک کارساز / نشانی آی‌پی صحیح وارد کنید</string>
@@ -716,6 +732,7 @@
<string name="pref_screen_security_explain">درخواست مسدود کردن عکس گرفتن از صفحه در لیست اخیر و درون نرم‌افزار</string>
<string name="pref_screen_security_please_restart_hint">برای اعمال کردن تنظیم‌های امنیت صفحه لطفا نرم‌افزار را بسته و دوباره باز کنید.</string>
<string name="pref_notifications">اعلان‌ها</string>
<string name="pref_mention_notifications">اشاره‌ها(منشن‌ها)</string>
<string name="pref_mention_notifications_explain">در گروه‌های بی‌صدا شده، برای پیام‌های مستقیم به خودتان، مانند پاسخ‌ها یا واکنش‌ها، اعلان دریافت کنید.</string>
<string name="pref_notifications_show">نمایش</string>
<string name="pref_notifications_priority">اولویت</string>
@@ -766,6 +783,8 @@
<string name="pref_imap_folder_warn_disable_defaults">اگر این گزینه را غیر فعال می‌کنید از اینکه سرور شما و حساب‌هایتان هم بر این اساس تنظیم شده باشند.\n\n در غیر این صورت ممکن است هیچ چیز کار نکند. </string>
<!-- No need to be literal here, you can also use "Use Multiple Devices", "Support Multiple Devices" or other fitting terms. However, it should fit to the wording or your language at https://delta.chat/help -->
<string name="pref_multidevice">حالت چند دستگاهی</string>
<string name="pref_multidevice_explain">همگام‌سازی پیام‌های شما با دیگر دستگاه‌هایتان. در صورت اضافه کردن دستگاه دوم به صورت خودکار فعال می‌شود.</string>
<string name="pref_multidevice_change_warn">حالت چند دستگاهی زمانی که از یک نمایه روی چند دستگاه استفاده می‌کنید باید فعال شود. تنها در صورتی این تنظیم را غیرفعال کنید که این نمایه را از روی همهٔ دیگر دستگاه‌هایتان حذف کرده‌اید. \n\n غیرفعال کردن حالت چند دستگاهی زمانی که یک نمایه را روی چند دستگاه دارید باعث می‌شود که بعضی پیام‌ها گم شوند و همچنین مشکل‌های دیگری به وجود می‌آورد.</string>
<string name="pref_auto_folder_moves">انتقال خودکار به پوشه دلتاچت</string>
<string name="pref_only_fetch_mvbox_title">فقط پوشه دلتاچت را بررسی کن</string>
<string name="pref_show_emails">نمایش رایانامه‌های معمولی</string>
@@ -773,6 +792,7 @@
<string name="pref_show_emails_accepted_contacts">برای مخاطبین پذیرفته شده</string>
<string name="pref_show_emails_all">همه</string>
<string name="pref_experimental_features">ویژگی‌های آزمایشی</string>
<string name="pref_experimental_features_explain">این ویژگی‌ها ممکن است ناپایدار باشند و همچنین ممکن است بعدا تغییر کنند یا حذف شوند</string>
<string name="pref_on_demand_location_streaming">هم‌رسانی زندهٔ به‌فرمان مکان</string>
<string name="pref_background_default">پس‌زمینه پیش‌فرض</string>
<string name="pref_background_default_color">رنگ پیش‌فرض</string>
@@ -796,7 +816,16 @@
<string name="pref_show_tray_icon">نمایش نمادک در زیر صفحه</string>
<string name="pref_edit_profile">ویرایش نمایه</string>
<string name="disable_imap_idle">غیر فعال‌سازی IMAP IDLE</string>
<string name="disable_imap_idle_explain">استفاده نکردن از افزونهٔ IMAP IDLE حتی در صورتی که ارائه‌دهنده از آن پشتیبانی می‌کند. فعال‌سازی این گزینه باعث می‌شوند دریافت پیام‌ها به تاخیر بیوفتد. تنها برای آزمایش آن را فعال کنید.</string>
<string name="send_stats_to_devs">ارسال آمار برای توسعه‌دهندگان دلتاچت</string>
<string name="stats_device_message">آیا می‌خواهید با فرستادن آمار استفادهٔ هفتگی به بهبود دلتاچت کمک کنید؟ 👈این‌جا را بزنید...👉</string>
<string name="stats_confirmation_dialog">می‌خواهید با فرستادن گزارش‌های ناشناس استفادهٔ هفتگی به بهبود دلتاچت کمک کنید؟</string>
<string name="stats_thanks">ممنون! هر زمان که خواستید می‌توانید فرستادن آمار‌ها را در «تنظیم‌ها - پیشرفته» غیرفعال کنید\n\n علاوه بر این، تمایل دارید ۵ دقیقه در یک تحقیق علمی راجع به امنیت دلتاچت شرکت کنید؟</string>
<string name="stats_disable_dialog">ارسال آمار‌ها قبلا فعال شدن است.\n\n می‌خواهید آن را غیر فعال کنید؟</string>
<string name="disable">غیرفعال کردن</string>
<string name="stats_keep_sending">ادامه‌دادن به ارسال</string>
<string name="stats_msg_body">این پیوست شامل آمار استفادهٔ ناشناس است که به ما کمک می‌کند دلتاچت را بهبود دهیم. برای اطلاعات بیش‌تر https://delta.chat/help#statssending را ببینید. متشکریم!</string>
<!-- Emoji picker and categories -->
<string name="emoji_search_results">نتایج جستجو</string>
<string name="emoji_not_found">شکلکی پیدا نشد</string>
@@ -829,6 +858,8 @@
<string name="autodel_server_ask">آیا می‌خواهید %1$d پیام را اکنون و تمام پیام‌های آیندهٔ «%2$s» را پاک کنید؟/n/n⚠️ این شامل رایانامه‌ها، رسانه و «پیام‌های ذخیره شده» در همه پوشه‌های کارساز(سرور) می‌شود/n/n⚠️ اگر می‌خواهید داده‌ها در کارساز باقی بمانند از این قابلیت استفاده نکنید/n/n⚠️ اگر به‌جز دلتاچت از دیگر نرم‌افزارهای رایانامه هم استفاده می‌کنید این قابلیت را به کار نگیرید.</string>
<!-- shown below enabled autodel_server-option, should be a summary of autodel_server_ask and remind about the impact -->
<string name="autodel_server_enabled_hint">این شامل رایانامه‌ها، رسانه‌ها و «پیام‌های ذخیره شده» در تمام پوشه‌های کارساز(سرور) می‌شود. اگر می‌خواهید داده‌ها را در کارساز نگه دارید از این قابلیت استفاده نکنید. اگر از دیگر نرم‌افزارهای رایانامه به‌جز دلتاچت استفاده می‌کنید هم از این قابلیت استفاده نکنید.</string>
<string name="autodel_server_warn_multi_device_title">فعال‌سازی حذف بلافاصله</string>
<string name="autodel_server_warn_multi_device">اگر حذف بلافاصله را فعال کنید، نمی‌توانید این نمایه را روی چند دستگاه داشته باشید.</string>
<string name="autodel_confirm">متوجه هستم، همه پیام‌ها حذف شوند</string>
<!-- "At once" in the meaning of "Immediately", without any intervening time. -->
<string name="autodel_at_once">یک بار بعد از بارگیری</string>
@@ -949,6 +980,7 @@
<string name="qrscan_fingerprint_label">اثر انگشت</string>
<string name="withdraw_verifycontact_explain">دیگران برای ارتباط با شما می‌توانند این کد کیوآر را اسکن کنند. \n\n می‌توان کد کیوآر را در اینجا غیر فعال کنید و با اسکن کردن مجدد، آن را فعال کنید. </string>
<string name="withdraw_verifygroup_explain">این کد کیوآر می‌تواند توسط دیگران و برای پیوستن به گروه «%1$s» اسکن شود.\n\n شما می‌تواند این کد کیوآر را در اینجا غیرفعال کنید و با اسکن کردن مجدد، آن را فعال نمایید. </string>
<string name="withdraw_joinbroadcast_explain">این کد کیوآر می‌تواند توسط بقیه برای پیوستن به کانال «%1$s» اسکن شود. \n\n می‌توانید آن را بازنشانی کنید تا کد یا پیوند دعوت فعلی از کار بیوفتد.</string>
<string name="withdraw_qr_code">غیرفعال کردن کد کیوآر</string>
<string name="revive_verifycontact_explain">این کد کیو‌آر بازنشانی شده است و دیگر فعال نیست.</string>
<string name="revive_qr_code">فعال کردن کد کیوآر</string>
@@ -959,6 +991,7 @@
<string name="qrshow_join_group_title">کد دعوت کیوآر</string>
<!-- This text is shown inside the "QR code card" with very limited space; please formulate the text as short as possible therefore. The placeholder will be replaced by the group name, eg. "Scan to join group \"Testing group\"" -->
<string name="qrshow_join_group_hint">برای عضو شدن در گروه\"%1$s\" این را اسکن کنید.</string>
<string name="qrshow_join_channel_hint">برای ملحق شدن به کانال «%1$s» اسکن کنید</string>
<string name="qrshow_join_contact_title">کد دعوت کیوآر</string>
<!-- This text is shown inside the "QR code card" with very limited space; please formulate the text as short as possible therefore. The placeholder will be replaced by the profile name eg. "Scan to chat with Alice" -->
<string name="qrshow_join_contact_hint">برای ارتباط با %1$s این را اسکن کنید.</string>
@@ -972,10 +1005,13 @@
<!-- first placeholder will be replaced by name of the inviter, second placeholder will be replaced by the name of the inviter. -->
<string name="secure_join_started">کاربر %1$s شما را دعوت کرده است تا عضو این گروه شوید.
در انتظار دستگاه %2$s برای پاسخگویی...</string>
<!-- first placeholder will be replaced by name of the inviter, second placeholder will be replaced by the name of the inviter. -->
<string name="secure_join_channel_started">%1$sشما را به این کانال دعوت کرده است.\n\n منتظر دستگاه %2$s برای پاسخ‌دادن هستیم...</string>
<!-- placeholder will be replaced by the name of the inviter. -->
<string name="secure_join_replies">کاربر %1$s پاسخ داد، در انتظار اضافه شدن به گروه...</string>
<string name="secure_join_wait">در حال برقراری ارتباط، لطفا صبر کنید...</string>
<string name="contact_verified">%1$s تأیید شد.</string>
<string name="contact_blocked">شما این مخاطب را مسدود کردید.</string>
<!-- Shown in contact profile. The placeholder will be replaced by the name of the contact that introduced the contact. -->
<string name="verified_by">احراز هویت شده توسط %1$s</string>
<string name="verified_by_you">تأیید شده توسط من</string>
+26 -6
View File
@@ -199,6 +199,7 @@
<string name="video">Vidéo</string>
<string name="documents">Documents</string>
<string name="contact">Contact</string>
<string name="bot">Robot</string>
<string name="camera">Caméra</string>
<!-- As in "start a video recording" or "take a photo"; eg. the description of the "shutter button" in cameras -->
<string name="capture">Enregistrer</string>
@@ -218,6 +219,8 @@
<string name="webxdc_app">Appli</string>
<!-- plural of "App"; used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_apps">Applis</string>
<string name="webxdc_store_url">URL du catalogue des apps </string>
<string name="webxdc_store_url_explain">Si elle est définit l\'URL sera utilisée à la place de l\'URL par défaut.</string>
<string name="webxdc_draft_hint">Appuyez \"Envoyer\" pour partager</string>
<string name="home">Accueil</string>
<string name="games">Jeux</string>
@@ -350,7 +353,7 @@
<string name="device_talk">Messages de l\'appareil</string>
<string name="device_talk_subtitle">Messages générés localement</string>
<string name="device_talk_explain">Les messages dans cette discussion sont générés localement par votre application Delta Chat. Cela vous permet de recevoir des informations concernant les mises à jour et problèmes rencontrés à l\'utilisation.</string>
<string name="device_talk_welcome_message2">Gardez contact !\n\n🙌 Cliquez sur « code QR » sur la page d\'accueil des deux appareils. Sélectionnez « Scanner le code QR » sur un des appareils et positionnez les deux appareils l\'un vers l\'autre\n\n🌍 Si vous n\'êtes pas dans la même pièce, vous pouvez scanner le code QR pendant un appel vidéo ou partager un lien d\'invitation à partir de « Scanner le code QR »\n\n Profitez ensuite de votre expérience de messagerie sur le plus grand réseau décentralisé du monde : les emails. Contrairement aux autres applications populaires, tout cela se fait sans contrôle centralisé, suivi ou revente de vos données, celles de vos amis, collègues ou famille à de grandes entreprises.</string>
<string name="device_talk_welcome_message2">Entrez en contact !\n\n🙌 Cliquez sur « code QR » sur la page d\'accueil des deux appareils. Sélectionnez « Scanner le code QR » sur un des appareils et positionnez les deux appareils l\'un vers l\'autre\n\n🌍 Si vous n\'êtes pas dans la même pièce, vous pouvez scanner le code QR pendant un appel vidéo ou partager un lien d\'invitation à partir de « Scanner le code QR »\n\n Profitez ensuite de votre expérience de messagerie sur le plus grand réseau décentralisé du monde : les emails. Contrairement aux autres applications populaires, tout cela se fait sans contrôle centralisé, suivi ou revente de vos données, celles de vos amis, collègues ou famille à de grandes entreprises.</string>
<string name="edit_contact">Mettre à jour le contact</string>
<!-- Verb "to pin", making something sticky, not a noun or abbreviation for "pin number". -->
<string name="pin_chat">Épingler la discussion</string>
@@ -366,7 +369,7 @@
<!-- Menu item beside an app/chat that adds an icon to the system's home screen. If the user taps that icon, the app/chat is opened directly. -->
<string name="add_to_home_screen">Ajouter à l\'écran d\'accueil</string>
<string name="donate">Faire un don</string>
<string name="donate_device_msg">❤️ On dirait que vous aimez bien Delta Chat!\n\nMerci de considérer une donation à Delta Chat afin d\'assurer qu\'il reste libre et gratuit pour tout le monde.\n\nBien que Delta Chat est gratuit à l\'utilisation et un logiciel libre, le développer coûte de l\'argent. Aidez-nous à garder Delta Chat indépendent et le rendre encore plus incroyable dans le future.\n\nhttps://delta.chat/donate</string>
<string name="donate_device_msg">❤️ On dirait que vous aimez bien Delta Chat!\n\nMerci de considérer une donation à Delta Chat afin d\'assurer qu\'il reste libre et gratuit pour tout le monde.\n\nBien que Delta Chat soit gratuit à l\'utilisation et un logiciel libre, le développer coûte de l\'argent. Aidez-nous à garder Delta Chat indépendant et le rendre encore plus incroyable dans le futur.\n\nhttps://delta.chat/donate</string>
<string name="mute_for_one_hour">Désactiver pour 1 heure</string>
<string name="mute_for_eight_hours">Désactiver pour 8 heures</string>
@@ -399,6 +402,9 @@
<string name="declined_call">Appel rejeté</string>
<string name="canceled_call">Appel annulé</string>
<string name="missed_call">Appel manqué</string>
<!-- the first placeholder will be replaced by a date, the second placeholder by duration, example: "Thu, 08:12 pm, 2 minutes" -->
<string name="call_date_and_duration">%1$s, %2$s</string>
<!-- get confirmations -->
<!-- confirmation for leaving groups or channels. If a subject is needed, "Are you sure you want to leave the chat?" would work as well -->
<string name="ask_leave_group">Êtes-vous sûr de vouloir quitter la discussion ?</string>
@@ -658,7 +664,7 @@
<string name="proxy_add_url_hint">Entrer le lien du proxy ici</string>
<string name="proxy_invalid">Proxy invalide ou non supporté</string>
<string name="proxy_list_header">Sauver les proxy</string>
<string name="proxy_delete">Effacer le proxy</string>
<string name="proxy_delete">Supprimer le proxy</string>
<string name="proxy_delete_explain">Voulez-vous vraiment supprimer « %1$s » ?</string>
<string name="proxy_use_proxy_confirm">Voulez-vous utilser le proxy \"%1$s\" ?</string>
<string name="proxy_share_explain">Vos amis peuvent ajouter ce proxy en scannant le code QR.</string>
@@ -672,6 +678,10 @@
<string name="edit_transport">Modifier le relais</string>
<!-- shown if a QR code was scanned that can be used as a relay -->
<string name="confirm_add_transport">Ajouter le relais ?</string>
<string name="invalid_transport_qr">Le QR code scanné ne renvoie pas vers un relai valide.</string>
<!-- placeholder will be replaced by a relay server name -->
<string name="confirm_remove_transport">Supprimer le relai \"%1$s\"\n\nVos contacts ne peuvent vous contacter que si vous les avez déjà contactés depuis un autre relai.\n\nDans le doute, supprimez le relai plus tard.</string>
<string name="login_certificate_checks">Vérification du certificat</string>
<string name="login_error_mail">Entrez une adresse email valide</string>
<string name="login_error_server">Entre un serveur / adresse IP valide</string>
@@ -947,7 +957,7 @@
<string name="ephemeral_timer_weeks_by_you">Vous avez réglé le temps d\'affichage des messages éphémères à %1$s semaines.</string>
<!-- %1$s will be replaced by the number of weeks (always >1) the timer is set to, %2$s will be replaced by name of the contact -->
<string name="ephemeral_timer_weeks_by_other">Minuterie des messages éphémères réglée à %1$s semaines par %2$s.</string>
<string name="chat_unencrypted_explanation">Les messages de cette conversation utilise le mail classique et ne sont pas chiffrés de bout-en-bout.</string>
<string name="chat_unencrypted_explanation">Les messages de cette conversation utilisent le mail classique et ne sont pas chiffrés de bout-en-bout.</string>
<string name="chat_protection_enabled_tap_to_learn_more">Les messages sont désormais garantis chiffrés de bout en bout. Appuyer pour en savoir plus.</string>
<string name="chat_protection_enabled_explanation">Il n\'est pas garanti que tous les messages de cette discussion sont chiffrés de bout en bout.\n\nLe chiffrement de bout en bout assure la confidentialité des messages échangés par les membres d\'une discussion. Même votre fournisseur de courriel ne peut pas les lire. </string>
<string name="invalid_unencrypted_tap_to_learn_more">⚠️ %1$s nécessite un chiffrement de bout en bout qui n\'est pas encore configuré sur cette discussion. Appuyez pour en savoir plus.</string>
@@ -997,7 +1007,7 @@
<string name="qrshow_join_contact_hint">Scanner pour discuter avec %1$s</string>
<string name="qrshow_join_contact_no_connection_toast">Pas de connexion Internet, ne peut pas effectuer la configuration du QR code.</string>
<string name="qraccount_ask_create_and_login">Créer une nouvelle adresse de courriel sur « %1$s » et se connecter ici ?</string>
<string name="set_name_and_avatar_explain">Choisissez un nom que vos contacts reconnaîtrons. Vous pouvez également définir une image pour votre profil.</string>
<string name="set_name_and_avatar_explain">Choisissez un nom que vos contacts reconnaîtront. Vous pouvez également définir une image pour votre profil.</string>
<string name="please_enter_name">Veuillez saisir un nom.</string>
<string name="qraccount_qr_code_cannot_be_used">Le code QR scanné ne peut pas être utilisé pour ouvrir un nouveau compte.</string>
<!-- the placeholder will be replaced by the address of the profile -->
@@ -1147,9 +1157,17 @@
<string name="notifications_avg_hours">Toutes les %1$d heures en moyenne</string>
<string name="last_check_at">Dernièrement vu à %1$s</string>
<string name="system_settings">Paramètres du système</string>
<!-- shown below the button "System Settings" on the notification screen. this is a hint about what can be edited on System Settings' Notification page - eg. the notification type (banner, lock screen, notification centre), sound, badges and so on. no need to be exhaustive here, it is only to give the user an idea. -->
<string name="system_settings_notify_explain_ios">Éditer le type, les badges, les prévisualisation et plus encore.</string>
<!-- iOS shortcut widget -->
<!-- use the same translation for "Shortcuts" as the system is using, often the term "Shortcut" stays untranslated; check eg. how the "Shortcuts" system app is called in your locale -->
<string name="shortcuts_widget_title">Raccourcis</string>
<!-- use the same translation for "Widget" as the system is using; often the term "Widget" stays untranslated -->
<string name="shortcuts_widget_description">Utiliser la fonction \"Ajouter au Widget\" de Delta Chat pour ajouter un élément.</string>
<!-- use the same translation for "Widget" as the system is using; often the term "Widget" stays untranslated -->
<string name="remove_from_widget">Retirer du Widget</string>
<!-- use the same translation for "Widget" as the system is using; often the term "Widget" stays untranslated -->
<string name="add_to_widget">Ajouter au Widget</string>
<!-- iOS permissions, copy from "deltachat-ios/Info.plist", which is used on missing translations in "deltachat-ios/LANG.lproj/InfoPlist.strings" -->
<string name="InfoPlist_NSCameraUsageDescription">Delta Chat utilise votre appareil photo pour prendre des photos et des vidéos et les envoyer, ainsi que pour scanner des QR codes.</string>
<string name="InfoPlist_NSLocationAlwaysAndWhenInUseUsageDescription">Delta Chat a besoin de votre autorisation pour partager votre position pour la durée d\'activation du partage de position.</string>
@@ -1164,11 +1182,13 @@
<string name="pref_background_notifications">Utiliser une connexion en arrière plan</string>
<string name="pref_background_notifications_explain">Requiert la désactivation des optimisations de battterie, utilisez cette option si les notifications sont en retard.</string>
<string name="pref_reliable_service">Forcer une connexion en arrière plan</string>
<string name="pref_reliable_service_explain">Entraîne une notification permanente.</string>
<string name="pref_background_notifications_rationale">Pour maintenir une connexion à votre serveur de courriel et recevoir les messages en arrière-plan, ignorez l\'optimisation de la batterie à l\'étape suivante.\n\nDelta Chat utilise peu de ressources et prend garde à ne pas vider votre batterie.</string>
<!-- disabling "Reliable service" will hide a the maybe annoying permanent-notification with the drawback that new-message-notifications get potentially unreliable -->
<string name="perm_enable_bg_reminder_title">Touchez ici pour recevoir des messages pendant que DeltaChat est en arrière-plan.</string>
<string name="perm_enable_bg_already_done">Vous avez déjà autorisé Delta Chat à recevoir des messages en arrière plan.\n\nSi vous ne les recevez toujours pas en arrière plan, vérifiez les paramètres de votre système.</string>
<!-- device messages for updates -->
<string name="update_2_0">Quoi de neuf?\n\n💯 Le chiffrement de bout-en-bout est maintenant fiable et péreint. 🔒Fini les cadenas !\n\n✉️ Les mails sans chiffrement de bout-en-bout sont maintenant marqués par une icône de mail \n\n😻 Nouvelle fenêtre de profil améliorée pour tous les contactsn\n🔲 Un nouveau button pour l\'accès rapide aux applis utilisés dans une discussion\n\n❤️ Merci de considérer à faire une donation afin que nous restions indépendents et continuons à améliorer Delta Chat: %1$s</string>
<string name="update_2_0">Quoi de neuf?\n\n💯 Le chiffrement de bout-en-bout est maintenant fiable et pérenne. 🔒Fini les cadenas !\n\n✉️ Les mails sans chiffrement de bout-en-bout sont maintenant marqués par une icône de mail \n\n😻 Nouvelle fenêtre de profil améliorée pour tous les contacts \n\n🔲 Nouveau bouton pour l\'accès rapide aux applis utilisées dans une discussion\n\n❤️ Merci d\'envisager de faire une donation afin que nous restions indépendants et continuions à améliorer Delta Chat: %1$s</string>
</resources>
+1
View File
@@ -1020,6 +1020,7 @@
<string name="secure_join_replies">%1$s ha risposto, in attesa di essere aggiunto al gruppo…</string>
<string name="secure_join_wait">Impostazione della crittografia end-to-end, attendere...</string>
<string name="contact_verified">%1$s introdotto.</string>
<string name="contact_blocked">Hai bloccato il contatto</string>
<!-- Shown in contact profile. The placeholder will be replaced by the name of the contact that introduced the contact. -->
<string name="verified_by">Introdotto da %1$s</string>
<string name="verified_by_you">Introdotto da me</string>
+50 -8
View File
@@ -5,6 +5,10 @@
<string name="ok">Gerai</string>
<string name="cancel">Atsisakyti</string>
<string name="clear_search">Išvalyti paiešką</string>
<!-- a noun, used on a button, short for "show link" -->
<string name="link">Nuoroda</string>
<!-- "scan" in the meaning of "scan QR code" -->
<string name="scan">Skenuoti</string>
<string name="yes">Taip</string>
<string name="no">Ne</string>
<string name="on">Įjungta</string>
@@ -47,6 +51,7 @@
<string name="ephemeral_messages_hint">Šie nustatymai taikomi visiems Delta Chat besinaudojantiems dalyviams. Vis dėlto, jie gali kopijuoti, įrašyti ir persiųsti žinutes arba naudoti kitas el. pašto kliento programas.</string>
<string name="save">Įrašyti</string>
<string name="media">Medija</string>
<string name="apps_and_media">Programėlės ir medija</string>
<string name="profile">Profilis</string>
<string name="all_profiles">Visi profiliai</string>
<string name="current_profile">Dabartinis profilis</string>
@@ -83,7 +88,7 @@
<string name="email_address">El. pašto adresas</string>
<string name="bad_email_address">Blogas el. pašto adresas.</string>
<string name="password">Slaptažodis</string>
<string name="existing_password">Esamas slaptažodis</string>
<string name="existing_password">Dabartinis slaptažodis</string>
<string name="now">Dabar</string>
<!-- Headline for destructive actions with no undo. Could also be "Caution" or "Warning". -->
<string name="danger">Dėmesio</string>
@@ -176,7 +181,7 @@
<!-- "App" is used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_app">Privati programėlė</string>
<!-- plural of "App"; used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_apps">Privačios programėlės</string>
<string name="webxdc_apps">Programėlės</string>
<string name="games">Žaidimai</string>
<string name="app_size">Dydis</string>
<!-- short for "Browse through the App Picker/Store/Catalogue"; could also be translated as "Discover" or "Search" -->
@@ -207,8 +212,11 @@
<string name="menu_new_classic_contact">Pridėti adresatą rankiniu būdu</string>
<string name="menu_new_chat">Naujas pokalbis</string>
<string name="menu_new_group">Nauja grupė</string>
<string name="clone_chat">Dubliuoti pokalbį</string>
<!-- deprecated -->
<string name="add_recipients">Pridėti gavėjus</string>
<!-- "New" as in "Create New Email"; shown together with "New Group" and "New Channel" -->
<string name="new_email">Naujas el. laiškas</string>
<!-- the "Subject" of an email, use the term common in classic email apps -->
<string name="subject">Tema</string>
<string name="menu_send">Siųsti</string>
@@ -227,6 +235,7 @@
<string name="menu_delete_location">Ištrinti šią vietą?</string>
<string name="menu_message_details">Išsamesnė žinutės informacija</string>
<string name="menu_copy_to_clipboard">Kopijuoti į iškarpinę</string>
<string name="share_invite_link_explain">Kiekvienas, turintis šią nuorodą, gali peržiūrėti jūsų profilį ar pradėti su jumis pokalbį. Bendrinkite ją tik su žmonėmis, kuriais pasitikite.</string>
<string name="menu_copy_selection_to_clipboard">Kopijuoti žymėjimą</string>
<string name="menu_copy_link_to_clipboard">Kopijuoti nuorodą</string>
<string name="menu_copy_text_to_clipboard">Kopijuoti tekstą</string>
@@ -241,6 +250,7 @@
<string name="menu_export_attachment">Eksportuoti priedą</string>
<string name="menu_export_attachments">Eksportuoti priedus</string>
<string name="menu_all_media">Visa medija</string>
<string name="all_apps_and_media">Visos programėlės ir medija</string>
<!-- Command to jump to the original message corresponding to a gallery image or document -->
<string name="show_in_chat">Rodyti pokalbyje</string>
<string name="menu_share">Bendrinti</string>
@@ -255,7 +265,7 @@
<string name="menu_scroll_to_top">Slinkti į viršų</string>
<string name="menu_help">Žinynas</string>
<!-- use the same term for "Apps" as elsewhere -->
<string name="what_is_webxdc">Kas yra „Webxdc“?</string>
<string name="what_is_webxdc">Apie programėles</string>
<string name="privacy_policy">Privatumo politika</string>
<string name="menu_select_all">Žymėti viską</string>
<string name="menu_edit_name">Taisyti vardą</string>
@@ -360,6 +370,7 @@
<string name="chat_share_with_title">Bendrinti su</string>
<string name="chat_input_placeholder">Žinutė</string>
<string name="chat_archived_label">Archyvuotas</string>
<string name="chat_request_label">Užklausa</string>
<string name="chat_no_messages">Žinučių nėra.</string>
<string name="chat_self_talk_subtitle">Žinutės, kurias siunčiu asmeniškai sau</string>
<string name="archive_empty_hint">Jei archyvuosite pokalbius, jie bus rodomi čia.</string>
@@ -406,7 +417,6 @@
<string name="tab_image_empty_hint">Čia bus rodomi šiame pokalbyje bendrinti paveikslai.</string>
<string name="tab_video_empty_hint">Čia bus rodomi šiame pokalbyje bendrinti vaizdo įrašai.</string>
<string name="tab_audio_empty_hint">Čia bus rodomi šiame pokalbyje bendrinti garso įrašų failai ir balso žinutės.</string>
<string name="tab_webxdc_empty_hint">Čia bus rodomos šiame pokalbyje bendrintos privačios programėlės.</string>
<string name="tab_all_media_empty_hint">Čia bus rodoma bet kuriame pokalbyje bendrinama medija.</string>
<string name="media_preview">Medijos peržiūra</string>
<string name="send_message">Siųsti žinutę</string>
@@ -415,8 +425,15 @@
<!-- "Second Device" can also be translated as "Another Device", if that is catchier in the destination language. However, make sure to use the term consistently. -->
<string name="multidevice_title">Pridėti antrąjį įrenginį</string>
<string name="multidevice_same_network_hint">Įsitikinkite, kad abu įrenginiai yra prisijungę prie to pačio belaidžio (Wi-Fi) arba laidinio tinklo</string>
<string name="multidevice_this_creates_a_qr_code">Tai sukuria QR kodą, kurį antrasis įrenginys gali nuskenuoti, kad nukopijuotų profilį.\n\nĮsitikinkite, kad joks pašalinis stebėtojas ar kamera nematys lango, kuris bus parodytas paspaudus mygtuką.</string>
<string name="multidevice_install_dc_on_other_device">Įsidiekite Delta Chat kitame savo įrenginyje (https://get.delta.chat)</string>
<!-- "I Already Have a Profile / Add as Second Device” should be the same text as defined by the keys onboarding_alternative_logins and multidevice_receiver_title -->
<string name="multidevice_tap_scan_on_other_device">Paleiskite Delta Chat, bakstelėkite „Aš jau turiu profilį / Pridėti kaip antrąjį įrenginį“ ir nuskenuokite čia rodomą kodą</string>
<!-- Shown inside a "QR code card" with very limited space; please formulate the text as short as possible therefore. The placeholder will be replaced by the profile name eg. "Scan to set up second device for Alice" -->
<string name="multidevice_qr_subtitle">Nuskenuokite norėdami nustatyti antrąjį įrenginį, skirtą %1$s</string>
<string name="multidevice_receiver_title">Pridėti kaip antrąjį įrenginį</string>
<string name="multidevice_open_settings_on_other_device">Pirmajame įrenginyje pereikite į „Nustatymai / Pridėti antrąjį įrenginį“ ir nuskenuokite ten rodomą kodą</string>
<string name="multidevice_abort">Nutraukti antrojo įrenginio nustatymą?</string>
<!-- Shown beside progress bar, stay short -->
<string name="preparing_account">Ruošiama paskyra…</string>
<string name="troubleshooting">Nesklandumų šalinimas</string>
@@ -426,6 +443,8 @@
<string name="incoming_messages">Gaunamosios žinutės</string>
<!-- Headline for the "Outbox" eg. in the "Connectivity" view -->
<string name="outgoing_messages">Siunčiamosios žinutės</string>
<!-- deprecated -->
<string name="storage_on_domain">Saugykla ties %1$s</string>
<string name="connectivity">Jungiamumas</string>
<!-- Shown in the title bar if the app is "Not connected"; prefer short strings. -->
<string name="connectivity_not_connected">Neprisijungta</string>
@@ -437,6 +456,7 @@
<string name="connectivity_connected">Prisijungta</string>
<string name="sending">Siunčiama…</string>
<string name="last_msg_sent_successfully">Paskutinė žinutė sėkmingai išsiųsta.</string>
<string name="not_supported_by_provider">Jūsų teikėjas nepalaiko šios ypatybės.</string>
<!-- Subtitle in quota context of "Connectivity" view. Should be be plural always, no number is prefixed. -->
<string name="messages">Žinutės</string>
<!-- welcome and login -->
@@ -444,14 +464,21 @@
<string name="onboarding_create_instant_account">Sukurti naują profilį</string>
<!-- Secondary button on the welcome screen, allows to "Add as Second Device", "Restore from Backup" -->
<string name="onboarding_alternative_logins">Aš jau turiu profilį</string>
<!-- This is a button and a title, allowing to use existing, classic email, setting ports, passwords and so on -->
<string name="manual_account_setup_option">Naudoti klasikinį el. paštą kaip retransliavimą</string>
<!-- Instant onboarding title (there is not more to do than to set name and avatar) -->
<string name="instant_onboarding_title">Jūsų profilis</string>
<!-- The placeholder will be replaced by the default onboarding server -->
<string name="instant_onboarding_agree_default2">%1$s privatumo politika</string>
<!-- Confirmation button on the instant onboarding screen -->
<string name="instant_onboarding_create">Sutinku ir sukurti profilį</string>
<!-- Secondary, link-like button to open a page with other possible instances -->
<string name="instant_onboarding_show_more_instances">Naudoti kitą serverį</string>
<string name="instant_onboarding_other_server">Išvardyti retransliavimo serverius</string>
<string name="welcome_chat_over_email">Saugūs decentralizuoti pokalbiai</string>
<string name="scan_invitation_code">Skenuoti pakvietimo kodą</string>
<string name="login_title">Prisijungti</string>
<string name="login_advanced_hint">Šis prisijungimas skirtas patyrusiems naudotojams:\n\n• Nenaudokite adreso, kurį naudojate kitoje programoje.\n\n• Klasikinis el. pašto serveris leidžia pokalbius be ištisinio šifravimo, pažymėtus voko piktograma.</string>
<string name="login_inbox">Gauta</string>
<string name="login_imap_login">IMAP prisijungimo vardas</string>
<string name="login_imap_server">IMAP serveris</string>
@@ -468,10 +495,13 @@
<string name="proxy_use_proxy">Naudoti įgaliotąjį serverį</string>
<string name="proxy_add">Pridėti įgaliotąjį serverį</string>
<string name="proxy_add_explain">Palaikomi įgaliotųjų serverių tipai: HTTP(S), SOCKS5 ir Shadowsocks.</string>
<string name="proxy_add_url_hint">Įveskite čia įgaliotojo serverio nuorodą</string>
<string name="proxy_list_header">Įrašyti įgaliotieji serveriai</string>
<string name="proxy_delete">Ištrinti įgaliotąjį serverį</string>
<string name="proxy_delete_explain">Ar tikrai norite ištrinti „%1$s“?</string>
<string name="proxy_use_proxy_confirm">Ar norite naudoti įgaliotąjį serverį „%1$s“?</string>
<!-- "Relay" refers to the relay servers used; please chose a common term in your language, no need to be literal. If in doubt, stay with "Relay" -->
<string name="transports">Retransliavimai</string>
<string name="login_certificate_checks">Liudijimų patikrinimai</string>
<string name="login_error_mail">Įveskite teisingą el. pašto adresą.</string>
<string name="login_error_server">Įveskite teisingą serverį / IP adresą</string>
@@ -523,6 +553,8 @@
<string name="pref_screen_security_explain">Prašyti blokuoti ekrano kopijas paskiausiųjų sąraše ir programėlės viduje</string>
<string name="pref_screen_security_please_restart_hint">Norėdami pritaikyti ekrano saugumo nustatymą, paleiskite programėlę iš naujo.</string>
<string name="pref_notifications">Pranešimai</string>
<string name="pref_mention_notifications">Paminėjimai</string>
<string name="pref_mention_notifications_explain">Grupėse, kuriose išjungti pranešimai, pranešti apie jums adresuotas žinutes, tokias kaip atsakymai ar reakcijos</string>
<string name="pref_notifications_show">Rodyti</string>
<string name="pref_notifications_priority">Pirmenybė</string>
<string name="pref_notifications_explain">Įjungti sistemos pranešimus apie naujas žinutes</string>
@@ -558,7 +590,8 @@
<string name="pref_other">Kita</string>
<string name="pref_backup">Atsarginė kopija</string>
<string name="pref_backup_explain">Padaro atsarginę pokalbių kopiją į išorinę saugyklą</string>
<string name="pref_backup_export_explain">Atsarginė kopija padeda jums nusistatyti naują diegimą šiame ar kitame įrenginyje.\n\nAtsarginėje kopijoje bus visos žinutės, adresatai ir pokalbiai bei jūsų ištisinė Autocrypt sąranka. Atsarginės kopijos failą laikykite saugioje vietoje arba kaip įmanoma greičiau jį ištrinkite.</string>
<string name="pref_backup_export_explain">Atsarginė kopija padeda jums nusistatyti naują diegimą šiame ar kitame įrenginyje.\n\nAtsarginėje kopijoje bus visos žinutės, adresatai ir pokalbiai bei jūsų ištisinio perdavimo „Autocrypt sąranka. Atsarginės kopijos failą laikykite saugioje vietoje arba kaip įmanoma greičiau jį ištrinkite.</string>
<string name="pref_backup_export_this">Eksportuoti šį profilį</string>
<!-- the placeholder will be replaced by the number of profiles to export; the number is always larger than 1 -->
<string name="pref_backup_export_all">Eksportuoti visas %1$d paskyras</string>
<string name="pref_backup_export_start_button">Pradėti daryti atsarginę kopiją</string>
@@ -568,6 +601,8 @@
<string name="pref_background_btn_default">Naudoti numatytąjį paveikslą</string>
<string name="pref_background_btn_gallery">Pasirinkti iš galerijos</string>
<string name="pref_imap_folder_warn_disable_defaults">Jeigu išjungiate šį parametrą, įsitikinkite, kad jūsų serveris ir kiti klientai yra sukonfigūruoti atitinkamai.\n\nPriešingu atveju, tai gali iš viso neveikti.</string>
<!-- No need to be literal here, you can also use "Use Multiple Devices", "Support Multiple Devices" or other fitting terms. However, it should fit to the wording or your language at https://delta.chat/help -->
<string name="pref_multidevice">Kelių įrenginių veiksena</string>
<string name="pref_auto_folder_moves">Automatiniai perkėlimai į DeltaChat aplanką</string>
<string name="pref_show_emails">Rodyti klasikinius el. laiškus</string>
<string name="pref_show_emails_no">Ne, tik pokalbius</string>
@@ -579,7 +614,7 @@
<string name="pref_background_custom_image">Tinkintas paveikslas</string>
<string name="pref_background_custom_color">Tinkinta spalva</string>
<string name="export_aborted">Eksportavimas nutrauktas.</string>
<string name="auto_download_messages">Automatinis žinučių atsisiuntimas</string>
<string name="auto_download_messages">Automatinis žinučių atsiuntimas</string>
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB -->
<string name="up_to_x">Iki %1$s</string>
<string name="up_to_x_most_worse_quality_images">Iki %1$s, iš esmės, blogos kokybės paveikslus</string>
@@ -587,7 +622,9 @@
<string name="download_failed">Atsiuntimas nepavyko</string>
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
<string name="n_bytes_message">%1$s žinutė</string>
<string name="profile_image_delete">Ištrinti profilio paveikslą</string>
<string name="profile_image_select">Pasirinkti profilio nuotrauką</string>
<string name="select_your_new_profile_image">Pasirinkite naują profilio nuotrauką</string>
<string name="profile_image_delete">Ištrinti profilio nuotrauką</string>
<string name="pref_show_tray_icon">Rodyti dėklo piktogramą</string>
<string name="pref_edit_profile">Taisyti profilį</string>
<string name="send_stats_to_devs">Siųsti statistiką Delta Chat plėtotojams</string>
@@ -671,6 +708,9 @@
<string name="ephemeral_timer_weeks_by_you">Jūs nustatėte išnykstančių žinučių laikmatį į %1$s sav.</string>
<!-- %1$s will be replaced by the number of weeks (always >1) the timer is set to, %2$s will be replaced by name of the contact -->
<string name="ephemeral_timer_weeks_by_other">%2$s nustatė išnykstančių žinučių laikmatį į %1$s sav. </string>
<string name="chat_unencrypted_explanation">Žinutės šiame pokalbyje naudoja klasikinius el. laiškus ir nėra šifruotos ištisinio perdavimo būdu.</string>
<string name="chat_protection_enabled_tap_to_learn_more">Žinutės yra šifruotos ištisinio perdavimo būdu. Bakstelėkite norėdami sužinoti daugiau.</string>
<string name="chat_protection_enabled_explanation">Visos žinutės šiame pokalbyje yra šifruotos ištisinio perdavimo būdu.\n\nIštisinis šifravimas išlaiko žinutes tarp jūsų ir jūsų pokalbio partnerių privačias. Netgi serveriai, teikėjai ar retransliavimai negali jų perskaityti.</string>
<string name="learn_more">Sužinoti daugiau</string>
<!-- %1%s will be replaced by date and time in some human-readable format -->
@@ -698,9 +738,10 @@
<string name="qrshow_join_group_hint">Nuskenuokite tai, norėdami prisijungti prie \"%1$s\" grupės.</string>
<string name="qrshow_join_contact_title">QR pakvietimo kodas</string>
<!-- This text is shown inside the "QR code card" with very limited space; please formulate the text as short as possible therefore. The placeholder will be replaced by the profile name eg. "Scan to chat with Alice" -->
<string name="qrshow_join_contact_hint">Nuskenuokite tai, norėdami užmegzti kontaktą su %1$s</string>
<string name="qrshow_join_contact_hint">Nuskenuokite norėdami pradėti pokalbį su %1$s</string>
<string name="qrshow_join_contact_no_connection_toast">Nėra interneto ryšio, nepavyksta atlikti QR kodo sąrankos.</string>
<string name="qraccount_ask_create_and_login">Sukurti naują el. pašto adresą ties „%1$s“ ir prisijungti ten?</string>
<string name="set_name_and_avatar_explain">Nusistatykite vardą, kurį jūsų adresatai atpažins. Taip pat galite nusistatyti profilio nuotrauką.</string>
<string name="please_enter_name">Įrašykite vardą.</string>
<string name="qraccount_qr_code_cannot_be_used">Nuskenuotas QR kodas negali būti naudojamas naujos paskyros nustatymui.</string>
<!-- the placeholder will be replaced by the address of the profile -->
@@ -797,6 +838,7 @@
<string name="InfoPlist_NSMicrophoneUsageDescription">Delta Chat naudoja mikrofoną, kad įrašytų ir siųstų balso žinutes bei vaizdo įrašus su garsu.</string>
<string name="InfoPlist_NSPhotoLibraryUsageDescription">Delta Chat leis jums pasirinkti, kurias nuotraukas iš savo kolekcijos išsiųsti.</string>
<string name="InfoPlist_NSPhotoLibraryAddUsageDescription">Delta Chat nori įrašyti paveikslus į jūsų nuotraukų kolekciją.</string>
<string name="pref_background_notifications">Naudoti foninį ryšį</string>
<string name="pref_background_notifications_rationale">Norėdami palaikyti ryšį su savo el. pašto serveriu ir gauti žinutes fone, kitame žingsnyje nepaisykite akumuliatoriaus optimizavimų (leiskite vykdyti programą fone).\n\nDelta Chat naudoja nedaug išteklių ir rūpinasi, kad neeikvotų jūsų akumuliatoriaus energijos.</string>
<!-- disabling "Reliable service" will hide a the maybe annoying permanent-notification with the drawback that new-message-notifications get potentially unreliable -->
<string name="perm_enable_bg_reminder_title">Bakstelėkite čia, norėdami gauti žinutes, kol Delta Chat veikia fone.</string>
+2 -2
View File
@@ -1088,8 +1088,8 @@
<string name="name_desktop">Naam</string>
<string name="select_group_image_desktop">Kies een groepsafbeelding</string>
<string name="export_backup_desktop">Reservekopie exporteren</string>
<string name="message_detail_sent_desktop">verstuurd</string>
<string name="message_detail_received_desktop">ontvangen</string>
<string name="message_detail_sent_desktop">Verstuurd</string>
<string name="message_detail_received_desktop">Ontvangen</string>
<string name="menu.view.developer.open.log.folder">Map met logbestanden openen</string>
<string name="menu.view.developer.open.current.log.file">Huidig logbestand openen</string>
<string name="explain_desktop_minimized_disabled_tray_pref" tools:ignore="TypographyDashes">Het systeemvakpictogram is uitgeschakeld omdat Delta Chat is gestart met de optie --minimized</string>
+3 -2
View File
@@ -1036,6 +1036,7 @@
<string name="secure_join_replies">Użytkownik %1$s odpowiedział, czekając na dodanie do grupy…</string>
<string name="secure_join_wait">Ustanawiam połączenie, proszę czekać…</string>
<string name="contact_verified">Kontakt %1$s zweryfikowany.</string>
<string name="contact_blocked">Zablokowano kontakt</string>
<!-- Shown in contact profile. The placeholder will be replaced by the name of the contact that introduced the contact. -->
<string name="verified_by">Kontakt zweryfikowany przez %1$s</string>
<string name="verified_by_you">Kontakt zweryfikowany przez ciebie</string>
@@ -1121,8 +1122,8 @@
<string name="name_desktop">Nazwa</string>
<string name="select_group_image_desktop">Wybierz obraz grupy</string>
<string name="export_backup_desktop">Eksport kopii zapasowej</string>
<string name="message_detail_sent_desktop">wysłane</string>
<string name="message_detail_received_desktop">odebrane</string>
<string name="message_detail_sent_desktop">Wysłana</string>
<string name="message_detail_received_desktop">Odebrana</string>
<string name="menu.view.developer.open.log.folder">Otwórz folder dziennika</string>
<string name="menu.view.developer.open.current.log.file">Otwórz bieżący plik dziennika</string>
<string name="explain_desktop_minimized_disabled_tray_pref" tools:ignore="TypographyDashes">Ikona w zasobniku nie może być wyłączona, ponieważ Delta Chat został uruchomiony z opcją --minimized.</string>
+124
View File
@@ -5,6 +5,10 @@
<string name="ok">OK</string>
<string name="cancel">Avbryt</string>
<string name="clear_search">Rensa sök</string>
<!-- a noun, used on a button, short for "show link" -->
<string name="link">Länk</string>
<!-- "scan" in the meaning of "scan QR code" -->
<string name="scan">Skanna</string>
<string name="yes">Ja</string>
<string name="no">Nej</string>
<string name="select">Välj</string>
@@ -24,29 +28,40 @@
<string name="join">Anslut</string>
<string name="rejoin">Återanslut</string>
<string name="delete">Ta bort</string>
<string name="delete_for_me">Ta bort för mig</string>
<string name="delete_for_everyone">Ta bort för alla</string>
<string name="info">Info</string>
<string name="update">Uppdatera</string>
<string name="emoji">Emoji</string>
<string name="attachment">Bilaga</string>
<!-- the placeholder will be replaced by the name of the image or file to attach -->
<string name="ask_attach">Vill du bifoga \"%1$s\"?</string>
<string name="back">Tillbaka</string>
<string name="close">Stäng</string>
<string name="close_window">Stäng fönster</string>
<string name="forward">Vidarebefordra</string>
<string name="create">Skapa</string>
<string name="reset">Återställ</string>
<string name="later">Senare</string>
<!-- "Resend" means "Sending the selected message(s) again to the same chat". The string is used in a menu and should be as short as possible. Resending may be needed after failures or to repost old messages to new members. -->
<string name="resend">Skicka igen</string>
<string name="edited">Redigerad</string>
<string name="edit_message">Redigera meddelande</string>
<!-- Verb "to archive", as in "put a chat in the archive", not a noun "The Archive". -->
<string name="archive">Arkivera</string>
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
<string name="unarchive">Avarkivera</string>
<string name="mute">Tyst</string>
<string name="muted">Tystad</string>
<string name="ephemeral_messages">Försvinnande meddelanden</string>
<string name="ephemeral_messages_hint">Dessa inställningar tillämpas för alla deltagare som använder Delta Chat. De kan dock kopiera, spara och vidarebefordra meddelanden eller använda andra e-postklienter.</string>
<string name="save">Spara</string>
<string name="chat">Chatt</string>
<string name="media">Media</string>
<string name="apps_and_media">Appar &amp; Media</string>
<string name="profile">Profil</string>
<string name="all_profiles">Alla profiler</string>
<string name="current_profile">Aktuell profil</string>
<string name="main_menu">Huvudmeny</string>
<string name="start_chat">Starta chatt</string>
<string name="show_full_message">Visa hela meddelandet...</string>
@@ -79,6 +94,8 @@
<string name="no_app_to_handle_data">Kan inte hitta någon app som hanterar denna typ av data.</string>
<string name="no_browser_installed">Ingen webbläsare installerad.</string>
<string name="file_not_found">Kunde inte hitta %1$s.</string>
<!-- the placeholder will be replaced by the name of a file -->
<string name="cannot_save_file">Kan inte spara %1$s.</string>
<string name="copied_to_clipboard">Kopierad till urklipp.</string>
<string name="contacts_headline">Kontakter</string>
<string name="email_address">E-postadress</string>
@@ -99,6 +116,16 @@
<!-- Refers to the time a contact was last seen. Shown below contact name in the profile. The placeholder will be replaced by a relative point in time as "3 minutes ago" (see https://momentjs.com for more examples and languages)-->
<string name="last_seen_relative">Sågs senast %1$s</string>
<string name="last_seen_unknown">Sågs senast: Okänt</string>
<!-- Shown in call duration. Avoid abbreviations, prefer full words ex. "N seconds". -->
<plurals name="n_seconds_ext">
<item quantity="one">%d sekund</item>
<item quantity="other">%d sekunder</item>
</plurals>
<!-- Shown in call duration. Avoid abbreviations, prefer full words ex. "N minutes". -->
<plurals name="n_minutes_ext">
<item quantity="one">%d minut</item>
<item quantity="other">%d minuter</item>
</plurals>
<!-- Shown beside messages that are "N minutes old". Prefer short strings, or well-known abbreviations. -->
<plurals name="n_minutes">
<item quantity="one">%d min</item>
@@ -122,6 +149,10 @@
<item quantity="one">%d meddelande</item>
<item quantity="other">%d meddelanden</item>
</plurals>
<plurals name="n_reactions">
<item quantity="one">%d reaktion</item>
<item quantity="other">%d reaktioner</item>
</plurals>
<plurals name="n_members">
<item quantity="one">%d medlem</item>
<item quantity="other">%d medlemmar</item>
@@ -135,6 +166,7 @@
<item quantity="one">%dmarkerade</item>
<item quantity="other">%d markerad</item>
</plurals>
<string name="selected_colon">Markerat:</string>
<string name="self">Jag</string>
<string name="draft">Utkast</string>
<string name="image">Bild</string>
@@ -168,10 +200,25 @@
<string name="images_and_videos">Bilder och videor</string>
<string name="file">Arkiv</string>
<string name="files">Filer</string>
<string name="files_attach_hint">Skicka originalfiler och okomprimerade bilder</string>
<!-- "Files" here means the "Files Selector App" or "Files Manager App" -->
<string name="choose_from_files">Välj från Filer</string>
<string name="choose_from_gallery">Välj från Galleri</string>
<!-- "App" is used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_app">App</string>
<!-- plural of "App"; used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_apps">Appar</string>
<string name="webxdc_store_url">URL till appväljare</string>
<string name="webxdc_store_url_explain">Om angivet, kommer URL:en att användas som appväljare istället för standardväljare</string>
<string name="webxdc_draft_hint">Tryck \"Skicka\" för att dela</string>
<string name="home">Hem</string>
<string name="games">Spel</string>
<string name="tools">Verktyg</string>
<string name="app_size">Storlek</string>
<string name="app_date_published">Publicerad</string>
<string name="add_to_chat">Lägg till i chatten</string>
<!-- short for "Browse through the App Picker/Store/Catalogue"; could also be translated as "Discover" or "Search" -->
<string name="browse">Bläddra</string>
<string name="unknown">Okänd</string>
<string name="green">Grön</string>
@@ -199,9 +246,24 @@
<string name="new_classic_contact_explain">Manuellt tillagda kontakter kan användas för att sms:a till klassisk e-post och är inte garanterat totalsträckskrypterat.</string>
<string name="menu_new_chat">Ny chatt</string>
<string name="menu_new_group">Ny grupp</string>
<!-- "Chat" is a verb here, "Message to" would also fit. the string might be used in the "New Chat" screen above the contact list -->
<string name="chat_with">Chatta med...</string>
<string name="clone_chat">Klona chatt</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channel">Kanal</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channels">Kanaler</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="new_channel">Ny kanal</string>
<!-- deprecated -->
<string name="add_recipients">Lägg till mottagare</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channel_name">Kanalnamn</string>
<string name="email">E-post</string>
<!-- "New" as in "Create New Email"; shown together with "New Group" and "New Channel" -->
<string name="new_email">Ny e-post</string>
<!-- the "Subject" of an email, use the term common in classic email apps -->
<string name="subject">Ämne</string>
<string name="menu_send">Skicka</string>
<string name="menu_toggle_keyboard">Byt emoji-tangentbord</string>
<string name="menu_edit_group">Redigera grupp</string>
@@ -211,6 +273,7 @@
<string name="menu_unarchive_chat">Avarkivera chatt</string>
<string name="menu_add_attachment">Lägg till bilaga</string>
<string name="menu_leave_group">Lämna grupp</string>
<string name="menu_leave_channel">Lämna kanalen</string>
<string name="menu_delete_chat">Ta bort chatt</string>
<!-- Command to delete all messages in a chat. The chat itself will not be deleted but will be empty afterwards, so make sure to be different from "Delete Chat" here. "Clear" is a verb here, "Empty Chat" would also be fine (eg. in German "Chat leeren") -->
<string name="clear_chat">Rensa chatt</string>
@@ -219,6 +282,7 @@
<string name="menu_message_details">Meddelandedetaljer</string>
<string name="menu_copy_to_clipboard">Kopiera till urklipp</string>
<string name="share_invite_link">Dela inbjudningslänk</string>
<string name="share_invite_link_explain">Vem som helst med den här länken kan se din profil och börja chatta med dig. Dela den bara med personer som du litar på.</string>
<string name="invite_friends">Bjud in vänner</string>
<!-- %1$s is replaced by the user's invitation link ("https://i.delta.chat/...") -->
<string name="invite_friends_text">Kontakta mig på Delta Chat:\n%1$s</string>
@@ -236,6 +300,7 @@
<string name="menu_export_attachment">Exportera bilaga</string>
<string name="menu_export_attachments">Exportera bilaga</string>
<string name="menu_all_media">All media</string>
<string name="all_apps_and_media">Alla appar &amp; media</string>
<!-- Command to jump to the original message corresponding to a gallery image or document -->
<string name="show_in_chat">Visa i chatten</string>
<string name="show_app_in_chat">Visa app i chatt</string>
@@ -257,6 +322,10 @@
<string name="select_chat">Välj chatt</string>
<string name="select_more">Välj flera</string>
<string name="menu_edit_name">Redigera namn</string>
<!-- The placeholder will be replaced by the name the contact gave themself -->
<string name="edit_name_explain">Ange ett chattnamn som kommer att visas för dig istället för \"%1$s\". Lämnas tomt om du vill använda kontaktens valda namn.</string>
<!-- The placeholder will be replaced by the name the contact gave themself -->
<string name="edit_name_placeholder">Chattnamn för \"%1$s\"</string>
<string name="menu_settings">Inställningar</string>
<string name="menu_advanced">Avancerat</string>
<string name="menu_view_profile">Visa profil</string>
@@ -266,6 +335,7 @@
<string name="menu_more_options">Mer alternativ</string>
<string name="menu_learn_spelling">Lär stavning</string>
<string name="jump_to_message">Hoppa till meddelande</string>
<string name="jump_to_original_message">Hoppa till ursprungligt meddelande</string>
<string name="copy_json">Kopiera JSON</string>
<string name="replace_draft">Ersätt utkast</string>
<string name="title_share_location">Dela plats med alla gruppmedlemmar</string>
@@ -288,6 +358,8 @@
<!-- Menu item beside an app/chat that adds an icon to the system's home screen. If the user taps that icon, the app/chat is opened directly. -->
<string name="add_to_home_screen">Lägg till på hemskärmen</string>
<string name="donate">Donera</string>
<string name="donate_device_msg">❤️ Det verkar som att du gillar Delta Chat!\n\nVänligen överväg att donera för att hjälpa till att säkerställa att Delta Chat förblir gratis för alla.\n\nÄven om Delta Chat är gratis att använda och öppen källkod, kostar utvecklingen pengar. Hjälp oss att hålla Delta Chat oberoende och göra det ännu bättre i framtiden.\n\nhttps://delta.chat/donate</string>
<string name="mute_for_one_hour">Tysta 1 timma</string>
<string name="mute_for_eight_hours">Tysta 8 timmar</string>
<string name="mute_for_one_day">Tysta 1 dag</string>
@@ -306,6 +378,29 @@
</plurals>
<string name="file_saved_to">Filen är sparad i \"%1$s\".</string>
<!-- the action "to call someone", used as a tooltip for the "phone" icon. not: "the call" -->
<string name="start_call">Ring upp</string>
<!-- the action "to answer" or to "accept" or to "pick up" a call. not: "the answer" -->
<string name="answer_call">Svara</string>
<!-- the action "to decline" a call, not: "the decline" -->
<string name="end_call">Avböj</string>
<string name="outgoing_call">Utgående samtal</string>
<string name="incoming_call">Inkommande samtal</string>
<string name="declined_call">Nekade samtal</string>
<string name="canceled_call">Avbrutna samtal</string>
<string name="missed_call">Missade samtal</string>
<!-- the first placeholder will be replaced by a date, the second placeholder by duration, example: "Thu, 08:12 pm, 2 minutes" -->
<string name="call_date_and_duration">%1$s, %2$s</string>
<!-- get confirmations -->
<!-- confirmation for leaving groups or channels. If a subject is needed, "Are you sure you want to leave the chat?" would work as well -->
<string name="ask_leave_group">Vill du verkligen lämna?</string>
<plurals name="ask_delete_chat">
<item quantity="one">Vill du ta bort %d chatt?</item>
<item quantity="other">Vill du ta bort %d chattar?</item>
</plurals>
<string name="ask_delete_named_chat">Vill du ta bort chatten\"%1$s\"?</string>
<string name="ask_delete_message">Vill du ta bort detta meddelande?</string>
<plurals name="ask_delete_messages">
<item quantity="one">Vill du ta bort %d meddelande?</item>
<item quantity="other">Vill du ta bort %d meddelanden?</item>
@@ -320,6 +415,8 @@
<string name="ask_start_chat_with">Chatta med %1$s ?</string>
<!-- %1$s is replaced by a comma-separated list of names -->
<string name="ask_remove_members">Ta bort %1$s från gruppen?</string>
<!-- %1$s is replaced by a comma-separated list of names -->
<string name="ask_remove_from_channel">Vill du ta bort %1$s från kanalen?</string>
<string name="open_url_confirmation">Vill du öppna denna länk?</string>
@@ -347,6 +444,8 @@
</plurals>
<!-- The placeholder will be replaced by the name of the recipient in a one-to-one chat. -->
<string name="chat_new_one_to_one_hint">Skicka ett meddelande till %1$s.</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="chat_new_channel_hint">Kanaler är ett en-till-många-verktyg för att skicka ut dina meddelanden.</string>
<string name="chat_new_group_hint">Komponera det första meddelandet, så att andra kan svara inom denna grupp.\n\n• Det är okej om inte alla medlemmar använder Delta Chat.\n\n• Det kan ta en stund innan det första meddelandet levereras.</string>
<string name="chat_record_slide_to_cancel">Svep för att avbryta</string>
<string name="chat_record_explain">Tryck och håll ner för att spela in ett röstmeddelande, släpp för att skicka.</string>
@@ -356,6 +455,7 @@
<string name="chat_input_placeholder">Meddelande</string>
<string name="chat_archived_label">Arkiverat</string>
<string name="chat_request_label">Efterfråga</string>
<string name="chat_request_label_explain">Inkludera förfrågningar i räknare och meddelanden</string>
<string name="chat_no_messages">Inga meddelanden.</string>
<string name="chat_self_talk_subtitle">Meddelanden jag skickat till mig själv</string>
<string name="archive_empty_hint">Arkiverde chattar kommer att visas här.</string>
@@ -384,6 +484,9 @@
<string name="send_file_to">Skicka \"%1$s\" till...</string>
<!-- title shown above a list contacts where one should be selected (eg. when a webxdc attempts to send a message to a chat) -->
<string name="send_message_to">Skicka meddelande till...</string>
<string name="enable_realtime">Realtidsappar</string>
<string name="enable_realtime_explain">Aktivera realtidsanslutningar för appar som är kopplade till chattar. Om detta är aktiverat kan chattpartners kanske få reda på din IP-adress när du startar en app.</string>
<!-- map -->
<string name="filter_map_on_time">Visa platser inom tidsramen</string>
<string name="show_location_traces">Visa spår</string>
@@ -406,6 +509,10 @@
<string name="search_files">Sök filer</string>
<string name="search_explain">Sök efter chattar, kontakter och meddelanden</string>
<string name="search_no_result_for_x">Inga resultat hittades för \" %s \"</string>
<!-- Adjective, as in "Show Unread Messages" -->
<string name="search_unread">Olästa</string>
<!-- create/edit groups, contact/group profile -->
<string name="group_name">Gruppnamn</string>
<string name="group_avatar">Gruppbild</string>
@@ -417,6 +524,8 @@
<string name="group_self_not_in_group">Du måste vara medlem i gruppen för att utföra denna åtgärd.</string>
<string name="profile_encryption">Kryptering</string>
<string name="profile_shared_chats">Delade chattar</string>
<!-- Separator between the list of actual members and past members -->
<string name="past_members">Tidigare medlemmar</string>
<string name="tab_contact">Kontakt</string>
<string name="tab_group">Grupp</string>
<string name="tab_gallery">Galleri</string>
@@ -430,6 +539,7 @@
<string name="tab_audio_empty_hint">Ljudfiler och röstmeddelanden delade i denna chatt kommer att visas här.</string>
<string name="tab_webxdc_empty_hint">Appar delade i denna chatt kommer att visas här.</string>
<string name="tab_all_media_empty_hint">Media delad i någon chatt kommer att visas här.</string>
<string name="all_files_empty_hint">Dokument och andra filer som bifogas en chatt visas här.</string>
<string name="all_apps_empty_hint">Appar som tas emot eller skickas i en chatt visas här.</string>
<string name="media_preview">Förhandsvisning av media</string>
<!-- option to show images in the gallery with the correct width/height aspect (instead of square); other gallery apps may be a source of inspiration for translation :) -->
@@ -451,6 +561,7 @@
<string name="multidevice_receiver_title">Lägg till som andra enhet</string>
<string name="multidevice_open_settings_on_other_device">På den första enheten går du till \"Inställningar / Lägg till en andra enhet\" och skannar koden som visas där</string>
<string name="multidevice_receiver_scanning_ask">Vill du kopiera profilen från den andra enheten till den här enheten?</string>
<string name="multidevice_receiver_needs_update">Profilen du vill importera är från en nyare version av Delta Chat. \n\nFör att fortsätta konfigurera den andra enheten måste du uppdatera den här enheten till den senaste versionen av Delta Chat.</string>
<string name="multidevice_abort">Vill du avbryta konfigurationen av den andra enheten?</string>
<string name="multidevice_abort_will_invalidate_copied_qr">Detta kommer att ogiltigförklara QR-koden som kopierats till urklipp.</string>
<string name="multidevice_transfer_done_devicemsg">️ Profilen överförd till din andra enhet.</string>
@@ -515,6 +626,7 @@
<string name="welcome_chat_over_email">Säker decentraliserad chatt</string>
<string name="scan_invitation_code">Skanna inbjudningskod</string>
<string name="login_title">Logga in</string>
<string name="login_advanced_hint">Denna inloggning är för avancerade användare:\n\n- Använd inte en adress som du använder i en annan app.\n\n- Klassisk e-postserver tillåter chattar utan totalsträckskryptering, markerad med en e-postikon.</string>
<string name="login_inbox">Inkorg</string>
<string name="login_imap_login">IMAP inloggningsnamn</string>
<string name="login_imap_server">IMAP server</string>
@@ -526,6 +638,18 @@
<string name="login_smtp_server">SMTP server</string>
<string name="login_smtp_port">SMTP port</string>
<string name="login_smtp_security">SMTP säkerhet</string>
<!-- the word "Proxy" might be left untranslated unless the destination language has a well-known term for a "Proxy Server", acting intermediary between the app and the server -->
<string name="proxy_settings">Proxy</string>
<string name="proxy_use_proxy">Använd proxy</string>
<string name="proxy_add">Lägg till proxy</string>
<string name="proxy_add_explain">Proxytyper som stöds: HTTP(S), SOCKS5 och Shadowsocks.</string>
<string name="proxy_add_url_hint">Ange proxylänk här</string>
<string name="proxy_invalid">Ogiltig proxy eller stöd saknas</string>
<string name="proxy_list_header">Sparade proxyer</string>
<string name="proxy_delete">Ta bort proxy</string>
<string name="proxy_delete_explain">Vill du verkligen ta bort \"%1$s\"?</string>
<string name="proxy_use_proxy_confirm">Vill du använda proxyn \"%1$s\"?</string>
<string name="proxy_share_explain">Dina vänner kan lägga till denna proxy genom att skanna QR-koden.</string>
<string name="login_certificate_checks">Certifikatkontroller</string>
<string name="login_error_mail">Ange en giltig e-postadress</string>
<string name="login_error_server">Ange en giltig server/IP-adress</string>
+28 -21
View File
@@ -420,18 +420,18 @@
<!-- confirmation for leaving groups or channels. If a subject is needed, "Are you sure you want to leave the chat?" would work as well -->
<string name="ask_leave_group">Ви впевнені, що хочете піти?</string>
<plurals name="ask_delete_chat">
<item quantity="one">¿Видалити %d чат на всіх Ваших пристроях?</item>
<item quantity="few">¿Видалити %d чати на всіх Ваших пристроях?</item>
<item quantity="many">¿Видалити %d чатів на всіх Ваших пристроях?</item>
<item quantity="other">¿Видалити %d чатів на всіх Ваших пристроях?</item>
<item quantity="one">Видалити %d чат?</item>
<item quantity="few">Видалити %d чати?</item>
<item quantity="many">Видалити %d чатів?</item>
<item quantity="other">Видалити %d чатів?</item>
</plurals>
<string name="ask_delete_named_chat">Видалити чат \"%1$s\" на всіх ваших пристроях?</string>
<string name="ask_delete_message">Видалити повідомлення на всіх ваших пристроях?</string>
<string name="ask_delete_message">Видалити це повідомлення?</string>
<plurals name="ask_delete_messages">
<item quantity="one">¿Видалити %d повідомлення на всіх Ваших пристроях?</item>
<item quantity="few">¿Видалити %d повідомлення на всіх Ваших пристроях?</item>
<item quantity="many">¿Видалити %d повідомлень на всіх Ваших пристроях?</item>
<item quantity="other">¿Видалити %d повідомлень на всіх Ваших пристроях?</item>
<item quantity="one">Видалити %d повідомлення?</item>
<item quantity="few">Видалити %d повідомлення?</item>
<item quantity="many">Видалити %d повідомлень?</item>
<item quantity="other">Видалити %d повідомлень?</item>
</plurals>
<string name="ask_forward">Переслати повідомлення %1$s?</string>
<string name="ask_forward_multiple">Переслати повідомлення в %1$d?</string>
@@ -439,7 +439,7 @@
<string name="ask_block_contact">Заблокувати цей контакт?\n\nПрямі повідомлення або групи, створені заблокованими контактами, не відображатимуться.\n\nІнші групи із заблокованими контактами все одно показуватимуть свої повідомлення.</string>
<string name="ask_unblock_contact">Розблокувати цей контакт?</string>
<string name="ask_delete_contacts">Видалити контакти?\n\nКонтакти з поточними чатами неможливо видалити остаточно.</string>
<string name="ask_delete_contact">Видалити %1$s?\n\nКонтакти з поточними чатами не можна видалити назавжди.</string>
<string name="ask_delete_contact">Видалити контакт %1$s?\n\nКонтакти з поточними чатами не можна видалити назавжди.</string>
<string name="ask_start_chat_with">Створити чат з %1$s?</string>
<!-- %1$s is replaced by a comma-separated list of names -->
<string name="ask_remove_members">Видалити %1$s із групи?</string>
@@ -593,7 +593,7 @@
<!-- Shown inside a "QR code card" with very limited space; please formulate the text as short as possible therefore. The placeholder will be replaced by the profile name eg. "Scan to set up second device for Alice" -->
<string name="multidevice_qr_subtitle">Скануйте, щоб налаштувати другий пристрій %1$s</string>
<string name="multidevice_receiver_title">Додати як другий пристрій</string>
<string name="multidevice_open_settings_on_other_device">На першому пристрої перейдіть до «Налаштування / Додати другий пристрій» і відскануйте показаний там код</string>
<string name="multidevice_open_settings_on_other_device">На першому пристрої запустіть Delta Chat, перейдіть до «Налаштування / Додати другий пристрій» і відскануйте зображений код</string>
<string name="multidevice_receiver_scanning_ask">Скопіювати профіль з іншого пристрою на цей?</string>
<string name="multidevice_receiver_needs_update">Профіль, який ви хочете імпортувати, належить до новішої версії Delta Chat.\n\nЩоб продовжити налаштування другого пристрою, будь ласка, оновіть його до останньої версії Delta Chat.</string>
<string name="multidevice_abort">Скасувати налаштування другого пристрою?</string>
@@ -636,7 +636,7 @@
<!-- Secondary button on the welcome screen, allows to "Add as Second Device", "Restore from Backup" -->
<string name="onboarding_alternative_logins">У мене вже є профіль</string>
<!-- This is a button and a title, allowing to use existing, classic email, setting ports, passwords and so on -->
<string name="manual_account_setup_option">Транспорт через класичну електронну пошту</string>
<string name="manual_account_setup_option">Використовувати класичну пошту як транспортний сервер</string>
<!-- Instant onboarding title (there is not more to do than to set name and avatar) -->
<string name="instant_onboarding_title">Ваш профіль</string>
<!-- The placeholder will be replaced by the default onboarding server -->
@@ -686,7 +686,7 @@
<string name="proxy_share_explain">Ваші друзі можуть додати цей проксі, відсканувавши QR-код.</string>
<string name="proxy_share_link">Поділитися посиланням</string>
<string name="proxy_enabled">Проксі увімкнено</string>
<string name="proxy_enabled_hint">Ви використовуєте проксі-сервер. Якщо у вас проблеми з підключенням, спробуйте інший проксі.</string>
<string name="proxy_enabled_hint">Ви використовуєте проксі-сервер. Якщо у вас проблеми зі зʼєднанням, спробуйте інший проксі.</string>
<!-- "Relay" refers to the relay servers used; please chose a common term in your language, no need to be literal. If in doubt, stay with "Relay" -->
<string name="transports">Транспортні сервери</string>
<string name="add_transport">Додати транспортний сервер</string>
@@ -705,7 +705,7 @@
<string name="login_error_required_fields">Вкажіть дійсну адресу електронної пошти та пароль</string>
<string name="import_backup_title">Відновити з резервної копії</string>
<string name="import_backup_ask">Знайдена резервна копія \"%1$s\".\n\nВідновити з неї всі дані й параметри?</string>
<string name="import_backup_no_backup_found">Резервних копій не знайдено.\n\nПомістіть резервну копію в папку \"%1$s\" та спробуйте знову запустити відновлення.</string>
<string name="import_backup_no_backup_found">Жодної резервної копії не знайдено. \n\nПомістіть резервну копію в теку \"%1$s\" та спробуйте знову.</string>
<!-- %1$s will be replaced by the failing address -->
<string name="login_error_cannot_login">Не вдалося увійти як \"%1$s\". Будь ласка, перевірте правильність адреси електронної пошти та паролю.</string>
<!-- TLS certificate checks -->
@@ -812,7 +812,7 @@
<string name="pref_imap_folder_warn_disable_defaults">Якщо ви змінюєте цей параметр, переконайтеся, що Ваш сервер та інші Ваші клієнти налаштовані відповідним чином.\n\nІнакше щось може взагалі не працювати.</string>
<!-- No need to be literal here, you can also use "Use Multiple Devices", "Support Multiple Devices" or other fitting terms. However, it should fit to the wording or your language at https://delta.chat/help -->
<string name="pref_multidevice">Режим роботи з декількома пристроями</string>
<string name="pref_multidevice_explain">Синхронізуйте свій повідомлення з іншими пристроями. Автоматично вмикається після додавання другого пристрою</string>
<string name="pref_multidevice_explain">Синхронізуйте свої повідомлення з іншими пристроями. Автоматично вмикається після додавання іншими пристроями</string>
<string name="pref_multidevice_change_warn">Режим декількох пристроїв має бути увімкнений, якщо ви використовуєте один і той самий профіль на декількох пристроях. Вимикайте цей параметр, лише якщо ви видалили цей профіль з усіх інших пристроїв.\n\nВимкнення режиму декількох пристроїв під час використання профілю на декількох пристроях призведе до пропусків повідомлення та інших проблем.</string>
<string name="pref_auto_folder_moves">Автоматично пересувати до папки DeltaChat</string>
<string name="pref_only_fetch_mvbox_title">Збирати лише з теки DeltaChat.</string>
@@ -847,7 +847,13 @@
<string name="disable_imap_idle">Вимкнути IMAP IDLE</string>
<string name="disable_imap_idle_explain">Не використовувати розширення IMAP IDLE навіть якщо сервер підтримує його. Увімкнення цієї опції затримає отримання повідомлень, вмикайте її лише для тестування.</string>
<string name="send_stats_to_devs">Надсилати статистику розробникам Delta Chat</string>
<string name="stats_msg_body">У вкладенні міститься анонімна статистика використання, яка допоможе нам покращити Delta Chat. Дякуємо!</string>
<string name="stats_device_message">Хочете допомогти покращити Delta Chat та підтримати дослідження, надсилаючи щотижневу анонімну статистику використання?\n\n 👉 Тисніть сюди… 👈</string>
<string name="stats_confirmation_dialog">Хочете допомогти покращити Delta Chat та підтримати дослідження, надсилаючи щотижневу анонімну статистику використання?</string>
<string name="stats_thanks">Дякуємо! Ви завжди маєте змогу це вимкнути у розділі \"Налаштування > Додатково\"\n\n Чи маєте Ви 5 хвилин для участі в науковому дослідженні стосовно безпеки Delta Chat?</string>
<string name="stats_disable_dialog">Надсилання статистики вже активне.\n\n Ви хочете її припинити?</string>
<string name="disable">Припинити</string>
<string name="stats_keep_sending">Продовжити</string>
<string name="stats_msg_body">У вкладенні міститься анонімна статистика використання, яка допомагає нам покращити Delta Chat. Дивіться https://delta.чат/help#statssending для отримання додаткової інформації. Дякуємо!</string>
<!-- Emoji picker and categories -->
<string name="emoji_search_results">Результати пошуку</string>
@@ -878,7 +884,7 @@
<!-- %1$d will be replaced by the number of messages, you can assume plural/lots here. %2$s will be replaced by a timespan option. -->
<string name="autodel_device_ask">Ви хочете видалити %1$d повідомлень зараз і всі нові повідомлення \"%2$s\" у майбутньому?\n\n- Це включає всі медіа\n\n- Повідомлення буде видалено незалежно від того, чи були вони переглянуті чи ні\n\n- \"Збережені повідомлення\" буде пропущено при локальному видаленні</string>
<!-- %1$d will be replaced by the number of messages, you can assume plural/lots here. %2$s will be replaced by a timespan option. -->
<string name="autodel_server_ask">Ви хочете видалити %1$d повідмолень зараз і всі нові повідомлення \"%2$s\" в майбутньому?\n\n⚠️ Сюди входять електронні листи, медіа та \"Збережені повідомлення\" у всіх папках сервера\n\n⚠️ Не використовуйте цю функцію, якщо ви хочете зберегти дані на сервері або якщо ви також використовуєте класичні поштові клієнти</string>
<string name="autodel_server_ask">Ви хочете видалити %1$d повідмолень зараз і всі нові повідомлення \"%2$s\" в майбутньому?\n\n⚠️ Сюди входять електронні листи, медіа та \"Збережені повідомлення\" у всіх теках сервера\n\n⚠️ Не використовуйте цю функцію, якщо ви хочете зберегти дані на сервері або якщо ви також використовуєте класичні поштові клієнти</string>
<!-- shown below enabled autodel_server-option, should be a summary of autodel_server_ask and remind about the impact -->
<string name="autodel_server_enabled_hint">Сюди входять електронні листи, медіафайли та \"Збережені повідомлення\" у всіх папках сервера. Не використовуйте цю функцію, якщо ви хочете зберігати дані на сервері або якщо ви використовуєте класичні поштові клієнти</string>
<string name="autodel_server_warn_multi_device_title">Увімкнути одномоментне видалення</string>
@@ -976,7 +982,7 @@
<string name="devicemsg_self_deleted">Ви видалили чат \"Збережені повідомлення\".\n\nℹ️ Аби використовувати функцію \"Збережені повідомлення\" знову, створіть новий чат самі із собою.</string>
<!-- %1$s will be replaced by the amount of storage already used, sth. as '500 MB'. If you want to use a percentage sign, type in two of them, eg. %1$s %% -->
<string name="devicemsg_storage_exceeding">⚠️ Сховище вашого провайдера електронної пошти майже заповнено, уже використовується %1$s%%.\n\nМожливо, Ви не зможете отримувати повідомлення, коли сховище буде заповнене.\n\n👉 Перевірте, чи можна видалити старі дані у веб-інтерфейсі провайдера та ввімкніть \"Налаштування / Видалити старі повідомлення\". Ви можете будь-коли перевірити своє поточне використання сховища в \"Налаштування / Підключення\". </string>
<string name="devicemsg_storage_exceeding">⚠️ Сховище вашого провайдера електронної пошти майже заповнено, уже використовується %1$s%%.\n\nМожливо, Ви не зможете отримувати повідомлення, коли сховище буде заповнене.\n\n👉 Перевірте, чи можна видалити старі дані у веб-інтерфейсі провайдера та ввімкніть \"Налаштування / Видалити старі повідомлення\". Ви можете будь-коли перевірити своє поточне використання сховища в \"Налаштування / Зʼєднання\". </string>
<!-- %1%s will be replaced by date and time in some human-readable format -->
<string name="devicemsg_bad_time">⚠️ Дата й час на вашому пристрої видаються неточними (%1$s).\n\nНалаштуйте свій годинник ⏰🔧 для правильного отримання повідомлень.</string>
<string name="devicemsg_update_reminder">⚠️ Ваша версія Delta Chat може бути застарілою.\n\nЦе може спричинити проблеми, оскільки Ваші співрозмовники в чаті використовують новіші версії, а Вам не вистачає найновіших функцій 😳 \nПеревірте оновлення на https://get.delta.chat або в магазині додатків. </string>
@@ -1030,6 +1036,7 @@
<string name="secure_join_replies">%1$s відповіли, очікуємо на додавання до групи…</string>
<string name="secure_join_wait">Встановлення гарантованого наскрізного шифрування, будь ласка, зачекайте...</string>
<string name="contact_verified">%1$s представлено!</string>
<string name="contact_blocked">Ви заблокували контакт</string>
<!-- Shown in contact profile. The placeholder will be replaced by the name of the contact that introduced the contact. -->
<string name="verified_by">Представни·ця: %1$s</string>
<string name="verified_by_you">Представлено мною</string>
@@ -1051,7 +1058,7 @@
<string name="notify_name_only">Тільки ім\'я</string>
<string name="notify_no_name_or_message">Без імені або повідомлення</string>
<string name="notifications_disabled">Сповіщення вимкнено</string>
<string name="unreliable_bg_notifications">Увімкніть \"Примусове фонове підключення\" для надійних сповіщень</string>
<string name="unreliable_bg_notifications">Увімкніть \"Примусове фонове зʼєднання\" для надійних сповіщень</string>
<string name="new_messages">Нові повідомлення</string>
<!-- Body text for a generic "New messages" notification. Shown if we do not have more information about a new messages. Note, that the string is also referenced at https://github.com/deltachat/notifiers -->
<string name="new_messages_body">У вас нові повідомлення</string>
@@ -1115,8 +1122,8 @@
<string name="name_desktop">Імʼя</string>
<string name="select_group_image_desktop">Вибрати емблему групи</string>
<string name="export_backup_desktop">Експорт резервної копії</string>
<string name="message_detail_sent_desktop">надіслано</string>
<string name="message_detail_received_desktop">отримано</string>
<string name="message_detail_sent_desktop">Надіслано</string>
<string name="message_detail_received_desktop">Отримано</string>
<string name="menu.view.developer.open.log.folder">Відкрити папку журналів</string>
<string name="menu.view.developer.open.current.log.file">Відкрити поточний файл журналу</string>
<string name="explain_desktop_minimized_disabled_tray_pref" tools:ignore="TypographyDashes">Іконку в треї не можна вимкнути, оскільки Delta Chat було запущено за допомогою параметра --minimized.</string>
+1
View File
@@ -79,6 +79,7 @@
<attr name="menu_reply_icon" format="reference" />
<attr name="pref_icon_tint" format="color"/>
<attr name="menu_icon_tint" format="color" />
<attr name="quote_missing_icon_color" format="color" />
<attr name="quote_dismiss_button_tint" format="color" />
+10 -2
View File
@@ -380,12 +380,20 @@
<!-- the action "to call someone", used as a tooltip for the "phone" icon. not: "the call" -->
<string name="start_call">Call</string>
<string name="start_audio_call">Audio Call</string>
<string name="start_video_call">Video Call</string>
<!-- the action "to answer" or to "accept" or to "pick up" a call. not: "the answer" -->
<string name="answer_call">Answer</string>
<!-- the action "to decline" a call, not: "the decline" -->
<string name="end_call">Decline</string>
<!-- deprecated, use outgoing_audio_call or outgoing_video_call instead -->
<string name="outgoing_call">Outgoing call</string>
<string name="outgoing_audio_call">Outgoing audio call</string>
<string name="outgoing_video_call">Outgoing video call</string>
<!-- deprecated, use incoming_audio_call or incoming_video_call instead -->
<string name="incoming_call">Incoming call</string>
<string name="incoming_audio_call">Incoming audio call</string>
<string name="incoming_video_call">Incoming video call</string>
<string name="declined_call">Declined call</string>
<string name="canceled_call">Canceled call</string>
<string name="missed_call">Missed call</string>
@@ -1088,8 +1096,8 @@
<string name="name_desktop">Name</string>
<string name="select_group_image_desktop">Select Group Image</string>
<string name="export_backup_desktop">Export Backup</string>
<string name="message_detail_sent_desktop">sent</string>
<string name="message_detail_received_desktop">received</string>
<string name="message_detail_sent_desktop">Sent</string>
<string name="message_detail_received_desktop">Received</string>
<string name="menu.view.developer.open.log.folder">Open the Log Folder</string>
<string name="menu.view.developer.open.current.log.file">Open Current Logfile</string>
<string name="explain_desktop_minimized_disabled_tray_pref" tools:ignore="TypographyDashes">Tray icon cannot be disabled as Delta Chat was started with the --minimized option.</string>
+2
View File
@@ -133,6 +133,7 @@
<item name="menu_reply_icon">@drawable/ic_reply_white_24dp</item>
<item name="pref_icon_tint">@color/def_primary</item>
<item name="menu_icon_tint">@color/black</item>
<item name="icon_tint">@color/grey_700</item>
<item name="icon_tint_dark">@color/grey_100</item>
@@ -243,6 +244,7 @@
<item name="menu_reply_icon">@drawable/ic_reply_white_24dp</item>
<item name="pref_icon_tint">@color/def_primary</item>
<item name="menu_icon_tint">@color/white</item>
<item name="icon_tint">@color/grey_100</item>
<item name="icon_tint_dark">?icon_tint</item>