mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-17 15:48:26 +00:00
commit
2ef7cbe7e3
@ -143,8 +143,8 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 182
|
||||
def canonicalVersionName = "1.10.13"
|
||||
def canonicalVersionCode = 188
|
||||
def canonicalVersionName = "1.11.0"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
@ -194,7 +194,7 @@ android {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
|
||||
minSdkVersion 21
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
|
||||
multiDexEnabled = true
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="network.loki.messenger">
|
||||
|
||||
@ -7,7 +8,7 @@
|
||||
|
||||
<permission
|
||||
android:name="network.loki.messenger.ACCESS_SESSION_SECRETS"
|
||||
android:label="Access to TextSecure Secrets"
|
||||
android:label="Access to Session secrets"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<uses-feature
|
||||
@ -36,32 +37,26 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<!-- For conversation 'shortcuts' on the desktop -->
|
||||
<uses-permission android:name="android.permission.INSTALL_SHORTCUT" />
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
|
||||
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
|
||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
||||
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<!-- Unused permissions that need to be removed -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/>
|
||||
|
||||
<!-- The allowBackup="false" below is important to guard against potential malicious backups -->
|
||||
|
||||
<application
|
||||
android:name="org.thoughtcrime.securesms.ApplicationContext"
|
||||
android:allowBackup="false"
|
||||
@ -73,7 +68,8 @@
|
||||
android:theme="@style/Theme.Session.DayNight"
|
||||
tools:replace="android:allowBackup">
|
||||
|
||||
<!-- Disable analytics -->
|
||||
<!-- Disable all analytics -->
|
||||
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_deactivated"
|
||||
android:value="true" />
|
||||
@ -90,22 +86,16 @@
|
||||
android:name="firebase_messaging_auto_init_enabled"
|
||||
android:value="false" />
|
||||
|
||||
<!-- Session -->
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.LandingActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"/>
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.RegisterActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"/>
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.RecoveryPhraseRestoreActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.BackupRestoreActivity"
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.RecoveryPhraseRestoreActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
@ -113,16 +103,16 @@
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.LinkDeviceActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.DisplayNameActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"/>
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.PNModeActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.HomeActivity"
|
||||
android:screenOrientation="portrait"
|
||||
@ -131,18 +121,19 @@
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.SettingsActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:label="@string/activity_settings_title"/>
|
||||
android:label="@string/activity_settings_title" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.PathActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.QRCodeActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"/>
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.CreatePrivateChatActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"/>
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.CreateClosedGroupActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
@ -164,14 +155,14 @@
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.PrivacySettingsActivity"
|
||||
android:label="@string/activity_privacy_settings_title"
|
||||
android:screenOrientation="portrait"/>
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.NotificationSettingsActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.ChatSettingsActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<!-- Session -->
|
||||
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.ShareActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@ -184,9 +175,7 @@
|
||||
android:windowSoftInputMode="stateHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="text/plain" />
|
||||
@ -195,7 +184,6 @@
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value=".service.DirectShareService" />
|
||||
@ -207,11 +195,9 @@
|
||||
android:targetActivity="org.thoughtcrime.securesms.loki.activities.HomeActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="com.sec.minimode.icon.portrait.normal"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
@ -232,14 +218,18 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.TextSecure.DayNight"/>
|
||||
android:theme="@style/Theme.TextSecure.DayNight" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.TextSecure.DayNight"/>
|
||||
android:theme="@style/Theme.TextSecure.DayNight" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.conversation.ConversationPopupActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@ -264,7 +254,7 @@
|
||||
android:name="org.thoughtcrime.securesms.PassphrasePromptActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Session.DayNight.NoActionBar"/>
|
||||
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.giph.ui.GiphyActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@ -306,7 +296,7 @@
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.scribbles.StickerSelectActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Theme.Session.ForceDark"/>
|
||||
android:theme="@style/Theme.Session.ForceDark" />
|
||||
<activity
|
||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
||||
android:screenOrientation="portrait"
|
||||
@ -436,7 +426,6 @@
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- Session -->
|
||||
<receiver
|
||||
android:name="org.thoughtcrime.securesms.loki.api.BackgroundPollWorker$BootBroadcastReceiver"
|
||||
android:enabled="true">
|
||||
@ -444,7 +433,6 @@
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- Session -->
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.jobmanager.JobSchedulerScheduler$SystemService"
|
||||
android:enabled="@bool/enable_job_service"
|
||||
@ -456,11 +444,9 @@
|
||||
<receiver
|
||||
android:name="org.thoughtcrime.securesms.jobmanager.AlarmManagerScheduler$RetryReceiver"
|
||||
android:enabled="@bool/enable_alarm_manager" /> <!-- Probably don't need this one -->
|
||||
|
||||
<uses-library
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
android:required="false" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:value="true" />
|
||||
|
@ -26,7 +26,7 @@ import android.widget.TextView;
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
|
||||
|
||||
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
@ -64,10 +64,12 @@ import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.components.MediaView;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
@ -116,6 +118,22 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
|
||||
private int restartItem = -1;
|
||||
|
||||
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
|
||||
Intent previewIntent = null;
|
||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||
previewIntent = new Intent(context, MediaPreviewActivity.class);
|
||||
previewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setDataAndType(slide.getUri(), slide.getContentType())
|
||||
.putExtra(ADDRESS_EXTRA, threadRecipient.getAddress())
|
||||
.putExtra(OUTGOING_EXTRA, mms.isOutgoing())
|
||||
.putExtra(DATE_EXTRA, mms.getTimestamp())
|
||||
.putExtra(SIZE_EXTRA, slide.asAttachment().getSize())
|
||||
.putExtra(CAPTION_EXTRA, slide.getCaption().orNull())
|
||||
.putExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
}
|
||||
return previewIntent;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
|
@ -187,8 +187,6 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
transportText = "-";
|
||||
} else if (messageRecord.isPending()) {
|
||||
transportText = getString(R.string.ConversationFragment_pending);
|
||||
} else if (messageRecord.isPush()) {
|
||||
transportText = getString(R.string.ConversationFragment_push);
|
||||
} else if (messageRecord.isMms()) {
|
||||
transportText = getString(R.string.ConversationFragment_mms);
|
||||
} else {
|
||||
@ -252,9 +250,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private void updateRecipients(MessageRecord messageRecord, Recipient recipient, List<RecipientDeliveryStatus> recipients) {
|
||||
final int toFromRes;
|
||||
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
|
||||
toFromRes = R.string.message_details_header__with;
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
toFromRes = R.string.message_details_header__to;
|
||||
} else {
|
||||
toFromRes = R.string.message_details_header__from;
|
||||
@ -272,9 +268,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private void inflateMessageViewIfAbsent(MessageRecord messageRecord) {
|
||||
if (conversationItem == null) {
|
||||
if (messageRecord.isGroupAction()) {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_update, itemParent, false);
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent, itemParent, false);
|
||||
} else {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received, itemParent, false);
|
||||
@ -362,7 +356,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
List<RecipientDeliveryStatus> recipients = new LinkedList<>();
|
||||
|
||||
if (!messageRecord.getRecipient().isGroupRecipient()) {
|
||||
recipients.add(new RecipientDeliveryStatus(messageRecord.getRecipient(), getStatusFor(messageRecord.getDeliveryReceiptCount(), messageRecord.getReadReceiptCount(), messageRecord.isPending()), messageRecord.isUnidentified(), -1));
|
||||
recipients.add(new RecipientDeliveryStatus(messageRecord.getRecipient(), getStatusFor(messageRecord.getDeliveryReceiptCount(), messageRecord.getReadReceiptCount(), messageRecord.isPending()), true, -1));
|
||||
} else {
|
||||
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
|
||||
|
||||
@ -396,7 +390,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
updateRecipients(messageRecord, messageRecord.getRecipient(), recipients);
|
||||
|
||||
boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty();
|
||||
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty();
|
||||
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup;
|
||||
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(getContext());
|
||||
String errorMessage = lokiMessageDatabase.getErrorMessage(messageRecord.id);
|
||||
|
@ -39,6 +39,7 @@ import org.session.libsession.utilities.DistributionTypes;
|
||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.loki.fragments.ContactSelectionListFragment;
|
||||
@ -215,9 +216,9 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void createConversation(long threadId, Address address, int distributionType) {
|
||||
final Intent intent = getBaseShareIntent(ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, address);
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
final Intent intent = getBaseShareIntent(ConversationActivityV2.class);
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, address);
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
|
||||
|
||||
isPassingAlongMedia = true;
|
||||
|
@ -23,6 +23,7 @@ import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.LoadControl;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
@ -103,9 +104,9 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
}
|
||||
|
||||
private void play(final double progress, boolean earpiece) throws IOException {
|
||||
if (this.mediaPlayer != null) return;
|
||||
if (this.mediaPlayer != null) { stop(); }
|
||||
|
||||
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl();
|
||||
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl();
|
||||
this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
|
||||
this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment());
|
||||
this.startTime = System.currentTimeMillis();
|
||||
@ -184,8 +185,6 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
Log.w(TAG, "MediaPlayer Error: " + error);
|
||||
|
||||
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
|
||||
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
mediaPlayer = null;
|
||||
|
||||
@ -267,8 +266,17 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
return slide;
|
||||
}
|
||||
|
||||
public Long getDuration() {
|
||||
if (mediaPlayer == null) { return 0L; }
|
||||
return mediaPlayer.getDuration();
|
||||
}
|
||||
|
||||
private Pair<Double, Integer> getProgress() {
|
||||
public Double getProgress() {
|
||||
if (mediaPlayer == null) { return 0.0; }
|
||||
return (double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration();
|
||||
}
|
||||
|
||||
private Pair<Double, Integer> getProgressTuple() {
|
||||
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
|
||||
return new Pair<>(0D, 0);
|
||||
} else {
|
||||
@ -277,6 +285,16 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
public float getPlaybackSpeed() {
|
||||
if (mediaPlayer == null) { return 1.0f; }
|
||||
return mediaPlayer.getPlaybackParameters().speed;
|
||||
}
|
||||
|
||||
public void setPlaybackSpeed(float speed) {
|
||||
if (mediaPlayer == null) { return; }
|
||||
mediaPlayer.setPlaybackParameters(new PlaybackParameters(speed));
|
||||
}
|
||||
|
||||
private void notifyOnStart() {
|
||||
Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this));
|
||||
}
|
||||
@ -383,7 +401,7 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
return;
|
||||
}
|
||||
|
||||
Pair<Double, Integer> progress = player.getProgress();
|
||||
Pair<Double, Integer> progress = player.getProgressTuple();
|
||||
player.notifyOnProgress(progress.first, progress.second);
|
||||
sendEmptyMessageDelayed(0, 50);
|
||||
}
|
||||
|
@ -1,25 +1,27 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.utilities.Stub;
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.session.libsession.utilities.Stub;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class AlbumThumbnailView extends FrameLayout {
|
||||
|
||||
private @Nullable SlideClickListener thumbnailClickListener;
|
||||
@ -51,8 +53,8 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
private void initialize() {
|
||||
inflate(getContext(), R.layout.album_thumbnail_view, this);
|
||||
|
||||
albumCellContainer = findViewById(R.id.album_cell_container);
|
||||
transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
|
||||
albumCellContainer = findViewById(R.id.albumCellContainer);
|
||||
transferControls = new Stub<>(findViewById(R.id.albumTransferControlsStub));
|
||||
}
|
||||
|
||||
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) {
|
||||
@ -147,10 +149,5 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) {
|
||||
ThumbnailView cell = findViewById(id);
|
||||
cell.setImageResource(glideRequests, slide, false, false);
|
||||
cell.setLoadIndicatorVisibile(slide.isInProgress());
|
||||
cell.setThumbnailClickListener(defaultThumbnailClickListener);
|
||||
cell.setOnLongClickListener(defaultLongClickListener);
|
||||
}
|
||||
}
|
||||
|
@ -4,15 +4,17 @@ import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
@ -88,8 +90,6 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
|
||||
if (messageRecord.isFailed()) {
|
||||
dateView.setText(R.string.ConversationItem_error_not_delivered);
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
|
||||
} else {
|
||||
dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
|
||||
}
|
||||
@ -131,14 +131,14 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
}
|
||||
|
||||
private void presentInsecureIndicator(@NonNull MessageRecord messageRecord) {
|
||||
insecureIndicatorView.setVisibility(messageRecord.isSecure() ? View.GONE : View.VISIBLE);
|
||||
insecureIndicatorView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) {
|
||||
if (!messageRecord.isFailed() && !messageRecord.isPendingInsecureSmsFallback()) {
|
||||
if (!messageRecord.isFailed()) {
|
||||
if (!messageRecord.isOutgoing()) deliveryStatusView.setNone();
|
||||
else if (messageRecord.isPending()) deliveryStatusView.setPending();
|
||||
else if (messageRecord.isRemoteRead()) deliveryStatusView.setRead();
|
||||
else if (messageRecord.isRead()) deliveryStatusView.setRead();
|
||||
else if (messageRecord.isDelivered()) deliveryStatusView.setDelivered();
|
||||
else deliveryStatusView.setSent();
|
||||
} else {
|
||||
|
@ -12,6 +12,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
@ -27,7 +28,7 @@ import network.loki.messenger.R;
|
||||
|
||||
public class ConversationItemThumbnail extends FrameLayout {
|
||||
|
||||
private ThumbnailView thumbnail;
|
||||
private ThumbnailView thumbnail;
|
||||
private AlbumThumbnailView album;
|
||||
private ImageView shade;
|
||||
private ConversationItemFooter footer;
|
||||
@ -64,15 +65,10 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
|
||||
thumbnail.setBounds(typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0),
|
||||
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0),
|
||||
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0),
|
||||
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0));
|
||||
typedArray.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
@Override
|
||||
protected void dispatchDraw(Canvas canvas) {
|
||||
super.dispatchDraw(canvas);
|
||||
|
@ -8,6 +8,7 @@ import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
|
@ -5,6 +5,7 @@ import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
@ -28,7 +29,6 @@ public class OutlinedThumbnailView extends ThumbnailView {
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||
setRadius(0);
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
|
@ -79,8 +79,7 @@ public class TypingStatusSender {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
|
||||
if (recipient == null) { return; }
|
||||
// Loki - Check whether we want to send a typing indicator to this user
|
||||
if (recipient != null && !SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; }
|
||||
if (!SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; }
|
||||
TypingIndicator typingIndicator;
|
||||
if (typingStarted) {
|
||||
typingIndicator = new TypingIndicator(TypingIndicator.Kind.STARTED);
|
||||
|
@ -11,7 +11,6 @@ import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
@ -19,9 +18,7 @@ import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
|
||||
|
||||
public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
private final boolean scaleEmojis;
|
||||
|
||||
private static final char ELLIPSIS = '…';
|
||||
@ -46,14 +43,9 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
|
||||
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
|
||||
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
|
||||
a.recycle();
|
||||
|
||||
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
|
||||
originalFontSize = a.getDimensionPixelSize(0, 0);
|
||||
a.recycle();
|
||||
scaleEmojis = true;
|
||||
maxLength = 1000;
|
||||
originalFontSize = getResources().getDimension(R.dimen.small_font_size);
|
||||
}
|
||||
|
||||
@Override public void setText(@Nullable CharSequence text, BufferType type) {
|
||||
@ -182,8 +174,11 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
@Override
|
||||
public void invalidateDrawable(@NonNull Drawable drawable) {
|
||||
if (drawable instanceof EmojiDrawable) invalidate();
|
||||
else super.invalidateDrawable(drawable);
|
||||
if (drawable instanceof EmojiDrawable) {
|
||||
invalidate();
|
||||
} else {
|
||||
super.invalidateDrawable(drawable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -102,7 +102,6 @@ import org.session.libsession.utilities.recipients.RecipientModifiedListener;
|
||||
import org.session.libsession.utilities.ExpirationUtil;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsession.utilities.MediaTypes;
|
||||
import org.session.libsession.utilities.SSKEnvironment;
|
||||
import org.session.libsession.utilities.ServiceUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
@ -165,8 +164,8 @@ import org.thoughtcrime.securesms.loki.views.MentionCandidateSelectionView;
|
||||
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager;
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager.MediaType;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
@ -422,9 +421,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return;
|
||||
}
|
||||
|
||||
if (!org.thoughtcrime.securesms.util.Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent()) {
|
||||
if (!org.thoughtcrime.securesms.util.Util.isEmpty(composeText)) {
|
||||
saveDraft();
|
||||
attachmentManager.clear(glideRequests, false);
|
||||
attachmentManager.clear();
|
||||
silentlySetComposeText("");
|
||||
}
|
||||
|
||||
@ -1424,9 +1423,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
case AttachmentTypeSelector.ADD_SOUND:
|
||||
AttachmentManager.selectAudio(this, PICK_AUDIO); break;
|
||||
case AttachmentTypeSelector.ADD_CONTACT_INFO:
|
||||
AttachmentManager.selectContactInfo(this, PICK_CONTACT); break;
|
||||
break;
|
||||
case AttachmentTypeSelector.ADD_LOCATION:
|
||||
AttachmentManager.selectLocation(this, PICK_LOCATION); break;
|
||||
break;
|
||||
case AttachmentTypeSelector.TAKE_PHOTO:
|
||||
attachmentManager.capturePhoto(this, TAKE_PHOTO); break;
|
||||
case AttachmentTypeSelector.ADD_GIF:
|
||||
@ -1620,7 +1619,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private String getMessage() throws InvalidMessageException {
|
||||
String result = composeText.getTextTrimmed();
|
||||
if (result.length() < 1 && !attachmentManager.isAttachmentPresent()) throw new InvalidMessageException();
|
||||
if (result.length() < 1) throw new InvalidMessageException();
|
||||
for (Mention mention : mentions) {
|
||||
try {
|
||||
int startIndex = result.indexOf("@" + mention.getDisplayName());
|
||||
@ -1723,7 +1722,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
String message = getMessage();
|
||||
boolean initiating = threadId == -1;
|
||||
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
|
||||
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
|
||||
boolean isMediaMessage = false ||
|
||||
// recipient.isGroupRecipient() ||
|
||||
inputPanel.getQuote().isPresent() ||
|
||||
linkPreviewViewModel.hasLinkPreview() ||
|
||||
@ -1785,7 +1784,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId);
|
||||
|
||||
inputPanel.clearQuote();
|
||||
attachmentManager.clear(glideRequests, false);
|
||||
attachmentManager.clear();
|
||||
silentlySetComposeText("");
|
||||
|
||||
final long id = fragment.stageOutgoingMessage(outgoingMessage);
|
||||
@ -1859,7 +1858,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return;
|
||||
}
|
||||
|
||||
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
|
||||
if (composeText.getText().length() == 0) {
|
||||
buttonToggle.display(attachButton);
|
||||
quickAttachmentToggle.show();
|
||||
inlineAttachmentToggle.hide();
|
||||
@ -1867,7 +1866,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
buttonToggle.display(sendButton);
|
||||
quickAttachmentToggle.hide();
|
||||
|
||||
if (!attachmentManager.isAttachmentPresent() && !linkPreviewViewModel.hasLinkPreview()) {
|
||||
if (!linkPreviewViewModel.hasLinkPreview()) {
|
||||
inlineAttachmentToggle.show();
|
||||
} else {
|
||||
inlineAttachmentToggle.hide();
|
||||
@ -1876,7 +1875,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void updateLinkPreviewState() {
|
||||
if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !attachmentManager.isAttachmentPresent()) {
|
||||
if (TextSecurePreferences.isLinkPreviewsEnabled(this)) {
|
||||
linkPreviewViewModel.onEnabled();
|
||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd());
|
||||
} else {
|
||||
|
@ -351,10 +351,7 @@ public class ConversationFragment extends Fragment
|
||||
}
|
||||
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
if (messageRecord.isGroupAction() || messageRecord.isCallLog() ||
|
||||
messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate() ||
|
||||
messageRecord.isEndSession() || messageRecord.isIdentityUpdate() ||
|
||||
messageRecord.isIdentityVerified() || messageRecord.isIdentityDefault() || messageRecord.isLokiSessionRestoreSent() || messageRecord.isLokiSessionRestoreDone())
|
||||
if (messageRecord.isCallLog() || messageRecord.isExpirationTimerUpdate())
|
||||
{
|
||||
actionMessage = true;
|
||||
}
|
||||
@ -385,8 +382,7 @@ public class ConversationFragment extends Fragment
|
||||
|
||||
menu.findItem(R.id.menu_context_reply).setVisible(!actionMessage &&
|
||||
!messageRecord.isPending() &&
|
||||
!messageRecord.isFailed() &&
|
||||
messageRecord.isSecure());
|
||||
!messageRecord.isFailed());
|
||||
}
|
||||
|
||||
menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage && hasText);
|
||||
@ -626,7 +622,7 @@ public class ConversationFragment extends Fragment
|
||||
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
||||
intent.putExtra(MessageDetailsActivity.ADDRESS_EXTRA, recipient.getAddress());
|
||||
intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, recipient.isGroupRecipient() && message.isPush());
|
||||
intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, recipient.isGroupRecipient());
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
|
@ -784,12 +784,10 @@ public class ConversationItem extends LinearLayout
|
||||
}
|
||||
|
||||
private void setStatusIcons(MessageRecord messageRecord) {
|
||||
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0);
|
||||
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
|
||||
if (messageRecord.isFailed()) {
|
||||
alertView.setFailed();
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
alertView.setPendingApproval();
|
||||
} else {
|
||||
alertView.setNone();
|
||||
}
|
||||
@ -859,7 +857,7 @@ public class ConversationItem extends LinearLayout
|
||||
|
||||
boolean differentTimestamps = next.isPresent() && !DateUtils.isSameExtendedRelativeTimestamp(context, locale, next.get().getTimestamp(), current.getTimestamp());
|
||||
|
||||
if (current.getExpiresIn() > 0 || !current.isSecure() || current.isPending() || current.isPendingInsecureSmsFallback() ||
|
||||
if (current.getExpiresIn() > 0 || current.isPending() ||
|
||||
current.isFailed() || differentTimestamps || isEndOfMessageCluster(current, next, isGroupThread))
|
||||
{
|
||||
ConversationItemFooter activeFooter = getActiveFooter(current);
|
||||
@ -881,10 +879,7 @@ public class ConversationItem extends LinearLayout
|
||||
}
|
||||
|
||||
private boolean shouldInterceptClicks(MessageRecord messageRecord) {
|
||||
return batchSelected.isEmpty() &&
|
||||
((messageRecord.isFailed() && !messageRecord.isMmsNotification()) ||
|
||||
messageRecord.isPendingInsecureSmsFallback() ||
|
||||
messageRecord.isBundleKeyExchange());
|
||||
return batchSelected.isEmpty() && (messageRecord.isFailed() && !messageRecord.isMmsNotification());
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@ -1199,7 +1194,7 @@ public class ConversationItem extends LinearLayout
|
||||
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, messageRecord.getId());
|
||||
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, messageRecord.getThreadId());
|
||||
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
||||
intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, groupThread && messageRecord.isPush());
|
||||
intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, groupThread);
|
||||
intent.putExtra(MessageDetailsActivity.ADDRESS_EXTRA, conversationRecipient.getAddress());
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import android.view.WindowManager;
|
||||
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsignal.utilities.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@ -80,9 +81,9 @@ public class ConversationPopupActivity extends ConversationActivity {
|
||||
@Override
|
||||
public void onSuccess(Long result) {
|
||||
ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height);
|
||||
Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, getRecipient().getAddress());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result);
|
||||
Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivityV2.class);
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, getRecipient().getAddress());
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, result);
|
||||
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
|
||||
startActivity(intent, transition.toBundle());
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.util.AttributeSet;
|
||||
@ -15,17 +14,16 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
|
||||
import org.session.libsession.utilities.ExpirationUtil;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.RecipientModifiedListener;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.RecipientModifiedListener;
|
||||
import org.session.libsession.utilities.ExpirationUtil;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
@ -101,18 +99,10 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
|
||||
this.sender.addListener(this);
|
||||
|
||||
if (messageRecord.isGroupAction()) setGroupRecord(messageRecord);
|
||||
else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
|
||||
else if (messageRecord.isJoined()) setJoinedRecord(messageRecord);
|
||||
if (messageRecord.isCallLog()) setCallRecord(messageRecord);
|
||||
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
|
||||
else if (messageRecord.isScreenshotExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT);
|
||||
else if (messageRecord.isMediaSavedExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED);
|
||||
else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord);
|
||||
else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord);
|
||||
else if (messageRecord.isIdentityVerified() ||
|
||||
messageRecord.isIdentityDefault()) setIdentityVerifyUpdate(messageRecord);
|
||||
else if (messageRecord.isLokiSessionRestoreSent()) setTextMessageRecord(messageRecord);
|
||||
else if (messageRecord.isLokiSessionRestoreDone()) setTextMessageRecord(messageRecord);
|
||||
else if (messageRecord.isScreenshotNotification()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT);
|
||||
else if (messageRecord.isMediaSavedNotification()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED);
|
||||
else throw new AssertionError("Neither group nor log nor joined.");
|
||||
|
||||
if (batchSelected.contains(messageRecord)) setSelected(true);
|
||||
@ -166,58 +156,6 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setIdentityRecord(final MessageRecord messageRecord) {
|
||||
icon.setImageResource(R.drawable.ic_security_white_24dp);
|
||||
icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setIdentityVerifyUpdate(final MessageRecord messageRecord) {
|
||||
if (messageRecord.isIdentityVerified()) icon.setImageResource(R.drawable.ic_check_white_24dp);
|
||||
else icon.setImageResource(R.drawable.ic_info_outline_white_24dp);
|
||||
|
||||
icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setGroupRecord(MessageRecord messageRecord) {
|
||||
icon.setImageResource(R.drawable.ic_group_grey600_24dp);
|
||||
icon.clearColorFilter();
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setJoinedRecord(MessageRecord messageRecord) {
|
||||
icon.setImageResource(R.drawable.ic_favorite_grey600_24dp);
|
||||
icon.clearColorFilter();
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setEndSessionRecord(MessageRecord messageRecord) {
|
||||
icon.setImageResource(R.drawable.ic_refresh_white_24dp);
|
||||
icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setTextMessageRecord(MessageRecord messageRecord) {
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
@ -254,36 +192,7 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if ((!messageRecord.isIdentityUpdate() &&
|
||||
!messageRecord.isIdentityDefault() &&
|
||||
!messageRecord.isIdentityVerified()) ||
|
||||
!batchSelected.isEmpty())
|
||||
{
|
||||
if (parent != null) parent.onClick(v);
|
||||
return;
|
||||
}
|
||||
|
||||
final Recipient sender = ConversationUpdateItem.this.sender;
|
||||
|
||||
// IdentityUtil.getRemoteIdentityKey(getContext(), sender).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
|
||||
// @Override
|
||||
// public void onSuccess(Optional<IdentityRecord> result) {
|
||||
// if (result.isPresent()) {
|
||||
// Intent intent = new Intent(getContext(), VerifyIdentityActivity.class);
|
||||
// intent.putExtra(VerifyIdentityActivity.ADDRESS_EXTRA, sender.getAddress());
|
||||
// intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(result.get().getIdentityKey()));
|
||||
// intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, result.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
|
||||
//
|
||||
// getContext().startActivity(intent);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onFailure(ExecutionException e) {
|
||||
// Log.w(TAG, e);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,142 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Rect
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import kotlinx.android.synthetic.main.view_visible_message.view.*
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
|
||||
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit,
|
||||
private val glide: GlideRequests)
|
||||
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
|
||||
private val messageDB = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
var selectedItems = mutableSetOf<MessageRecord>()
|
||||
private var searchQuery: String? = null
|
||||
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null
|
||||
|
||||
sealed class ViewType(val rawValue: Int) {
|
||||
object Visible : ViewType(0)
|
||||
object Control : ViewType(1)
|
||||
|
||||
companion object {
|
||||
|
||||
val allValues: Map<Int, ViewType> get() = mapOf(
|
||||
Visible.rawValue to Visible,
|
||||
Control.rawValue to Control
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class VisibleMessageViewHolder(val view: VisibleMessageView) : ViewHolder(view)
|
||||
class ControlMessageViewHolder(val view: ControlMessageView) : ViewHolder(view)
|
||||
|
||||
override fun getItemViewType(cursor: Cursor): Int {
|
||||
val message = getMessage(cursor)!!
|
||||
if (message.isControlMessage) { return ViewType.Control.rawValue }
|
||||
return ViewType.Visible.rawValue
|
||||
}
|
||||
|
||||
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val viewType = ViewType.allValues[viewType]
|
||||
when (viewType) {
|
||||
ViewType.Visible -> {
|
||||
val view = VisibleMessageView(context)
|
||||
return VisibleMessageViewHolder(view)
|
||||
}
|
||||
ViewType.Control -> {
|
||||
val view = ControlMessageView(context)
|
||||
return ControlMessageViewHolder(view)
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected view type: $viewType.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindItemViewHolder(viewHolder: ViewHolder, cursor: Cursor) {
|
||||
val message = getMessage(cursor)!!
|
||||
when (viewHolder) {
|
||||
is VisibleMessageViewHolder -> {
|
||||
val view = viewHolder.view
|
||||
val isSelected = selectedItems.contains(message)
|
||||
view.snIsSelected = isSelected
|
||||
view.messageTimestampTextView.isVisible = isSelected
|
||||
val position = viewHolder.adapterPosition
|
||||
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide, searchQuery)
|
||||
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
|
||||
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
||||
view.contentViewDelegate = visibleMessageContentViewDelegate
|
||||
}
|
||||
is ControlMessageViewHolder -> viewHolder.view.bind(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemViewRecycled(viewHolder: ViewHolder?) {
|
||||
when (viewHolder) {
|
||||
is VisibleMessageViewHolder -> viewHolder.view.recycle()
|
||||
is ControlMessageViewHolder -> viewHolder.view.recycle()
|
||||
}
|
||||
super.onItemViewRecycled(viewHolder)
|
||||
}
|
||||
|
||||
private fun getMessage(cursor: Cursor): MessageRecord? {
|
||||
return messageDB.readerFor(cursor).current
|
||||
}
|
||||
|
||||
private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? {
|
||||
// The message that's visually before the current one is actually after the current
|
||||
// one for the cursor because the layout is reversed
|
||||
if (!cursor.moveToPosition(position + 1)) { return null }
|
||||
return messageDB.readerFor(cursor).current
|
||||
}
|
||||
|
||||
private fun getMessageAfter(position: Int, cursor: Cursor): MessageRecord? {
|
||||
// The message that's visually after the current one is actually before the current
|
||||
// one for the cursor because the layout is reversed
|
||||
if (!cursor.moveToPosition(position - 1)) { return null }
|
||||
return messageDB.readerFor(cursor).current
|
||||
}
|
||||
|
||||
fun toggleSelection(message: MessageRecord, position: Int) {
|
||||
if (selectedItems.contains(message)) selectedItems.remove(message) else selectedItems.add(message)
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
|
||||
val cursor = this.cursor
|
||||
if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null
|
||||
for (i in 0 until itemCount) {
|
||||
cursor.moveToPosition(i)
|
||||
val message = messageDB.readerFor(cursor).current
|
||||
if (message.isOutgoing || message.dateReceived <= lastSeenTimestamp) { return i }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getItemPositionForTimestamp(timestamp: Long): Int? {
|
||||
val cursor = this.cursor
|
||||
if (timestamp <= 0L || cursor == null || !isActiveCursor) return null
|
||||
for (i in 0 until itemCount) {
|
||||
cursor.moveToPosition(i)
|
||||
val message = messageDB.readerFor(cursor).current
|
||||
if (message.dateSent == timestamp) { return i }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun onSearchQueryUpdated(query: String?) {
|
||||
this.searchQuery = query
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader
|
||||
|
||||
class ConversationLoader(private val threadID: Long, context: Context) : AbstractCursorLoader(context) {
|
||||
|
||||
override fun getCursor(): Cursor {
|
||||
return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadID)
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.VelocityTracker
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
||||
import org.thoughtcrime.securesms.loki.utilities.disableClipping
|
||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
class ConversationRecyclerView : RecyclerView {
|
||||
private val maxLongPressVelocityY = toPx(10, resources)
|
||||
private val minSwipeVelocityX = toPx(10, resources)
|
||||
private var velocityTracker: VelocityTracker? = null
|
||||
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
disableClipping()
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
|
||||
val velocityTracker = velocityTracker ?: return super.onInterceptTouchEvent(e)
|
||||
velocityTracker.computeCurrentVelocity(1000) // Specifying 1000 gives pixels per second
|
||||
val vx = velocityTracker.xVelocity
|
||||
val vy = velocityTracker.yVelocity
|
||||
// Only allow swipes to the left; allowing swipes to the right interferes with some back gestures
|
||||
if (vx > 0) { return super.onInterceptTouchEvent(e) }
|
||||
// Distinguish between scrolling gestures and long presses
|
||||
if (abs(vy) > maxLongPressVelocityY && abs(vx) < minSwipeVelocityX) { return super.onInterceptTouchEvent(e) }
|
||||
// Return false if abs(v.x) > abs(v.y) so that only swipes that are more horizontal than vertical
|
||||
// get passed on to the message view
|
||||
if (abs(vx) > abs(vy)) {
|
||||
return false
|
||||
} else {
|
||||
return super.onInterceptTouchEvent(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(e: MotionEvent): Boolean {
|
||||
when (e.action) {
|
||||
MotionEvent.ACTION_DOWN -> velocityTracker = VelocityTracker.obtain()
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> velocityTracker = null
|
||||
}
|
||||
velocityTracker?.addMovement(e)
|
||||
return super.dispatchTouchEvent(e)
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.album_thumbnail_view.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher
|
||||
import org.thoughtcrime.securesms.longmessage.LongMessageActivity
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AlbumThumbnailView : FrameLayout {
|
||||
|
||||
companion object {
|
||||
const val MAX_ALBUM_DISPLAY_SIZE = 5
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
initialize()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
initialize()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
initialize()
|
||||
}
|
||||
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var slides: List<Slide> = listOf()
|
||||
private var slideSize: Int = 0
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this)
|
||||
}
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas?) {
|
||||
super.dispatchDraw(canvas)
|
||||
cornerMask.mask(canvas)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
|
||||
fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient) {
|
||||
val rawXInt = event.rawX.toInt()
|
||||
val rawYInt = event.rawY.toInt()
|
||||
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||
// Z-check in specific order
|
||||
val testRect = Rect()
|
||||
// test "Read More"
|
||||
albumCellBodyTextReadMore.getGlobalVisibleRect(testRect)
|
||||
if (testRect.contains(eventRect)) {
|
||||
// dispatch to activity view
|
||||
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
||||
LongMessageActivity.getIntent(context, mms.recipient.address, mms.getId(), true)
|
||||
}
|
||||
return
|
||||
}
|
||||
// test each album child
|
||||
albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
|
||||
child.getGlobalVisibleRect(testRect)
|
||||
if (testRect.contains(eventRect)) {
|
||||
// hit intersects with this particular child
|
||||
val slide = slides.getOrNull(index) ?: return
|
||||
// only open to downloaded images
|
||||
if (slide.isInProgress) return
|
||||
|
||||
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
||||
MediaPreviewActivity.getPreviewIntent(context, slide, mms, threadRecipient)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(glideRequests: GlideRequests, message: MmsMessageRecord,
|
||||
isStart: Boolean, isEnd: Boolean) {
|
||||
slides = message.slideDeck.thumbnailSlides
|
||||
if (slides.isEmpty()) {
|
||||
// this should never be encountered because it's checked by parent
|
||||
return
|
||||
}
|
||||
calculateRadius(isStart, isEnd, message.isOutgoing)
|
||||
|
||||
// recreate cell views if different size to what we have already (for recycling)
|
||||
if (slides.size != this.slideSize) {
|
||||
albumCellContainer.removeAllViews()
|
||||
LayoutInflater.from(context).inflate(layoutRes(slides.size), albumCellContainer)
|
||||
val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE
|
||||
albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
|
||||
// overflowText will be null if !overflowed
|
||||
overflowText.isVisible = overflowed // more than max album size
|
||||
overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE)
|
||||
}
|
||||
this.slideSize = slides.size
|
||||
}
|
||||
// iterate binding
|
||||
slides.take(5).forEachIndexed { position, slide ->
|
||||
val thumbnailView = getThumbnailView(position)
|
||||
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
|
||||
}
|
||||
albumCellBodyParent.isVisible = message.body.isNotEmpty()
|
||||
albumCellBodyText.text = message.body
|
||||
post {
|
||||
// post to await layout of text
|
||||
albumCellBodyText.layout?.let { layout ->
|
||||
val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) }
|
||||
?: 0
|
||||
// show read more text if at least one line is ellipsized
|
||||
ViewUtil.setPaddingTop(albumCellBodyTextParent, if (maxEllipsis > 0) resources.getDimension(R.dimen.small_spacing).roundToInt() else resources.getDimension(R.dimen.medium_spacing).roundToInt())
|
||||
albumCellBodyTextReadMore.isVisible = maxEllipsis > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
|
||||
fun layoutRes(slideCount: Int) = when (slideCount) {
|
||||
1 -> R.layout.album_thumbnail_1 // single
|
||||
2 -> R.layout.album_thumbnail_2// two sidebyside
|
||||
3 -> R.layout.album_thumbnail_3// three stacked
|
||||
4 -> R.layout.album_thumbnail_4// four square
|
||||
5 -> R.layout.album_thumbnail_5//
|
||||
else -> R.layout.album_thumbnail_many// five or more
|
||||
}
|
||||
|
||||
fun getThumbnailView(position: Int): KThumbnailView = when (position) {
|
||||
0 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
|
||||
1 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
|
||||
2 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
|
||||
3 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
|
||||
4 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
|
||||
else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position")
|
||||
}
|
||||
|
||||
fun calculateRadius(isStart: Boolean, isEnd: Boolean, outgoing: Boolean) {
|
||||
val roundedDimen = context.resources.getDimension(R.dimen.message_corner_radius).toInt()
|
||||
val collapsedDimen = context.resources.getDimension(R.dimen.message_corner_collapse_radius).toInt()
|
||||
val (startTop, endTop, startBottom, endBottom) = when {
|
||||
// single message, consistent dimen
|
||||
isStart && isEnd -> intArrayOf(roundedDimen, roundedDimen, roundedDimen, roundedDimen)
|
||||
// start of message cluster, collapsed BL
|
||||
isStart -> intArrayOf(roundedDimen, roundedDimen, collapsedDimen, roundedDimen)
|
||||
// end of message cluster, collapsed TL
|
||||
isEnd -> intArrayOf(collapsedDimen, roundedDimen, roundedDimen, roundedDimen)
|
||||
// else in the middle, no rounding left side
|
||||
else -> intArrayOf(collapsedDimen, roundedDimen, collapsedDimen, roundedDimen)
|
||||
}
|
||||
// TL, TR, BR, BL (CW direction)
|
||||
cornerMask.setRadii(
|
||||
if (!outgoing) startTop else endTop, // TL
|
||||
if (!outgoing) endTop else startTop, // TR
|
||||
if (!outgoing) endBottom else startBottom, // BR
|
||||
if (!outgoing) startBottom else endBottom // BL
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
package org.thoughtcrime.securesms.conversation.v2.components;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
@ -118,5 +118,4 @@ public class ExpirationTimerView extends androidx.appcompat.widget.AppCompatImag
|
||||
Util.runOnMainDelayed(this, timerView.calculateAnimationDelay(timerView.startedAt, timerView.expiresIn));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.components
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_link_preview_draft.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
|
||||
class LinkPreviewDraftView : LinearLayout {
|
||||
var delegate: LinkPreviewDraftViewDelegate? = null
|
||||
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
// Start out with the loader showing and the content view hidden
|
||||
LayoutInflater.from(context).inflate(R.layout.view_link_preview_draft, this)
|
||||
linkPreviewDraftContainer.isVisible = false
|
||||
thumbnailImageView.clipToOutline = true
|
||||
linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
||||
}
|
||||
|
||||
fun update(glide: GlideRequests, linkPreview: LinkPreview) {
|
||||
// Hide the loader and show the content view
|
||||
linkPreviewDraftContainer.isVisible = true
|
||||
linkPreviewDraftLoader.isVisible = false
|
||||
thumbnailImageView.radius = toPx(4, resources)
|
||||
if (linkPreview.getThumbnail().isPresent) {
|
||||
// This internally fetches the thumbnail
|
||||
thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false)
|
||||
}
|
||||
linkPreviewDraftTitleTextView.text = linkPreview.title
|
||||
}
|
||||
|
||||
private fun cancel() {
|
||||
delegate?.cancelLinkPreviewDraft()
|
||||
}
|
||||
}
|
||||
|
||||
interface LinkPreviewDraftViewDelegate {
|
||||
|
||||
fun cancelLinkPreviewDraft()
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.views
|
||||
package org.thoughtcrime.securesms.conversation.v2.components
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -7,30 +7,22 @@ import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import kotlinx.android.synthetic.main.view_open_group_guidelines.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
|
||||
class OpenGroupGuidelinesView : FrameLayout {
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
private fun initialize() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null)
|
||||
addView(contentView)
|
||||
readButton.setOnClickListener {
|
||||
val activity = context as ConversationActivity
|
||||
val activity = context as ConversationActivityV2
|
||||
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
|
||||
activity.push(intent)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
package org.thoughtcrime.securesms.conversation.v2.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
@ -13,18 +13,14 @@ import android.widget.LinearLayout;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class TypingIndicatorView extends LinearLayout {
|
||||
private boolean isActive;
|
||||
private long startTime;
|
||||
|
||||
private static final long DURATION = 300;
|
||||
private static final long PRE_DELAY = 500;
|
||||
private static final long POST_DELAY = 500;
|
||||
private static final long CYCLE_DURATION = 1500;
|
||||
private static final long DOT_DURATION = 600;
|
||||
private static final float MIN_ALPHA = 0.4f;
|
||||
private static final float MIN_SCALE = 0.75f;
|
||||
|
||||
private boolean isActive;
|
||||
private long startTime;
|
||||
|
||||
private View dot1;
|
||||
private View dot2;
|
||||
private View dot3;
|
||||
@ -40,7 +36,7 @@ public class TypingIndicatorView extends LinearLayout {
|
||||
}
|
||||
|
||||
private void initialize(@Nullable AttributeSet attrs) {
|
||||
inflate(getContext(), R.layout.typing_indicator_view, this);
|
||||
inflate(getContext(), R.layout.view_typing_indicator, this);
|
||||
|
||||
setWillNotDraw(false);
|
||||
|
@ -0,0 +1,25 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.components
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_conversation_typing_container.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
|
||||
class TypingIndicatorViewContainer : LinearLayout {
|
||||
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_conversation_typing_container, this)
|
||||
}
|
||||
|
||||
fun setTypists(typists: List<Recipient>) {
|
||||
if (typists.isEmpty()) { typingIndicator.stopAnimation(); return }
|
||||
typingIndicator.startAnimation()
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_blocked.view.*
|
||||
import kotlinx.android.synthetic.main.dialog_blocked.view.cancelButton
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
|
||||
/** Shown upon sending a message to a user that's blocked. */
|
||||
class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_blocked, null)
|
||||
val contactDB = DatabaseFactory.getSessionContactDatabase(requireContext())
|
||||
val sessionID = recipient.address.toString()
|
||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
val title = resources.getString(R.string.dialog_blocked_title, name)
|
||||
contentView.blockedTitleTextView.text = title
|
||||
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
contentView.blockedExplanationTextView.text = spannable
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.unblockButton.setOnClickListener { unblock() }
|
||||
builder.setView(contentView)
|
||||
}
|
||||
|
||||
private fun unblock() {
|
||||
DatabaseFactory.getRecipientDatabase(requireContext()).setBlocked(recipient, false)
|
||||
dismiss()
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_download.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
|
||||
/** Shown when receiving media from a contact for the first time, to confirm that
|
||||
* they are to be trusted and files sent by them are to be downloaded. */
|
||||
class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_download, null)
|
||||
val contactDB = DatabaseFactory.getSessionContactDatabase(requireContext())
|
||||
val sessionID = recipient.address.toString()
|
||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
val title = resources.getString(R.string.dialog_download_title, name)
|
||||
contentView.downloadTitleTextView.text = title
|
||||
val explanation = resources.getString(R.string.dialog_download_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
contentView.downloadExplanationTextView.text = spannable
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.downloadButton.setOnClickListener { trust() }
|
||||
builder.setView(contentView)
|
||||
}
|
||||
|
||||
private fun trust() {
|
||||
// TODO: Implement
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
|
||||
/** Shown upon tapping an open group invitation. */
|
||||
class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_join_open_group, null)
|
||||
val title = resources.getString(R.string.dialog_join_open_group_title, name)
|
||||
contentView.joinOpenGroupTitleTextView.text = title
|
||||
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
contentView.joinOpenGroupExplanationTextView.text = spannable
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.joinButton.setOnClickListener { join() }
|
||||
builder.setView(contentView)
|
||||
}
|
||||
|
||||
private fun join() {
|
||||
val openGroup = OpenGroupUrlParser.parseUrl(url)
|
||||
val activity = requireContext() as AppCompatActivity
|
||||
ThreadUtils.queue {
|
||||
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
|
||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(activity)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_link_preview.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
|
||||
/** Shown the first time the user inputs a URL that could generate a link preview, to
|
||||
* let them know that Session offers the ability to send and receive link previews. */
|
||||
class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_link_preview, null)
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.enableLinkPreviewsButton.setOnClickListener { enable() }
|
||||
builder.setView(contentView)
|
||||
}
|
||||
|
||||
private fun enable() {
|
||||
TextSecurePreferences.setLinkPreviewsEnabled(requireContext(), true)
|
||||
dismiss()
|
||||
onEnabled()
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_open_url.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
|
||||
/** Shown upon tapping a URL. */
|
||||
class OpenURLDialog(private val url: String) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_open_url, null)
|
||||
val explanation = resources.getString(R.string.dialog_open_url_explanation, url)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(url)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
contentView.openURLExplanationTextView.text = spannable
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.openURLButton.setOnClickListener { open() }
|
||||
builder.setView(contentView)
|
||||
}
|
||||
|
||||
private fun open() {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
requireContext().startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.input_bar
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.text.InputType
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_input_bar.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftView
|
||||
import org.thoughtcrime.securesms.conversation.v2.components.LinkPreviewDraftViewDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.QuoteView
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.loki.utilities.toDp
|
||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate {
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private val vMargin by lazy { toDp(4, resources) }
|
||||
private val minHeight by lazy { toPx(56, resources) }
|
||||
private var linkPreviewDraftView: LinkPreviewDraftView? = null
|
||||
var delegate: InputBarDelegate? = null
|
||||
var additionalContentHeight = 0
|
||||
var quote: MessageRecord? = null
|
||||
var linkPreview: LinkPreview? = null
|
||||
var showInput: Boolean = true
|
||||
set(value) { field = value; showOrHideInputIfNeeded() }
|
||||
|
||||
var text: String
|
||||
get() { return inputBarEditText.text.toString() }
|
||||
set(value) { inputBarEditText.setText(value) }
|
||||
|
||||
private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) }
|
||||
private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone) }
|
||||
private val sendButton by lazy { InputBarButton(context, R.drawable.ic_arrow_up, true) }
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_input_bar, this)
|
||||
// Attachments button
|
||||
attachmentsButtonContainer.addView(attachmentsButton)
|
||||
attachmentsButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
attachmentsButton.onPress = { toggleAttachmentOptions() }
|
||||
// Microphone button
|
||||
microphoneOrSendButtonContainer.addView(microphoneButton)
|
||||
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
microphoneButton.onLongPress = { startRecordingVoiceMessage() }
|
||||
microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
|
||||
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
|
||||
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }
|
||||
// Send button
|
||||
microphoneOrSendButtonContainer.addView(sendButton)
|
||||
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
sendButton.isVisible = false
|
||||
sendButton.onUp = { delegate?.sendMessage() }
|
||||
// Edit text
|
||||
inputBarEditText.imeOptions = inputBarEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
inputBarEditText.inputType = inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
inputBarEditText.delegate = this
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region General
|
||||
private fun setHeight(newHeight: Int) {
|
||||
val layoutParams = inputBarLinearLayout.layoutParams as LayoutParams
|
||||
layoutParams.height = newHeight
|
||||
inputBarLinearLayout.layoutParams = layoutParams
|
||||
delegate?.inputBarHeightChanged(newHeight)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
override fun inputBarEditTextContentChanged(text: CharSequence) {
|
||||
sendButton.isVisible = text.isNotEmpty()
|
||||
microphoneButton.isVisible = text.isEmpty()
|
||||
delegate?.inputBarEditTextContentChanged(text)
|
||||
}
|
||||
|
||||
override fun inputBarEditTextHeightChanged(newValue: Int) {
|
||||
val newHeight = max(newValue + 2 * vMargin, minHeight) + inputBarAdditionalContentContainer.height
|
||||
setHeight(newHeight)
|
||||
}
|
||||
|
||||
private fun toggleAttachmentOptions() {
|
||||
delegate?.toggleAttachmentOptions()
|
||||
}
|
||||
|
||||
private fun startRecordingVoiceMessage() {
|
||||
delegate?.startRecordingVoiceMessage()
|
||||
}
|
||||
|
||||
// Drafting quotes and drafting link previews is mutually exclusive, i.e. you can't draft
|
||||
// a quote and a link preview at the same time.
|
||||
|
||||
fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) {
|
||||
quote = message
|
||||
linkPreview = null
|
||||
linkPreviewDraftView = null
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
val quoteView = QuoteView(context, QuoteView.Mode.Draft)
|
||||
quoteView.delegate = this
|
||||
inputBarAdditionalContentContainer.addView(quoteView)
|
||||
val attachments = (message as? MmsMessageRecord)?.slideDeck
|
||||
// The max content width is the screen width - 2 times the horizontal input bar padding - the
|
||||
// quote view content area's start and end margins. This unfortunately has to be calculated manually
|
||||
// here to get the layout right.
|
||||
val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt()
|
||||
val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize()
|
||||
quoteView.bind(sender, message.body, attachments,
|
||||
thread, true, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide)
|
||||
// The 6 DP below is the padding the quote view applies to itself, which isn't included in the
|
||||
// intrinsic height calculation.
|
||||
val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources)
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + quoteViewIntrinsicHeight
|
||||
additionalContentHeight = quoteViewIntrinsicHeight
|
||||
setHeight(newHeight)
|
||||
}
|
||||
|
||||
override fun cancelQuoteDraft() {
|
||||
quote = null
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
|
||||
additionalContentHeight = 0
|
||||
setHeight(newHeight)
|
||||
}
|
||||
|
||||
fun draftLinkPreview() {
|
||||
quote = null
|
||||
val linkPreviewDraftHeight = toPx(88, resources)
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
val linkPreviewDraftView = LinkPreviewDraftView(context)
|
||||
linkPreviewDraftView.delegate = this
|
||||
this.linkPreviewDraftView = linkPreviewDraftView
|
||||
inputBarAdditionalContentContainer.addView(linkPreviewDraftView)
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + linkPreviewDraftHeight
|
||||
additionalContentHeight = linkPreviewDraftHeight
|
||||
setHeight(newHeight)
|
||||
}
|
||||
|
||||
fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) {
|
||||
this.linkPreview = linkPreview
|
||||
val linkPreviewDraftView = this.linkPreviewDraftView ?: return
|
||||
linkPreviewDraftView.update(glide, linkPreview)
|
||||
}
|
||||
|
||||
override fun cancelLinkPreviewDraft() {
|
||||
if (quote != null) { return }
|
||||
linkPreview = null
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
|
||||
additionalContentHeight = 0
|
||||
setHeight(newHeight)
|
||||
}
|
||||
|
||||
private fun showOrHideInputIfNeeded() {
|
||||
if (showInput) {
|
||||
setOf( inputBarEditText, attachmentsButton ).forEach { it.isVisible = true }
|
||||
microphoneButton.isVisible = text.isEmpty()
|
||||
sendButton.isVisible = text.isNotEmpty()
|
||||
} else {
|
||||
cancelQuoteDraft()
|
||||
cancelLinkPreviewDraft()
|
||||
val views = setOf( inputBarEditText, attachmentsButton, microphoneButton, sendButton )
|
||||
views.forEach { it.isVisible = false }
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
interface InputBarDelegate {
|
||||
|
||||
fun inputBarHeightChanged(newValue: Int)
|
||||
fun inputBarEditTextContentChanged(newContent: CharSequence)
|
||||
fun toggleAttachmentOptions()
|
||||
fun showVoiceMessageUI()
|
||||
fun startRecordingVoiceMessage()
|
||||
fun onMicrophoneButtonMove(event: MotionEvent)
|
||||
fun onMicrophoneButtonCancel(event: MotionEvent)
|
||||
fun onMicrophoneButtonUp(event: MotionEvent)
|
||||
fun sendMessage()
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.input_bar
|
||||
|
||||
import android.animation.PointFEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.PointF
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.DrawableRes
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||
import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.loki.views.InputBarButtonImageViewContainer
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
class InputBarButton : RelativeLayout {
|
||||
private val gestureHandler = Handler(Looper.getMainLooper())
|
||||
private var isSendButton = false
|
||||
private var hasOpaqueBackground = false
|
||||
private var isGIFButton = false
|
||||
@DrawableRes private var iconID = 0
|
||||
private var longPressCallback: Runnable? = null
|
||||
private var onDownTimestamp = 0L
|
||||
var snIsEnabled = true
|
||||
var onPress: (() -> Unit)? = null
|
||||
var onMove: ((MotionEvent) -> Unit)? = null
|
||||
var onCancel: ((MotionEvent) -> Unit)? = null
|
||||
var onUp: ((MotionEvent) -> Unit)? = null
|
||||
var onLongPress: (() -> Unit)? = null
|
||||
|
||||
companion object {
|
||||
const val animationDuration = 250.toLong()
|
||||
const val longPressDurationThreshold = 250L // ms
|
||||
}
|
||||
|
||||
private val expandedImageViewPosition by lazy { PointF(0.0f, 0.0f) }
|
||||
private val collapsedImageViewPosition by lazy { PointF((expandedSize - collapsedSize) / 2, (expandedSize - collapsedSize) / 2) }
|
||||
private val colorID by lazy {
|
||||
if (hasOpaqueBackground) {
|
||||
R.color.input_bar_button_background_opaque
|
||||
} else if (isSendButton) {
|
||||
R.color.accent
|
||||
} else {
|
||||
R.color.input_bar_button_background
|
||||
}
|
||||
}
|
||||
|
||||
val expandedSize by lazy { resources.getDimension(R.dimen.input_bar_button_expanded_size) }
|
||||
val collapsedSize by lazy { resources.getDimension(R.dimen.input_bar_button_collapsed_size) }
|
||||
|
||||
private val imageViewContainer by lazy {
|
||||
val result = InputBarButtonImageViewContainer(context)
|
||||
val size = collapsedSize.toInt()
|
||||
result.layoutParams = LayoutParams(size, size)
|
||||
result.setBackgroundResource(R.drawable.input_bar_button_background)
|
||||
result.mainColor = resources.getColorWithID(colorID, context.theme)
|
||||
if (hasOpaqueBackground) {
|
||||
result.strokeColor = resources.getColorWithID(R.color.input_bar_button_background_opaque_border, context.theme)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
private val imageView by lazy {
|
||||
val result = ImageView(context)
|
||||
val size = if (isGIFButton) toPx(24, resources) else toPx(16, resources)
|
||||
result.layoutParams = LayoutParams(size, size)
|
||||
result.scaleType = ImageView.ScaleType.CENTER_INSIDE
|
||||
result.setImageResource(iconID)
|
||||
val colorID = if (isSendButton) R.color.black else R.color.text
|
||||
result.imageTintList = ColorStateList.valueOf(resources.getColorWithID(colorID, context.theme))
|
||||
result
|
||||
}
|
||||
|
||||
constructor(context: Context) : super(context) { throw IllegalAccessException("Use InputBarButton(context:iconID:) instead.") }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { throw IllegalAccessException("Use InputBarButton(context:iconID:) instead.") }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { throw IllegalAccessException("Use InputBarButton(context:iconID:) instead.") }
|
||||
|
||||
constructor(context: Context, @DrawableRes iconID: Int, isSendButton: Boolean = false,
|
||||
hasOpaqueBackground: Boolean = false, isGIFButton: Boolean = false) : super(context) {
|
||||
this.isSendButton = isSendButton
|
||||
this.iconID = iconID
|
||||
this.hasOpaqueBackground = hasOpaqueBackground
|
||||
this.isGIFButton = isGIFButton
|
||||
val size = resources.getDimension(R.dimen.input_bar_button_expanded_size).toInt()
|
||||
val layoutParams = LayoutParams(size, size)
|
||||
this.layoutParams = layoutParams
|
||||
addView(imageViewContainer)
|
||||
imageViewContainer.x = collapsedImageViewPosition.x
|
||||
imageViewContainer.y = collapsedImageViewPosition.y
|
||||
imageViewContainer.addView(imageView)
|
||||
val imageViewLayoutParams = imageView.layoutParams as LayoutParams
|
||||
imageViewLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT)
|
||||
imageView.layoutParams = imageViewLayoutParams
|
||||
gravity = Gravity.TOP or Gravity.LEFT // Intentionally not Gravity.START
|
||||
isHapticFeedbackEnabled = true
|
||||
}
|
||||
|
||||
fun expand() {
|
||||
GlowViewUtilities.animateColorChange(context, imageViewContainer, colorID, R.color.accent)
|
||||
imageViewContainer.animateSizeChange(R.dimen.input_bar_button_collapsed_size, R.dimen.input_bar_button_expanded_size, animationDuration)
|
||||
animateImageViewContainerPositionChange(collapsedImageViewPosition, expandedImageViewPosition)
|
||||
}
|
||||
|
||||
fun collapse() {
|
||||
GlowViewUtilities.animateColorChange(context, imageViewContainer, R.color.accent, colorID)
|
||||
imageViewContainer.animateSizeChange(R.dimen.input_bar_button_expanded_size, R.dimen.input_bar_button_collapsed_size, animationDuration)
|
||||
animateImageViewContainerPositionChange(expandedImageViewPosition, collapsedImageViewPosition)
|
||||
}
|
||||
|
||||
private fun animateImageViewContainerPositionChange(startPosition: PointF, endPosition: PointF) {
|
||||
val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition)
|
||||
animation.duration = animationDuration
|
||||
animation.addUpdateListener { animator ->
|
||||
val point = animator.animatedValue as PointF
|
||||
imageViewContainer.x = point.x
|
||||
imageViewContainer.y = point.y
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (!snIsEnabled) { return false }
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> onDown(event)
|
||||
MotionEvent.ACTION_MOVE -> onMove(event)
|
||||
MotionEvent.ACTION_UP -> onUp(event)
|
||||
MotionEvent.ACTION_CANCEL -> onCancel(event)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onDown(event: MotionEvent) {
|
||||
expand()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||
} else {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
}
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
val newLongPressCallback = Runnable { onLongPress?.invoke() }
|
||||
this.longPressCallback = newLongPressCallback
|
||||
gestureHandler.postDelayed(newLongPressCallback, InputBarButton.longPressDurationThreshold)
|
||||
onDownTimestamp = Date().time
|
||||
}
|
||||
|
||||
private fun onMove(event: MotionEvent) {
|
||||
onMove?.invoke(event)
|
||||
}
|
||||
|
||||
private fun onCancel(event: MotionEvent) {
|
||||
onCancel?.invoke(event)
|
||||
collapse()
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
}
|
||||
|
||||
private fun onUp(event: MotionEvent) {
|
||||
onUp?.invoke(event)
|
||||
collapse()
|
||||
if ((Date().time - onDownTimestamp) < InputBarButton.longPressDurationThreshold) {
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
onPress?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.input_bar
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class InputBarEditText : AppCompatEditText {
|
||||
private val screenWidth get() = Resources.getSystem().displayMetrics.widthPixels
|
||||
var delegate: InputBarEditTextDelegate? = null
|
||||
|
||||
private val snMinHeight = toPx(40.0f, resources)
|
||||
private val snMaxHeight = toPx(80.0f, resources)
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun onTextChanged(text: CharSequence, start: Int, lengthBefore: Int, lengthAfter: Int) {
|
||||
super.onTextChanged(text, start, lengthBefore, lengthAfter)
|
||||
delegate?.inputBarEditTextContentChanged(text)
|
||||
// Calculate the width manually to get it right even before layout has happened (i.e.
|
||||
// when restoring a draft). The 64 DP is the horizontal margin around the input bar
|
||||
// edit text.
|
||||
val width = (screenWidth - 2 * toPx(64.0f, resources)).roundToInt()
|
||||
if (width < 0) { return } // screenWidth initially evaluates to 0
|
||||
val height = TextUtilities.getIntrinsicHeight(text, paint, width).toFloat()
|
||||
val constrainedHeight = min(max(height, snMinHeight), snMaxHeight)
|
||||
if (constrainedHeight.roundToInt() == this.height) { return }
|
||||
val layoutParams = this.layoutParams as? RelativeLayout.LayoutParams ?: return
|
||||
layoutParams.height = constrainedHeight.roundToInt()
|
||||
this.layoutParams = layoutParams
|
||||
delegate?.inputBarEditTextHeightChanged(constrainedHeight.roundToInt())
|
||||
}
|
||||
}
|
||||
|
||||
interface InputBarEditTextDelegate {
|
||||
|
||||
fun inputBarEditTextContentChanged(text: CharSequence)
|
||||
fun inputBarEditTextHeightChanged(newValue: Int)
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.input_bar
|
||||
|
||||
import android.animation.FloatEvaluator
|
||||
import android.animation.IntEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.loki.utilities.animateSizeChange
|
||||
import org.thoughtcrime.securesms.loki.utilities.disableClipping
|
||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.*
|
||||
|
||||
class InputBarRecordingView : RelativeLayout {
|
||||
private var startTimestamp = 0L
|
||||
private val snHandler = Handler(Looper.getMainLooper())
|
||||
private var dotViewAnimation: ValueAnimator? = null
|
||||
private var pulseAnimation: ValueAnimator? = null
|
||||
var delegate: InputBarRecordingViewDelegate? = null
|
||||
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_input_bar_recording, this)
|
||||
inputBarMiddleContentContainer.disableClipping()
|
||||
inputBarCancelButton.setOnClickListener { hide() }
|
||||
}
|
||||
|
||||
fun show() {
|
||||
startTimestamp = Date().time
|
||||
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
||||
inputBarCancelButton.alpha = 0.0f
|
||||
inputBarMiddleContentContainer.alpha = 1.0f
|
||||
lockView.alpha = 1.0f
|
||||
isVisible = true
|
||||
alpha = 0.0f
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||
animation.duration = 250L
|
||||
animation.addUpdateListener { animator ->
|
||||
alpha = animator.animatedValue as Float
|
||||
}
|
||||
animation.start()
|
||||
animateDotView()
|
||||
pulse()
|
||||
animateLockViewUp()
|
||||
updateTimer()
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
alpha = 1.0f
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||
animation.duration = 250L
|
||||
animation.addUpdateListener { animator ->
|
||||
alpha = animator.animatedValue as Float
|
||||
if (animator.animatedFraction == 1.0f) {
|
||||
isVisible = false
|
||||
dotViewAnimation?.repeatCount = 0
|
||||
pulseAnimation?.removeAllUpdateListeners()
|
||||
}
|
||||
}
|
||||
animation.start()
|
||||
delegate?.handleVoiceMessageUIHidden()
|
||||
}
|
||||
|
||||
private fun animateDotView() {
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||
dotViewAnimation = animation
|
||||
animation.duration = 500L
|
||||
animation.addUpdateListener { animator ->
|
||||
dotView.alpha = animator.animatedValue as Float
|
||||
}
|
||||
animation.repeatCount = ValueAnimator.INFINITE
|
||||
animation.repeatMode = ValueAnimator.REVERSE
|
||||
animation.start()
|
||||
}
|
||||
|
||||
private fun pulse() {
|
||||
val collapsedSize = toPx(80.0f, resources)
|
||||
val expandedSize = toPx(104.0f, resources)
|
||||
pulseView.animateSizeChange(collapsedSize, expandedSize, 1000)
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.5, 0.0f)
|
||||
pulseAnimation = animation
|
||||
animation.duration = 1000L
|
||||
animation.addUpdateListener { animator ->
|
||||
pulseView.alpha = animator.animatedValue as Float
|
||||
if (animator.animatedFraction == 1.0f && isVisible) { pulse() }
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
private fun animateLockViewUp() {
|
||||
val startMarginBottom = toPx(32, resources)
|
||||
val endMarginBottom = toPx(72, resources)
|
||||
val layoutParams = lockView.layoutParams as LayoutParams
|
||||
layoutParams.bottomMargin = startMarginBottom
|
||||
lockView.layoutParams = layoutParams
|
||||
val animation = ValueAnimator.ofObject(IntEvaluator(), startMarginBottom, endMarginBottom)
|
||||
animation.duration = 250L
|
||||
animation.addUpdateListener { animator ->
|
||||
layoutParams.bottomMargin = animator.animatedValue as Int
|
||||
lockView.layoutParams = layoutParams
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
private fun updateTimer() {
|
||||
val duration = (Date().time - startTimestamp) / 1000L
|
||||
recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||
snHandler.postDelayed({ updateTimer() }, 500)
|
||||
}
|
||||
|
||||
fun lock() {
|
||||
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||
fadeOutAnimation.duration = 250L
|
||||
fadeOutAnimation.addUpdateListener { animator ->
|
||||
inputBarMiddleContentContainer.alpha = animator.animatedValue as Float
|
||||
lockView.alpha = animator.animatedValue as Float
|
||||
}
|
||||
fadeOutAnimation.start()
|
||||
val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||
fadeInAnimation.duration = 250L
|
||||
fadeInAnimation.addUpdateListener { animator ->
|
||||
inputBarCancelButton.alpha = animator.animatedValue as Float
|
||||
}
|
||||
fadeInAnimation.start()
|
||||
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
|
||||
recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
|
||||
inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }
|
||||
}
|
||||
}
|
||||
|
||||
interface InputBarRecordingViewDelegate {
|
||||
|
||||
fun handleVoiceMessageUIHidden()
|
||||
fun sendVoiceMessage()
|
||||
fun cancelVoiceMessage()
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.mentions.Mention
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : RelativeLayout(context, attrs, defStyleAttr) {
|
||||
var candidate = Mention("", "")
|
||||
set(newValue) { field = newValue; update() }
|
||||
var glide: GlideRequests? = null
|
||||
var openGroupServer: String? = null
|
||||
var openGroupRoom: String? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context) : this(context, null)
|
||||
|
||||
companion object {
|
||||
|
||||
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView {
|
||||
return layoutInflater.inflate(R.layout.view_mention_candidate_v2, parent, false) as MentionCandidateView
|
||||
}
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
mentionCandidateNameTextView.text = candidate.displayName
|
||||
profilePictureView.publicKey = candidate.publicKey
|
||||
profilePictureView.displayName = candidate.displayName
|
||||
profilePictureView.additionalPublicKey = null
|
||||
profilePictureView.glide = glide!!
|
||||
profilePictureView.update()
|
||||
if (openGroupServer != null && openGroupRoom != null) {
|
||||
val isUserModerator = OpenGroupAPIV2.isUserModerator(candidate.publicKey, openGroupRoom!!, openGroupServer!!)
|
||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||
} else {
|
||||
moderatorIconImageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.ListView
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.session.libsession.messaging.mentions.Mention
|
||||
|
||||
class MentionCandidatesView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
|
||||
private var candidates = listOf<Mention>()
|
||||
set(newValue) { field = newValue; snAdapter.candidates = newValue }
|
||||
var glide: GlideRequests? = null
|
||||
set(newValue) { field = newValue; snAdapter.glide = newValue }
|
||||
var openGroupServer: String? = null
|
||||
set(newValue) { field = newValue; snAdapter.openGroupServer = openGroupServer }
|
||||
var openGroupRoom: String? = null
|
||||
set(newValue) { field = newValue; snAdapter.openGroupRoom = openGroupRoom }
|
||||
var onCandidateSelected: ((Mention) -> Unit)? = null
|
||||
|
||||
private val snAdapter by lazy { Adapter(context) }
|
||||
|
||||
private class Adapter(private val context: Context) : BaseAdapter() {
|
||||
var candidates = listOf<Mention>()
|
||||
set(newValue) { field = newValue; notifyDataSetChanged() }
|
||||
var glide: GlideRequests? = null
|
||||
var openGroupServer: String? = null
|
||||
var openGroupRoom: String? = null
|
||||
|
||||
override fun getCount(): Int { return candidates.count() }
|
||||
override fun getItemId(position: Int): Long { return position.toLong() }
|
||||
override fun getItem(position: Int): Mention { return candidates[position] }
|
||||
|
||||
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
|
||||
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent)
|
||||
val mentionCandidate = getItem(position)
|
||||
cell.glide = glide
|
||||
cell.candidate = mentionCandidate
|
||||
cell.openGroupServer = openGroupServer
|
||||
cell.openGroupRoom = openGroupRoom
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context) : this(context, null)
|
||||
|
||||
init {
|
||||
clipToOutline = true
|
||||
adapter = snAdapter
|
||||
snAdapter.candidates = candidates
|
||||
setOnItemClickListener { _, _, position, _ ->
|
||||
onCandidateSelected?.invoke(candidates[position])
|
||||
}
|
||||
}
|
||||
|
||||
fun show(candidates: List<Mention>, threadID: Long) {
|
||||
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||
if (openGroup != null) {
|
||||
openGroupServer = openGroup.server
|
||||
openGroupRoom = openGroup.room
|
||||
}
|
||||
setMentionCandidates(candidates)
|
||||
}
|
||||
|
||||
fun setMentionCandidates(candidates: List<Mention>) {
|
||||
this.candidates = candidates
|
||||
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
|
||||
layoutParams.height = toPx(Math.min(candidates.count(), 4) * 44, resources)
|
||||
this.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
|
||||
layoutParams.height = 0
|
||||
this.layoutParams = layoutParams
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.menus
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long,
|
||||
private val context: Context) : ActionMode.Callback {
|
||||
var delegate: ConversationActionModeCallbackDelegate? = null
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val inflater = mode.menuInflater
|
||||
inflater.inflate(R.menu.menu_conversation_item_action, menu)
|
||||
updateActionModeMenu(menu)
|
||||
return true
|
||||
}
|
||||
|
||||
fun updateActionModeMenu(menu: Menu) {
|
||||
// Prepare
|
||||
val selectedItems = adapter.selectedItems
|
||||
val containsControlMessage = selectedItems.any { it.isUpdate }
|
||||
val hasText = selectedItems.any { it.body.isNotEmpty() }
|
||||
if (selectedItems.isEmpty()) { return }
|
||||
val firstMessage = selectedItems.iterator().next()
|
||||
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
fun userCanDeleteSelectedItems(): Boolean {
|
||||
if (openGroup == null) { return true }
|
||||
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
|
||||
if (allSentByCurrentUser) { return true }
|
||||
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
|
||||
}
|
||||
fun userCanBanSelectedUsers(): Boolean {
|
||||
if (openGroup == null) { return false }
|
||||
val anySentByCurrentUser = selectedItems.any { it.isOutgoing }
|
||||
if (anySentByCurrentUser) { return false } // Users can't ban themselves
|
||||
val selectedUsers = selectedItems.map { it.recipient.address.toString() }.toSet()
|
||||
if (selectedUsers.size > 1) { return false }
|
||||
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
|
||||
}
|
||||
// Delete message
|
||||
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()
|
||||
// Ban user
|
||||
menu.findItem(R.id.menu_context_ban_user).isVisible = userCanBanSelectedUsers()
|
||||
// Copy message text
|
||||
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
|
||||
// Copy Session ID
|
||||
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
||||
(openGroup != null && selectedItems.size == 1 && firstMessage.recipient.address.toString() != userPublicKey)
|
||||
// Resend
|
||||
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
|
||||
// Save media
|
||||
menu.findItem(R.id.menu_context_save_attachment).isVisible = (selectedItems.size == 1
|
||||
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
|
||||
// Reply
|
||||
menu.findItem(R.id.menu_context_reply).isVisible =
|
||||
(selectedItems.size == 1 && !firstMessage.isPending && !firstMessage.isFailed)
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
val selectedItems = adapter.selectedItems
|
||||
when (item.itemId) {
|
||||
R.id.menu_context_delete_message -> delegate?.deleteMessages(selectedItems)
|
||||
R.id.menu_context_ban_user -> delegate?.banUser(selectedItems)
|
||||
R.id.menu_context_copy -> delegate?.copyMessages(selectedItems)
|
||||
R.id.menu_context_copy_public_key -> delegate?.copySessionID(selectedItems)
|
||||
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
|
||||
R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems)
|
||||
R.id.menu_context_reply -> delegate?.reply(selectedItems)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
adapter.selectedItems.clear()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
interface ConversationActionModeCallbackDelegate {
|
||||
|
||||
fun deleteMessages(messages: Set<MessageRecord>)
|
||||
fun banUser(messages: Set<MessageRecord>)
|
||||
fun copyMessages(messages: Set<MessageRecord>)
|
||||
fun copySessionID(messages: Set<MessageRecord>)
|
||||
fun resendMessage(messages: Set<MessageRecord>)
|
||||
fun saveAttachment(messages: Set<MessageRecord>)
|
||||
fun reply(messages: Set<MessageRecord>)
|
||||
}
|
@ -0,0 +1,328 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.menus
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.AsyncTask
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.leave
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.*
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity
|
||||
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity.Companion.groupIDKey
|
||||
import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity
|
||||
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import java.io.IOException
|
||||
|
||||
object ConversationMenuHelper {
|
||||
|
||||
fun onPrepareOptionsMenu(menu: Menu, inflater: MenuInflater, thread: Recipient, threadId: Long, context: Context, onOptionsItemSelected: (MenuItem) -> Unit) {
|
||||
// Prepare
|
||||
menu.clear()
|
||||
val isOpenGroup = thread.isOpenGroupRecipient
|
||||
// Base menu (options that should always be present)
|
||||
inflater.inflate(R.menu.menu_conversation, menu)
|
||||
// Expiring messages
|
||||
if (!isOpenGroup) {
|
||||
if (thread.expireMessages > 0) {
|
||||
inflater.inflate(R.menu.menu_conversation_expiration_on, menu)
|
||||
val item = menu.findItem(R.id.menu_expiring_messages)
|
||||
val actionView = item.actionView
|
||||
val iconView = actionView.findViewById<ImageView>(R.id.menu_badge_icon)
|
||||
val badgeView = actionView.findViewById<TextView>(R.id.expiration_badge)
|
||||
@ColorInt val color = context.resources.getColorWithID(R.color.text, context.theme)
|
||||
iconView.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)
|
||||
badgeView.text = ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, thread.expireMessages)
|
||||
actionView.setOnClickListener { onOptionsItemSelected(item) }
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_conversation_expiration_off, menu)
|
||||
}
|
||||
}
|
||||
// One-on-one chat menu (options that should only be present for one-on-one chats)
|
||||
if (thread.isContactRecipient) {
|
||||
if (thread.isBlocked) {
|
||||
inflater.inflate(R.menu.menu_conversation_unblock, menu)
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_conversation_block, menu)
|
||||
}
|
||||
inflater.inflate(R.menu.menu_conversation_copy_session_id, menu)
|
||||
}
|
||||
// Closed group menu (options that should only be present in closed groups)
|
||||
if (thread.isClosedGroupRecipient) {
|
||||
inflater.inflate(R.menu.menu_conversation_closed_group, menu)
|
||||
}
|
||||
// Open group menu
|
||||
if (isOpenGroup) {
|
||||
inflater.inflate(R.menu.menu_conversation_open_group, menu)
|
||||
}
|
||||
// Muting
|
||||
if (thread.isMuted) {
|
||||
inflater.inflate(R.menu.menu_conversation_muted, menu)
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_conversation_unmuted, menu)
|
||||
}
|
||||
|
||||
// Search
|
||||
val searchViewItem = menu.findItem(R.id.menu_search)
|
||||
(context as ConversationActivityV2).searchViewItem = searchViewItem
|
||||
val searchView = searchViewItem.actionView as SearchView
|
||||
val searchViewModel = context.searchViewModel!!
|
||||
val queryListener = object : OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
searchViewModel.onQueryUpdated(query, threadId)
|
||||
context.searchBottomBar.showLoading()
|
||||
context.onSearchQueryUpdated(query)
|
||||
return true
|
||||
}
|
||||
}
|
||||
searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
searchView.setOnQueryTextListener(queryListener)
|
||||
searchViewModel.onSearchOpened()
|
||||
context.searchBottomBar.visibility = View.VISIBLE
|
||||
context.searchBottomBar.setData(0, 0)
|
||||
context.inputBar.visibility = View.GONE
|
||||
for (i in 0 until menu.size()) {
|
||||
if (menu.getItem(i) != searchViewItem) {
|
||||
menu.getItem(i).isVisible = false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
searchView.setOnQueryTextListener(null)
|
||||
searchViewModel.onSearchClosed()
|
||||
context.searchBottomBar.visibility = View.GONE
|
||||
context.inputBar.visibility = View.VISIBLE
|
||||
context.onSearchQueryUpdated(null)
|
||||
context.invalidateOptionsMenu()
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun onOptionItemSelected(context: Context, item: MenuItem, thread: Recipient): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_view_all_media -> { showAllMedia(context, thread) }
|
||||
R.id.menu_search -> { search(context) }
|
||||
R.id.menu_add_shortcut -> { addShortcut(context, thread) }
|
||||
R.id.menu_expiring_messages -> { showExpiringMessagesDialog(context, thread) }
|
||||
R.id.menu_expiring_messages_off -> { showExpiringMessagesDialog(context, thread) }
|
||||
R.id.menu_unblock -> { unblock(context, thread) }
|
||||
R.id.menu_block -> { block(context, thread) }
|
||||
R.id.menu_copy_session_id -> { copySessionID(context, thread) }
|
||||
R.id.menu_edit_group -> { editClosedGroup(context, thread) }
|
||||
R.id.menu_leave_group -> { leaveClosedGroup(context, thread) }
|
||||
R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) }
|
||||
R.id.menu_unmute_notifications -> { unmute(context, thread) }
|
||||
R.id.menu_mute_notifications -> { mute(context, thread) }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showAllMedia(context: Context, thread: Recipient) {
|
||||
val intent = Intent(context, MediaOverviewActivity::class.java)
|
||||
intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, thread.address)
|
||||
val activity = context as AppCompatActivity
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun search(context: Context) {
|
||||
val searchViewModel = (context as ConversationActivityV2).searchViewModel!!
|
||||
searchViewModel.onSearchOpened()
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private fun addShortcut(context: Context, thread: Recipient) {
|
||||
object : AsyncTask<Void?, Void?, IconCompat?>() {
|
||||
|
||||
override fun doInBackground(vararg params: Void?): IconCompat? {
|
||||
var icon: IconCompat? = null
|
||||
val contactPhoto = thread.contactPhoto
|
||||
if (contactPhoto != null) {
|
||||
try {
|
||||
var bitmap = BitmapFactory.decodeStream(contactPhoto.openInputStream(context))
|
||||
bitmap = BitmapUtil.createScaledBitmap(bitmap, 300, 300)
|
||||
icon = IconCompat.createWithAdaptiveBitmap(bitmap)
|
||||
} catch (e: IOException) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
if (icon == null) {
|
||||
icon = IconCompat.createWithResource(context, if (thread.isGroupRecipient) R.mipmap.ic_group_shortcut else R.mipmap.ic_person_shortcut)
|
||||
}
|
||||
return icon
|
||||
}
|
||||
|
||||
override fun onPostExecute(icon: IconCompat?) {
|
||||
val name = Optional.fromNullable<String>(thread.name)
|
||||
.or(Optional.fromNullable<String>(thread.profileName))
|
||||
.or(thread.toShortString())
|
||||
val shortcutInfo = ShortcutInfoCompat.Builder(context, thread.address.serialize() + '-' + System.currentTimeMillis())
|
||||
.setShortLabel(name)
|
||||
.setIcon(icon)
|
||||
.setIntent(ShortcutLauncherActivity.createIntent(context, thread.address))
|
||||
.build()
|
||||
if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) {
|
||||
Toast.makeText(context, context.resources.getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
private fun showExpiringMessagesDialog(context: Context, thread: Recipient) {
|
||||
if (thread.isClosedGroupRecipient) {
|
||||
val group = DatabaseFactory.getGroupDatabase(context).getGroup(thread.address.toGroupString()).orNull()
|
||||
if (group?.isActive == false) { return }
|
||||
}
|
||||
ExpirationDialog.show(context, thread.expireMessages) { expirationTime: Int ->
|
||||
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(thread, expirationTime)
|
||||
val message = ExpirationTimerUpdate(expirationTime)
|
||||
message.recipient = thread.address.serialize()
|
||||
message.sentTimestamp = System.currentTimeMillis()
|
||||
val expiringMessageManager = ApplicationContext.getInstance(context).expiringMessageManager
|
||||
expiringMessageManager.setExpirationTimer(message)
|
||||
MessageSender.send(message, thread.address)
|
||||
val activity = context as AppCompatActivity
|
||||
activity.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun unblock(context: Context, thread: Recipient) {
|
||||
if (!thread.isContactRecipient) { return }
|
||||
val title = R.string.ConversationActivity_unblock_this_contact_question
|
||||
val message = R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ConversationActivity_unblock) { _, _ ->
|
||||
DatabaseFactory.getRecipientDatabase(context)
|
||||
.setBlocked(thread, false)
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun block(context: Context, thread: Recipient) {
|
||||
if (!thread.isContactRecipient) { return }
|
||||
val title = R.string.RecipientPreferenceActivity_block_this_contact_question
|
||||
val message = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { _, _ ->
|
||||
DatabaseFactory.getRecipientDatabase(context)
|
||||
.setBlocked(thread, true)
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun copySessionID(context: Context, thread: Recipient) {
|
||||
if (!thread.isContactRecipient) { return }
|
||||
val sessionID = thread.address.toString()
|
||||
val clip = ClipData.newPlainText("Session ID", sessionID)
|
||||
val activity = context as AppCompatActivity
|
||||
val manager = activity.getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
manager.setPrimaryClip(clip)
|
||||
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun editClosedGroup(context: Context, thread: Recipient) {
|
||||
if (!thread.isClosedGroupRecipient) { return }
|
||||
val intent = Intent(context, EditClosedGroupActivity::class.java)
|
||||
val groupID: String = thread.address.toGroupString()
|
||||
intent.putExtra(groupIDKey, groupID)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun leaveClosedGroup(context: Context, thread: Recipient) {
|
||||
if (!thread.isClosedGroupRecipient) { return }
|
||||
val builder = AlertDialog.Builder(context)
|
||||
builder.setTitle(context.resources.getString(R.string.ConversationActivity_leave_group))
|
||||
builder.setCancelable(true)
|
||||
val group = DatabaseFactory.getGroupDatabase(context).getGroup(thread.address.toGroupString()).orNull()
|
||||
val admins = group.admins
|
||||
val sessionID = TextSecurePreferences.getLocalNumber(context)
|
||||
val isCurrentUserAdmin = admins.any { it.toString() == sessionID }
|
||||
val message = if (isCurrentUserAdmin) {
|
||||
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
||||
} else {
|
||||
context.resources.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)
|
||||
}
|
||||
builder.setMessage(message)
|
||||
builder.setPositiveButton(R.string.yes) { _, _ ->
|
||||
var groupPublicKey: String?
|
||||
var isClosedGroup: Boolean
|
||||
try {
|
||||
groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
|
||||
isClosedGroup = DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey)
|
||||
} catch (e: IOException) {
|
||||
groupPublicKey = null
|
||||
isClosedGroup = false
|
||||
}
|
||||
try {
|
||||
if (isClosedGroup) {
|
||||
MessageSender.leave(groupPublicKey!!, true)
|
||||
} else {
|
||||
Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
builder.setNegativeButton(R.string.no, null)
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun inviteContacts(context: Context, thread: Recipient) {
|
||||
if (!thread.isOpenGroupRecipient) { return }
|
||||
val intent = Intent(context, SelectContactsActivity::class.java)
|
||||
val activity = context as AppCompatActivity
|
||||
activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS)
|
||||
}
|
||||
|
||||
private fun unmute(context: Context, thread: Recipient) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setMuted(thread, 0)
|
||||
}
|
||||
|
||||
private fun mute(context: Context, thread: Recipient) {
|
||||
MuteDialog.show(context) { until: Long ->
|
||||
DatabaseFactory.getRecipientDatabase(context).setMuted(thread, until)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.view_control_message.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
class ControlMessageView : LinearLayout {
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_control_message, this)
|
||||
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord) {
|
||||
iconImageView.visibility = View.GONE
|
||||
if (message.isExpirationTimerUpdate) {
|
||||
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme))
|
||||
iconImageView.visibility = View.VISIBLE
|
||||
} else if (message.isMediaSavedNotification) {
|
||||
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme))
|
||||
iconImageView.visibility = View.VISIBLE
|
||||
}
|
||||
textView.text = message.getDisplayBody(context)
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import kotlinx.android.synthetic.main.view_document.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
|
||||
class DocumentView : LinearLayout {
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_document, this)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MmsMessageRecord, @ColorInt textColor: Int) {
|
||||
val document = message.slideDeck.documentSlide!!
|
||||
documentTitleTextView.text = document.fileName.or("Untitled File")
|
||||
documentTitleTextView.setTextColor(textColor)
|
||||
documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_link_preview.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
|
||||
class LinkPreviewView : LinearLayout {
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var url: String? = null
|
||||
lateinit var bodyTextView: TextView
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_link_preview, this)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, searchQuery: String?) {
|
||||
val linkPreview = message.linkPreviews.first()
|
||||
url = linkPreview.url
|
||||
// Thumbnail
|
||||
if (linkPreview.getThumbnail().isPresent) {
|
||||
// This internally fetches the thumbnail
|
||||
thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
||||
thumbnailImageView.loadIndicator.isVisible = false
|
||||
}
|
||||
// Title
|
||||
titleTextView.text = linkPreview.title
|
||||
val textColorID = if (message.isOutgoing && UiModeUtilities.isDayUiMode(context)) {
|
||||
R.color.white
|
||||
} else {
|
||||
if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.white
|
||||
}
|
||||
titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
|
||||
// Body
|
||||
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
mainLinkPreviewContainer.addView(bodyTextView)
|
||||
// Corner radii
|
||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||
cornerMask.setTopRightRadius(cornerRadii[1])
|
||||
cornerMask.setBottomRightRadius(cornerRadii[2])
|
||||
cornerMask.setBottomLeftRadius(cornerRadii[3])
|
||||
}
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas) {
|
||||
super.dispatchDraw(canvas)
|
||||
cornerMask.mask(canvas)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
fun calculateHit(event: MotionEvent) {
|
||||
val rawXInt = event.rawX.toInt()
|
||||
val rawYInt = event.rawY.toInt()
|
||||
val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||
val previewRect = Rect()
|
||||
mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
|
||||
if (previewRect.contains(hitRect)) {
|
||||
openURL()
|
||||
return
|
||||
}
|
||||
// intersectedModalSpans should only be a list of one item
|
||||
val hitSpans = bodyTextView.getIntersectedModalSpans(hitRect)
|
||||
hitSpans.forEach { span ->
|
||||
span.onClick(bodyTextView)
|
||||
}
|
||||
}
|
||||
|
||||
fun openURL() {
|
||||
val url = this.url ?: return
|
||||
val activity = context as AppCompatActivity
|
||||
OpenURLDialog(url).show(activity.supportFragmentManager, "Open URL Dialog")
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.android.synthetic.main.view_open_group_invitation.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
import org.session.libsession.messaging.utilities.UpdateMessageData
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
class OpenGroupInvitationView : LinearLayout {
|
||||
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
|
||||
|
||||
constructor(context: Context): super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_open_group_invitation, this)
|
||||
}
|
||||
|
||||
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||
// FIXME: This is a really weird approach...
|
||||
val umd = UpdateMessageData.fromJSON(message.body)!!
|
||||
val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation
|
||||
this.data = data
|
||||
val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus
|
||||
openGroupInvitationIconImageView.setImageResource(iconID)
|
||||
openGroupTitleTextView.text = data.groupName
|
||||
openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl)
|
||||
openGroupTitleTextView.setTextColor(textColor)
|
||||
openGroupJoinMessageTextView.setTextColor(textColor)
|
||||
openGroupURLTextView.setTextColor(textColor)
|
||||
}
|
||||
|
||||
fun joinOpenGroup() {
|
||||
val data = data ?: return
|
||||
val activity = context as AppCompatActivity
|
||||
JoinOpenGroupDialog(data.groupName, data.groupUrl).show(activity.supportFragmentManager, "Join Open Group Dialog")
|
||||
}
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginStart
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import kotlinx.android.synthetic.main.view_link_preview.view.*
|
||||
import kotlinx.android.synthetic.main.view_quote.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.utilities.UpdateMessageData
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// There's quite some calculation going on here. It's a bit complex so don't make changes
|
||||
// if you don't need to. If you do then test:
|
||||
// • Quoted text in both private chats and group chats
|
||||
// • Quoted images and videos in both private chats and group chats
|
||||
// • Quoted voice messages and documents in both private chats and group chats
|
||||
// • All of the above in both dark mode and light mode
|
||||
|
||||
class QuoteView : LinearLayout {
|
||||
private lateinit var mode: Mode
|
||||
private val vPadding by lazy { toPx(6, resources) }
|
||||
var delegate: QuoteViewDelegate? = null
|
||||
|
||||
enum class Mode { Regular, Draft }
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") }
|
||||
|
||||
constructor(context: Context, mode: Mode) : super(context) {
|
||||
this.mode = mode
|
||||
LayoutInflater.from(context).inflate(R.layout.view_quote, this)
|
||||
// Add padding here (not on mainQuoteViewContainer) to get a bit of a top inset while avoiding
|
||||
// the clipping issue described in getIntrinsicHeight(maxContentWidth:).
|
||||
setPadding(0, toPx(6, resources), 0, 0)
|
||||
when (mode) {
|
||||
Mode.Draft -> quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
|
||||
Mode.Regular -> {
|
||||
quoteViewCancelButton.isVisible = false
|
||||
mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme))
|
||||
val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
|
||||
// Since we're not showing the cancel button we can shorten the end margin
|
||||
quoteViewMainContentContainerLayoutParams.marginEnd = resources.getDimension(R.dimen.medium_spacing).roundToInt()
|
||||
quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region General
|
||||
fun getIntrinsicContentHeight(maxContentWidth: Int): Int {
|
||||
// If we're showing an attachment thumbnail, just constrain to the height of that
|
||||
if (quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) }
|
||||
var result = 0
|
||||
var authorTextViewIntrinsicHeight = 0
|
||||
if (quoteViewAuthorTextView.isVisible) {
|
||||
val author = quoteViewAuthorTextView.text
|
||||
authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, quoteViewAuthorTextView.paint, maxContentWidth)
|
||||
result += authorTextViewIntrinsicHeight
|
||||
}
|
||||
val body = quoteViewBodyTextView.text
|
||||
val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, quoteViewBodyTextView.paint, maxContentWidth)
|
||||
result += bodyTextViewIntrinsicHeight
|
||||
if (!quoteViewAuthorTextView.isVisible) {
|
||||
// We want to at least be as high as the cancel button, and no higher than 56 DP (that's
|
||||
// approximately the height of 3 lines.
|
||||
return min(max(result, toPx(32, resources)), toPx(56, resources))
|
||||
} else {
|
||||
// Because we're showing the author text view, we should have a height of at least 32 DP
|
||||
// anyway, so there's no need to constrain to that. We constrain to a max height of 56 DP
|
||||
// because that's approximately the height of the author text view + 2 lines of the body
|
||||
// text view.
|
||||
return min(result, toPx(56, resources))
|
||||
}
|
||||
}
|
||||
|
||||
fun getIntrinsicHeight(maxContentWidth: Int): Int {
|
||||
// The way all this works is that we just calculate the total height the quote view should be
|
||||
// and then center everything inside vertically. This effectively means we're applying padding.
|
||||
// Applying padding the regular way results in a clipping issue though due to a bug in
|
||||
// RelativeLayout.
|
||||
return getIntrinsicContentHeight(maxContentWidth) + 2 * vPadding
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
|
||||
isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean, threadID: Long, glide: GlideRequests) {
|
||||
val contactDB = DatabaseFactory.getSessionContactDatabase(context)
|
||||
// Reduce the max body text view line count to 2 if this is a group thread because
|
||||
// we'll be showing the author text view and we don't want the overall quote view height
|
||||
// to get too big.
|
||||
quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3
|
||||
// Author
|
||||
if (thread.isGroupRecipient) {
|
||||
val author = contactDB.getContactWithSessionID(authorPublicKey)
|
||||
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
|
||||
quoteViewAuthorTextView.text = authorDisplayName
|
||||
quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
}
|
||||
quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
||||
// Body
|
||||
quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context);
|
||||
quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
// Accent line / attachment preview
|
||||
val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty())
|
||||
quoteViewAccentLine.isVisible = !hasAttachments
|
||||
quoteViewAttachmentPreviewContainer.isVisible = hasAttachments
|
||||
if (!hasAttachments) {
|
||||
val accentLineLayoutParams = quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams
|
||||
accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height
|
||||
quoteViewAccentLine.layoutParams = accentLineLayoutParams
|
||||
quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage))
|
||||
} else {
|
||||
attachments!!
|
||||
quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme))
|
||||
val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent
|
||||
val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme)
|
||||
quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
||||
quoteViewAttachmentPreviewImageView.isVisible = false
|
||||
quoteViewAttachmentThumbnailImageView.isVisible = false
|
||||
if (attachments.audioSlide != null) {
|
||||
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||
quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
|
||||
} else if (attachments.documentSlide != null) {
|
||||
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
||||
quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
quoteViewBodyTextView.text = resources.getString(R.string.document)
|
||||
} else if (attachments.thumbnailSlide != null) {
|
||||
val slide = attachments.thumbnailSlide!!
|
||||
// This internally fetches the thumbnail
|
||||
quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources)
|
||||
quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false)
|
||||
quoteViewAttachmentThumbnailImageView.isVisible = true
|
||||
quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
|
||||
}
|
||||
}
|
||||
mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth))
|
||||
val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
|
||||
// The start margin is different if we just show the accent line vs if we show an attachment thumbnail
|
||||
quoteViewMainContentContainerLayoutParams.marginStart = if (!hasAttachments) toPx(16, resources) else toPx(48, resources)
|
||||
quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
@ColorInt private fun getLineColor(isOutgoingMessage: Boolean): Int {
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||
if ((mode == Mode.Regular && isLightMode) || (mode == Mode.Draft && isLightMode)) {
|
||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
} else if (mode == Mode.Regular && !isLightMode) {
|
||||
if (isOutgoingMessage) {
|
||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
} else {
|
||||
return ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
||||
}
|
||||
} else { // Draft & dark mode
|
||||
return ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt private fun getTextColor(isOutgoingMessage: Boolean): Int {
|
||||
if (mode == Mode.Draft) { return ResourcesCompat.getColor(resources, R.color.text, context.theme) }
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||
if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) {
|
||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
} else {
|
||||
return ResourcesCompat.getColor(resources, R.color.white, context.theme)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
interface QuoteViewDelegate {
|
||||
|
||||
fun cancelQuoteDraft()
|
||||
}
|
@ -0,0 +1,218 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.URLSpan
|
||||
import android.text.util.Linkify
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
import kotlinx.android.synthetic.main.view_link_preview.view.*
|
||||
import kotlinx.android.synthetic.main.view_visible_message_content.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.SearchUtil
|
||||
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VisibleMessageContentView : LinearLayout {
|
||||
var onContentClick: ((event: MotionEvent) -> Unit)? = null
|
||||
var onContentDoubleTap: (() -> Unit)? = null
|
||||
var delegate: VisibleMessageContentViewDelegate? = null
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_visible_message_content, this)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean,
|
||||
glide: GlideRequests, maxWidth: Int, thread: Recipient, searchQuery: String?) {
|
||||
// Background
|
||||
val background = getBackground(message.isOutgoing, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
val colorID = if (message.isOutgoing) R.attr.message_sent_background_color else R.attr.message_received_background_color
|
||||
val color = ThemeUtil.getThemedColor(context, colorID)
|
||||
val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
|
||||
background.colorFilter = filter
|
||||
setBackground(background)
|
||||
// Body
|
||||
mainContainer.removeAllViews()
|
||||
onContentClick = null
|
||||
onContentDoubleTap = null
|
||||
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
||||
val linkPreviewView = LinkPreviewView(context)
|
||||
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
|
||||
mainContainer.addView(linkPreviewView)
|
||||
onContentClick = { event -> linkPreviewView.calculateHit(event) }
|
||||
// Body text view is inside the link preview for layout convenience
|
||||
} else if (message is MmsMessageRecord && message.quote != null) {
|
||||
val quote = message.quote!!
|
||||
val quoteView = QuoteView(context, QuoteView.Mode.Regular)
|
||||
// The max content width is the max message bubble size - 2 times the horizontal padding - the
|
||||
// quote view content area's start margin. This unfortunately has to be calculated manually
|
||||
// here to get the layout right.
|
||||
val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources)).roundToInt()
|
||||
quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread,
|
||||
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide)
|
||||
mainContainer.addView(quoteView)
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
ViewUtil.setPaddingTop(bodyTextView, 0)
|
||||
mainContainer.addView(bodyTextView)
|
||||
onContentClick = { event ->
|
||||
val r = Rect()
|
||||
quoteView.getGlobalVisibleRect(r)
|
||||
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
|
||||
delegate?.scrollToMessageIfPossible(quote.id)
|
||||
}
|
||||
}
|
||||
} else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) {
|
||||
val voiceMessageView = VoiceMessageView(context)
|
||||
voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
mainContainer.addView(voiceMessageView)
|
||||
// We have to use onContentClick (rather than a click listener directly on the voice
|
||||
// message view) so as to not interfere with all the other gestures.
|
||||
onContentClick = { voiceMessageView.togglePlayback() }
|
||||
onContentDoubleTap = { voiceMessageView.handleDoubleTap() }
|
||||
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
|
||||
val documentView = DocumentView(context)
|
||||
documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
mainContainer.addView(documentView)
|
||||
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
|
||||
val albumThumbnailView = AlbumThumbnailView(context)
|
||||
mainContainer.addView(albumThumbnailView)
|
||||
// isStart and isEnd of cluster needed for calculating the mask for full bubble image groups
|
||||
// bind after add view because views are inflated and calculated during bind
|
||||
albumThumbnailView.bind(
|
||||
glideRequests = glide,
|
||||
message = message,
|
||||
isStart = isStartOfMessageCluster,
|
||||
isEnd = isEndOfMessageCluster
|
||||
)
|
||||
onContentClick = { event ->
|
||||
albumThumbnailView.calculateHitObject(event, message, thread)
|
||||
}
|
||||
} else if (message.isOpenGroupInvitation) {
|
||||
val openGroupInvitationView = OpenGroupInvitationView(context)
|
||||
openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
mainContainer.addView(openGroupInvitationView)
|
||||
onContentClick = { openGroupInvitationView.joinOpenGroup() }
|
||||
} else {
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
mainContainer.addView(bodyTextView)
|
||||
onContentClick = { event ->
|
||||
// intersectedModalSpans should only be a list of one item
|
||||
bodyTextView.getIntersectedModalSpans(event).forEach { span ->
|
||||
span.onClick(bodyTextView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable {
|
||||
val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster)
|
||||
@DrawableRes val backgroundID: Int
|
||||
if (isSingleMessage) {
|
||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
|
||||
} else if (isStartOfMessageCluster) {
|
||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start
|
||||
} else if (isEndOfMessageCluster) {
|
||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end
|
||||
} else {
|
||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle
|
||||
}
|
||||
return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!!
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
mainContainer.removeAllViews()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
companion object {
|
||||
|
||||
fun getBodyTextView(context: Context, message: MessageRecord, searchQuery: String?): TextView {
|
||||
val result = EmojiTextView(context)
|
||||
val vPadding = context.resources.getDimension(R.dimen.small_spacing).toInt()
|
||||
val hPadding = toPx(12, context.resources)
|
||||
result.setPadding(hPadding, vPadding, hPadding, vPadding)
|
||||
result.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.resources.getDimension(R.dimen.small_font_size))
|
||||
val color = getTextColor(context, message)
|
||||
result.setTextColor(color)
|
||||
result.setLinkTextColor(color)
|
||||
var body = message.body.toSpannable()
|
||||
Linkify.addLinks(body, Linkify.WEB_URLS)
|
||||
|
||||
// replace URLSpans with ModalURLSpans
|
||||
body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan ->
|
||||
val replacementSpan = ModalURLSpan(urlSpan.url) { url ->
|
||||
val activity = context as AppCompatActivity
|
||||
OpenURLDialog(url).show(activity.supportFragmentManager, "Open URL Dialog")
|
||||
}
|
||||
val start = body.getSpanStart(urlSpan)
|
||||
val end = body.getSpanEnd(urlSpan)
|
||||
val flags = body.getSpanFlags(urlSpan)
|
||||
body.removeSpan(urlSpan)
|
||||
body.setSpan(replacementSpan, start, end, flags)
|
||||
}
|
||||
|
||||
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context)
|
||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
|
||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
|
||||
|
||||
result.text = body
|
||||
return result
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun getTextColor(context: Context, message: MessageRecord): Int {
|
||||
val isDayUiMode = UiModeUtilities.isDayUiMode(context)
|
||||
val colorID = if (message.isOutgoing) {
|
||||
if (isDayUiMode) R.color.white else R.color.black
|
||||
} else {
|
||||
if (isDayUiMode) R.color.black else R.color.white
|
||||
}
|
||||
return context.resources.getColorWithID(colorID, context.theme)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
interface VisibleMessageContentViewDelegate {
|
||||
|
||||
fun scrollToMessageIfPossible(timestamp: Long)
|
||||
}
|
@ -0,0 +1,367 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.*
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_visible_message.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.loki.utilities.disableClipping
|
||||
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
|
||||
import org.thoughtcrime.securesms.loki.utilities.toDp
|
||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class VisibleMessageView : LinearLayout {
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
||||
private val swipeToReplyIconRect = Rect()
|
||||
private var dx = 0.0f
|
||||
private var previousTranslationX = 0.0f
|
||||
private val gestureHandler = Handler(Looper.getMainLooper())
|
||||
private var pressCallback: Runnable? = null
|
||||
private var longPressCallback: Runnable? = null
|
||||
private var onDownTimestamp = 0L
|
||||
private var onDoubleTap: (() -> Unit)? = null
|
||||
var snIsSelected = false
|
||||
set(value) { field = value; handleIsSelectedChanged()}
|
||||
var onPress: ((event: MotionEvent) -> Unit)? = null
|
||||
var onSwipeToReply: (() -> Unit)? = null
|
||||
var onLongPress: (() -> Unit)? = null
|
||||
var contentViewDelegate: VisibleMessageContentViewDelegate? = null
|
||||
|
||||
companion object {
|
||||
const val swipeToReplyThreshold = 80.0f // dp
|
||||
const val longPressMovementTreshold = 10.0f // dp
|
||||
const val longPressDurationThreshold = 250L // ms
|
||||
const val maxDoubleTapInterval = 200L
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_visible_message, this)
|
||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
isHapticFeedbackEnabled = true
|
||||
setWillNotDraw(false)
|
||||
expirationTimerViewContainer.disableClipping()
|
||||
messageContentContainer.disableClipping()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?, glide: GlideRequests, searchQuery: String?) {
|
||||
val sender = message.individualRecipient
|
||||
val senderSessionID = sender.address.serialize()
|
||||
val threadID = message.threadId
|
||||
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
||||
val thread = threadDB.getRecipientForThreadId(threadID)!!
|
||||
val contactDB = DatabaseFactory.getSessionContactDatabase(context)
|
||||
val isGroupThread = thread.isGroupRecipient
|
||||
val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread)
|
||||
val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread)
|
||||
// Show profile picture and sender name if this is a group thread AND
|
||||
// the message is incoming
|
||||
if (isGroupThread && !message.isOutgoing) {
|
||||
profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
|
||||
profilePictureView.publicKey = senderSessionID
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.update()
|
||||
if (thread.isOpenGroupRecipient) {
|
||||
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)!!
|
||||
val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server)
|
||||
moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
|
||||
} else {
|
||||
moderatorIconImageView.visibility = View.INVISIBLE
|
||||
}
|
||||
senderNameTextView.isVisible = isStartOfMessageCluster
|
||||
val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||
senderNameTextView.text = contactDB.getContactWithSessionID(senderSessionID)?.displayName(context) ?: senderSessionID
|
||||
} else {
|
||||
profilePictureContainer.visibility = View.GONE
|
||||
senderNameTextView.visibility = View.GONE
|
||||
}
|
||||
// Date break
|
||||
val showDateBreak = (previous == null || !DateUtils.isSameDay(message.timestamp, previous.timestamp))
|
||||
dateBreakTextView.isVisible = showDateBreak
|
||||
dateBreakTextView.text = if (showDateBreak) DateUtils.getRelativeDate(context, Locale.getDefault(), message.timestamp) else ""
|
||||
// Timestamp
|
||||
messageTimestampTextView.text = DateUtils.getExtendedRelativeTimeSpanString(context, Locale.getDefault(), message.timestamp)
|
||||
// Margins
|
||||
val startPadding: Int
|
||||
if (isGroupThread) {
|
||||
startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() else 0
|
||||
} else {
|
||||
startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt()
|
||||
else resources.getDimension(R.dimen.medium_spacing).toInt()
|
||||
}
|
||||
val endPadding = if (message.isOutgoing) resources.getDimension(R.dimen.medium_spacing).toInt()
|
||||
else resources.getDimension(R.dimen.very_large_spacing).toInt()
|
||||
messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0)
|
||||
// Set inter-message spacing
|
||||
setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
// Gravity
|
||||
val gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
||||
mainContainer.gravity = gravity or Gravity.BOTTOM
|
||||
// Message status indicator
|
||||
val (iconID, iconColor) = getMessageStatusImage(message)
|
||||
if (iconID != null) {
|
||||
val drawable = ContextCompat.getDrawable(context, iconID)?.mutate()
|
||||
if (iconColor != null) {
|
||||
drawable?.setTint(iconColor)
|
||||
}
|
||||
messageStatusImageView.setImageDrawable(drawable)
|
||||
}
|
||||
if (message.isOutgoing) {
|
||||
val lastMessageID = DatabaseFactory.getMmsSmsDatabase(context).getLastMessageID(message.threadId)
|
||||
messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
|
||||
} else {
|
||||
messageStatusImageView.isVisible = false
|
||||
}
|
||||
// Expiration timer
|
||||
updateExpirationTimer(message)
|
||||
// Calculate max message bubble width
|
||||
var maxWidth = screenWidth - startPadding - endPadding
|
||||
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
|
||||
// Populate content view
|
||||
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery)
|
||||
messageContentView.delegate = contentViewDelegate
|
||||
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
|
||||
}
|
||||
|
||||
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||
val topPadding = if (isStartOfMessageCluster) R.dimen.conversation_vertical_message_spacing_default else R.dimen.conversation_vertical_message_spacing_collapse
|
||||
ViewUtil.setPaddingTop(this, resources.getDimension(topPadding).roundToInt())
|
||||
val bottomPadding = if (isEndOfMessageCluster) R.dimen.conversation_vertical_message_spacing_default else R.dimen.conversation_vertical_message_spacing_collapse
|
||||
ViewUtil.setPaddingBottom(this, resources.getDimension(bottomPadding).roundToInt())
|
||||
}
|
||||
|
||||
private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean {
|
||||
return if (isGroupThread) {
|
||||
previous == null || previous.isUpdate || !DateUtils.isSameDay(current.timestamp, previous.timestamp)
|
||||
|| current.recipient.address != previous.recipient.address
|
||||
} else {
|
||||
previous == null || previous.isUpdate || !DateUtils.isSameDay(current.timestamp, previous.timestamp)
|
||||
|| current.isOutgoing != previous.isOutgoing
|
||||
}
|
||||
}
|
||||
|
||||
private fun isEndOfMessageCluster(current: MessageRecord, next: MessageRecord?, isGroupThread: Boolean): Boolean {
|
||||
return if (isGroupThread) {
|
||||
next == null || next.isUpdate || !DateUtils.isSameDay(current.timestamp, next.timestamp)
|
||||
|| current.recipient.address != next.recipient.address
|
||||
} else {
|
||||
next == null || next.isUpdate || !DateUtils.isSameDay(current.timestamp, next.timestamp)
|
||||
|| current.isOutgoing != next.isOutgoing
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMessageStatusImage(message: MessageRecord): Pair<Int?,Int?> {
|
||||
return when {
|
||||
!message.isOutgoing -> null to null
|
||||
message.isFailed -> R.drawable.ic_error to resources.getColor(R.color.destructive, context.theme)
|
||||
message.isPending -> R.drawable.ic_circle_dot_dot_dot to null
|
||||
message.isRead -> R.drawable.ic_filled_circle_check to null
|
||||
else -> R.drawable.ic_circle_check to null
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExpirationTimer(message: MessageRecord) {
|
||||
val expirationTimerViewLayoutParams = expirationTimerView.layoutParams as RelativeLayout.LayoutParams
|
||||
val ruleToAdd = if (message.isOutgoing) RelativeLayout.ALIGN_START else RelativeLayout.ALIGN_END
|
||||
val ruleToRemove = if (message.isOutgoing) RelativeLayout.ALIGN_END else RelativeLayout.ALIGN_START
|
||||
expirationTimerViewLayoutParams.removeRule(ruleToRemove)
|
||||
expirationTimerViewLayoutParams.addRule(ruleToAdd, R.id.messageContentView)
|
||||
val expirationTimerViewSize = toPx(12, resources)
|
||||
val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt()
|
||||
expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0
|
||||
expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize)
|
||||
expirationTimerView.layoutParams = expirationTimerViewLayoutParams
|
||||
if (message.expiresIn > 0 && !message.isPending) {
|
||||
expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
|
||||
expirationTimerView.isVisible = true
|
||||
expirationTimerView.setPercentComplete(0.0f)
|
||||
if (message.expireStarted > 0) {
|
||||
expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
||||
expirationTimerView.startAnimation()
|
||||
if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) {
|
||||
ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule()
|
||||
}
|
||||
} else if (!message.isOutgoing && !message.isMediaPending) {
|
||||
ThreadUtils.queue {
|
||||
val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager
|
||||
val id = message.getId()
|
||||
val mms = message.isMms
|
||||
if (mms) DatabaseFactory.getMmsDatabase(context).markExpireStarted(id) else DatabaseFactory.getSmsDatabase(context).markExpireStarted(id)
|
||||
expirationManager.scheduleDeletion(id, mms, message.expiresIn)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
expirationTimerView.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIsSelectedChanged() {
|
||||
background = if (snIsSelected) {
|
||||
ColorDrawable(context.resources.getColorWithID(R.color.message_selected, context.theme))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
if (translationX < 0 && !expirationTimerView.isVisible) {
|
||||
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
||||
val threshold = VisibleMessageView.swipeToReplyThreshold
|
||||
val iconSize = toPx(24, context.resources)
|
||||
val bottomVOffset = paddingBottom + messageStatusImageView.height + (messageContentView.height - iconSize) / 2
|
||||
swipeToReplyIconRect.left = messageContentContainer.right - messageContentContainer.paddingEnd + spacing
|
||||
swipeToReplyIconRect.top = height - bottomVOffset - iconSize
|
||||
swipeToReplyIconRect.right = messageContentContainer.right - messageContentContainer.paddingEnd + iconSize + spacing
|
||||
swipeToReplyIconRect.bottom = height - bottomVOffset
|
||||
swipeToReplyIcon.bounds = swipeToReplyIconRect
|
||||
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
|
||||
} else {
|
||||
swipeToReplyIcon.alpha = 0
|
||||
}
|
||||
swipeToReplyIcon.draw(canvas)
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
profilePictureView.recycle()
|
||||
messageContentView.recycle()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> onDown(event)
|
||||
MotionEvent.ACTION_MOVE -> onMove(event)
|
||||
MotionEvent.ACTION_CANCEL -> onCancel(event)
|
||||
MotionEvent.ACTION_UP -> onUp(event)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onDown(event: MotionEvent) {
|
||||
dx = x - event.rawX
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
val newLongPressCallback = Runnable { onLongPress() }
|
||||
this.longPressCallback = newLongPressCallback
|
||||
gestureHandler.postDelayed(newLongPressCallback, VisibleMessageView.longPressDurationThreshold)
|
||||
onDownTimestamp = Date().time
|
||||
}
|
||||
|
||||
private fun onMove(event: MotionEvent) {
|
||||
val translationX = toDp(event.rawX + dx, context.resources)
|
||||
if (abs(translationX) < VisibleMessageView.longPressMovementTreshold || snIsSelected) {
|
||||
return
|
||||
} else {
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
}
|
||||
if (translationX > 0) { return } // Only allow swipes to the left
|
||||
// The idea here is to asymptotically approach a maximum drag distance
|
||||
val damping = 50.0f
|
||||
val sign = -1.0f
|
||||
val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign
|
||||
this.translationX = x
|
||||
this.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving
|
||||
postInvalidate() // Ensure onDraw(canvas:) is called
|
||||
if (abs(x) > VisibleMessageView.swipeToReplyThreshold && abs(previousTranslationX) < VisibleMessageView.swipeToReplyThreshold) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||
} else {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
}
|
||||
}
|
||||
previousTranslationX = x
|
||||
}
|
||||
|
||||
private fun onCancel(event: MotionEvent) {
|
||||
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) {
|
||||
onSwipeToReply?.invoke()
|
||||
}
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
resetPosition()
|
||||
}
|
||||
|
||||
private fun onUp(event: MotionEvent) {
|
||||
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) {
|
||||
onSwipeToReply?.invoke()
|
||||
} else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) {
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
val pressCallback = this.pressCallback
|
||||
if (pressCallback != null) {
|
||||
// If we're here and pressCallback isn't null, it means that we tapped again within
|
||||
// maxDoubleTapInterval ms and we should count this as a double tap
|
||||
gestureHandler.removeCallbacks(pressCallback)
|
||||
this.pressCallback = null
|
||||
onDoubleTap?.invoke()
|
||||
} else {
|
||||
val newPressCallback = Runnable { onPress(event) }
|
||||
this.pressCallback = newPressCallback
|
||||
gestureHandler.postDelayed(newPressCallback, VisibleMessageView.maxDoubleTapInterval)
|
||||
}
|
||||
}
|
||||
resetPosition()
|
||||
}
|
||||
|
||||
private fun resetPosition() {
|
||||
animate()
|
||||
.translationX(0.0f)
|
||||
.setDuration(150)
|
||||
.setUpdateListener {
|
||||
postInvalidate() // Ensure onDraw(canvas:) is called
|
||||
}
|
||||
.start()
|
||||
// Bit of a hack to keep the date break text view from moving
|
||||
dateBreakTextView.animate()
|
||||
.translationX(0.0f)
|
||||
.setDuration(150)
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun onLongPress() {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
onLongPress?.invoke()
|
||||
}
|
||||
|
||||
fun onContentClick(event: MotionEvent) {
|
||||
messageContentView.onContentClick?.invoke(event)
|
||||
}
|
||||
|
||||
private fun onPress(event: MotionEvent) {
|
||||
onPress?.invoke(event)
|
||||
pressCallback = null
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_voice_message.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var isPlaying = false
|
||||
private var progress = 0.0
|
||||
private var duration = 0L
|
||||
private var player: AudioSlidePlayer? = null
|
||||
private var isPreparing = false
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_voice_message, this)
|
||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(0),
|
||||
TimeUnit.MILLISECONDS.toSeconds(0))
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||
val audio = message.slideDeck.audioSlide!!
|
||||
val player = AudioSlidePlayer.createFor(context, audio, this)
|
||||
this.player = player
|
||||
isPreparing = true
|
||||
if (!audio.isPendingDownload && !audio.isInProgress) {
|
||||
player.play(0.0)
|
||||
}
|
||||
voiceMessageViewLoader.isVisible = audio.isPendingDownload
|
||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||
cornerMask.setTopRightRadius(cornerRadii[1])
|
||||
cornerMask.setBottomRightRadius(cornerRadii[2])
|
||||
cornerMask.setBottomLeftRadius(cornerRadii[3])
|
||||
}
|
||||
|
||||
override fun onPlayerStart(player: AudioSlidePlayer) {
|
||||
if (!isPreparing) { return }
|
||||
isPreparing = false
|
||||
duration = player.duration
|
||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(duration),
|
||||
TimeUnit.MILLISECONDS.toSeconds(duration))
|
||||
player.stop()
|
||||
}
|
||||
|
||||
override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, unused: Long) {
|
||||
if (progress == 1.0) {
|
||||
togglePlayback()
|
||||
handleProgressChanged(0.0)
|
||||
} else {
|
||||
handleProgressChanged(progress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleProgressChanged(progress: Double) {
|
||||
this.progress = progress
|
||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()),
|
||||
TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong()))
|
||||
val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams
|
||||
layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
|
||||
progressView.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
override fun onPlayerStop(player: AudioSlidePlayer) { }
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas) {
|
||||
super.dispatchDraw(canvas)
|
||||
cornerMask.mask(canvas)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
fun togglePlayback() {
|
||||
val player = this.player ?: return
|
||||
isPlaying = !isPlaying
|
||||
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
|
||||
voiceMessagePlaybackImageView.setImageResource(iconID)
|
||||
if (isPlaying) {
|
||||
player.play(progress)
|
||||
} else {
|
||||
player.stop()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleDoubleTap() {
|
||||
val player = this.player ?: return
|
||||
player.playbackSpeed = if (player.playbackSpeed == 1.0f) 1.5f else 1.0f
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.search
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_search_bottom_bar.view.*
|
||||
import network.loki.messenger.R
|
||||
|
||||
|
||||
class SearchBottomBar : LinearLayout {
|
||||
private var eventListener: EventListener? = null
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_search_bottom_bar, this)
|
||||
}
|
||||
|
||||
fun setData(position: Int, count: Int) {
|
||||
searchProgressWheel.visibility = GONE
|
||||
searchUp.setOnClickListener { v: View? ->
|
||||
if (eventListener != null) {
|
||||
eventListener!!.onSearchMoveUpPressed()
|
||||
}
|
||||
}
|
||||
searchDown.setOnClickListener { v: View? ->
|
||||
if (eventListener != null) {
|
||||
eventListener!!.onSearchMoveDownPressed()
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
searchPosition.text = resources.getString(R.string.ConversationActivity_search_position, position + 1, count)
|
||||
} else {
|
||||
searchPosition.text = ""
|
||||
}
|
||||
setViewEnabled(searchUp, position < count - 1)
|
||||
setViewEnabled(searchDown, position > 0)
|
||||
}
|
||||
|
||||
fun showLoading() {
|
||||
searchProgressWheel.visibility = VISIBLE
|
||||
}
|
||||
|
||||
private fun setViewEnabled(view: View, enabled: Boolean) {
|
||||
view.isEnabled = enabled
|
||||
view.alpha = if (enabled) 1f else 0.25f
|
||||
}
|
||||
|
||||
fun setEventListener(eventListener: EventListener?) {
|
||||
this.eventListener = eventListener
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
fun onSearchMoveUpPressed()
|
||||
fun onSearchMoveDownPressed()
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.search
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.session.libsession.utilities.Debouncer
|
||||
import org.session.libsession.utilities.Util.runOnMain
|
||||
import org.session.libsession.utilities.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor
|
||||
import org.thoughtcrime.securesms.database.CursorList
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.search.SearchRepository
|
||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||
import org.thoughtcrime.securesms.util.CloseableLiveData
|
||||
import java.io.Closeable
|
||||
|
||||
|
||||
class SearchViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val searchRepository: SearchRepository
|
||||
private val result: CloseableLiveData<SearchResult>
|
||||
private val debouncer: Debouncer
|
||||
private var firstSearch = false
|
||||
private var searchOpen = false
|
||||
private var activeQuery: String? = null
|
||||
private var activeThreadId: Long = 0
|
||||
val searchResults: LiveData<SearchResult>
|
||||
get() = result
|
||||
|
||||
fun onQueryUpdated(query: String, threadId: Long) {
|
||||
if (query == activeQuery) {
|
||||
return
|
||||
}
|
||||
updateQuery(query, threadId)
|
||||
}
|
||||
|
||||
fun onMissingResult() {
|
||||
if (activeQuery != null) {
|
||||
updateQuery(activeQuery!!, activeThreadId)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMoveUp() {
|
||||
debouncer.clear()
|
||||
val messages = result.value!!.getResults() as CursorList<MessageResult?>
|
||||
val position = Math.min(result.value!!.position + 1, messages.size - 1)
|
||||
result.setValue(SearchResult(messages, position), false)
|
||||
}
|
||||
|
||||
fun onMoveDown() {
|
||||
debouncer.clear()
|
||||
val messages = result.value!!.getResults() as CursorList<MessageResult?>
|
||||
val position = Math.max(result.value!!.position - 1, 0)
|
||||
result.setValue(SearchResult(messages, position), false)
|
||||
}
|
||||
|
||||
fun onSearchOpened() {
|
||||
searchOpen = true
|
||||
firstSearch = true
|
||||
}
|
||||
|
||||
fun onSearchClosed() {
|
||||
searchOpen = false
|
||||
activeQuery = null
|
||||
debouncer.clear()
|
||||
result.close()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
result.close()
|
||||
}
|
||||
|
||||
private fun updateQuery(query: String, threadId: Long) {
|
||||
activeQuery = query
|
||||
activeThreadId = threadId
|
||||
debouncer.publish {
|
||||
firstSearch = false
|
||||
searchRepository.query(query, threadId) { messages: CursorList<MessageResult?> ->
|
||||
runOnMain {
|
||||
if (searchOpen && query == activeQuery) {
|
||||
result.setValue(SearchResult(messages, 0))
|
||||
} else {
|
||||
messages.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SearchResult(private val results: CursorList<MessageResult?>, val position: Int) : Closeable {
|
||||
|
||||
fun getResults(): List<MessageResult?> {
|
||||
return results
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
results.close()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val context = application.applicationContext
|
||||
result = CloseableLiveData()
|
||||
debouncer = Debouncer(500)
|
||||
searchRepository = SearchRepository(context,
|
||||
DatabaseFactory.getSearchDatabase(context),
|
||||
DatabaseFactory.getThreadDatabase(context),
|
||||
ContactAccessor.getInstance(),
|
||||
SignalExecutors.SERIAL)
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
@ -23,29 +23,31 @@ import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.session.libsignal.utilities.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.session.libsignal.utilities.ExternalStorageUtil;
|
||||
@ -53,13 +55,8 @@ import org.thoughtcrime.securesms.util.FileProviderUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
import org.session.libsession.utilities.ViewUtil;
|
||||
import org.session.libsession.utilities.Stub;
|
||||
import org.session.libsignal.utilities.ListenableFuture;
|
||||
import org.session.libsignal.utilities.ListenableFuture.Listener;
|
||||
import org.session.libsignal.utilities.SettableFuture;
|
||||
|
||||
import java.io.File;
|
||||
@ -67,26 +64,18 @@ import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import static android.provider.MediaStore.EXTRA_OUTPUT;
|
||||
|
||||
|
||||
public class AttachmentManager {
|
||||
|
||||
private final static String TAG = AttachmentManager.class.getSimpleName();
|
||||
|
||||
private final @NonNull Context context;
|
||||
private final @NonNull Stub<View> attachmentViewStub;
|
||||
private final @NonNull AttachmentListener attachmentListener;
|
||||
|
||||
private RemovableEditableMediaView removableMediaView;
|
||||
private ThumbnailView thumbnail;
|
||||
private MessageAudioView audioView;
|
||||
private DocumentView documentView;
|
||||
|
||||
private @NonNull List<Uri> garbage = new LinkedList<>();
|
||||
private @NonNull Optional<Slide> slide = Optional.absent();
|
||||
private @Nullable Uri captureUri;
|
||||
@ -94,51 +83,12 @@ public class AttachmentManager {
|
||||
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
|
||||
this.context = activity;
|
||||
this.attachmentListener = listener;
|
||||
this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub);
|
||||
}
|
||||
|
||||
private void inflateStub() {
|
||||
if (!attachmentViewStub.resolved()) {
|
||||
View root = attachmentViewStub.get();
|
||||
|
||||
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
|
||||
this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
|
||||
this.documentView = ViewUtil.findById(root, R.id.attachment_document);
|
||||
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
|
||||
|
||||
removableMediaView.setRemoveClickListener(new RemoveButtonListener());
|
||||
thumbnail.setOnClickListener(new ThumbnailClickListener());
|
||||
documentView.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_bubble_background), PorterDuff.Mode.MULTIPLY);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear(@NonNull GlideRequests glideRequests, boolean animate) {
|
||||
if (attachmentViewStub.resolved()) {
|
||||
|
||||
if (animate) {
|
||||
ViewUtil.fadeOut(attachmentViewStub.get(), 200).addListener(new Listener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
thumbnail.clear(glideRequests);
|
||||
attachmentViewStub.get().setVisibility(View.GONE);
|
||||
attachmentListener.onAttachmentChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
thumbnail.clear(glideRequests);
|
||||
attachmentViewStub.get().setVisibility(View.GONE);
|
||||
attachmentListener.onAttachmentChanged();
|
||||
}
|
||||
|
||||
markGarbage(getSlideUri());
|
||||
slide = Optional.absent();
|
||||
|
||||
audioView.cleanup();
|
||||
}
|
||||
public void clear() {
|
||||
markGarbage(getSlideUri());
|
||||
slide = Optional.absent();
|
||||
attachmentListener.onAttachmentChanged();
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
@ -190,16 +140,12 @@ public class AttachmentManager {
|
||||
final int width,
|
||||
final int height)
|
||||
{
|
||||
inflateStub();
|
||||
|
||||
final SettableFuture<Boolean> result = new SettableFuture<>();
|
||||
|
||||
new AsyncTask<Void, Void, Slide>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
thumbnail.clear(glideRequests);
|
||||
thumbnail.showProgressSpinner();
|
||||
attachmentViewStub.get().setVisibility(View.VISIBLE);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -222,35 +168,12 @@ public class AttachmentManager {
|
||||
@Override
|
||||
protected void onPostExecute(@Nullable final Slide slide) {
|
||||
if (slide == null) {
|
||||
attachmentViewStub.get().setVisibility(View.GONE);
|
||||
Toast.makeText(context,
|
||||
R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
result.set(false);
|
||||
} else if (!areConstraintsSatisfied(context, slide, constraints)) {
|
||||
attachmentViewStub.get().setVisibility(View.GONE);
|
||||
Toast.makeText(context,
|
||||
R.string.ConversationActivity_attachment_exceeds_size_limits,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
result.set(false);
|
||||
} else {
|
||||
setSlide(slide);
|
||||
attachmentViewStub.get().setVisibility(View.VISIBLE);
|
||||
|
||||
if (slide.hasAudio()) {
|
||||
audioView.setAudio((AudioSlide) slide, false);
|
||||
removableMediaView.display(audioView, false);
|
||||
result.set(true);
|
||||
} else if (slide.hasDocument()) {
|
||||
documentView.setDocument((DocumentSlide) slide, false);
|
||||
removableMediaView.display(documentView, false);
|
||||
result.set(true);
|
||||
} else {
|
||||
Attachment attachment = slide.asAttachment();
|
||||
result.deferTo(thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight()));
|
||||
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
|
||||
}
|
||||
|
||||
result.set(true);
|
||||
attachmentListener.onAttachmentChanged();
|
||||
}
|
||||
}
|
||||
@ -317,11 +240,8 @@ public class AttachmentManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isAttachmentPresent() {
|
||||
return attachmentViewStub.resolved() && attachmentViewStub.get().getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
public @NonNull SlideDeck buildSlideDeck() {
|
||||
public @NonNull
|
||||
SlideDeck buildSlideDeck() {
|
||||
SlideDeck deck = new SlideDeck();
|
||||
if (slide.isPresent()) deck.addSlide(slide.get());
|
||||
return deck;
|
||||
@ -333,43 +253,16 @@ public class AttachmentManager {
|
||||
|
||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
||||
.execute();
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectAudio(Activity activity, int requestCode) {
|
||||
selectMediaType(activity, "audio/*", null, requestCode);
|
||||
}
|
||||
|
||||
public static void selectContactInfo(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_CONTACTS)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectLocation(Activity activity, int requestCode) {
|
||||
/* Loki - Enable again once we have location sharing
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location))
|
||||
.onAllGranted(() -> {
|
||||
try {
|
||||
activity.startActivityForResult(new PlacePicker.IntentBuilder().build(activity), requestCode);
|
||||
} catch (GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
*/
|
||||
}
|
||||
|
||||
public static void selectGif(Activity activity, int requestCode) {
|
||||
Intent intent = new Intent(activity, GiphyActivity.class);
|
||||
intent.putExtra(GiphyActivity.EXTRA_IS_MMS, false);
|
||||
@ -386,28 +279,25 @@ public class AttachmentManager {
|
||||
|
||||
public void capturePhoto(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> {
|
||||
try {
|
||||
File captureFile = File.createTempFile(
|
||||
"conversation-capture",
|
||||
".jpg",
|
||||
ExternalStorageUtil.getImageDir(activity));
|
||||
Uri captureUri = FileProviderUtil.getUriFor(context, captureFile);
|
||||
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
captureIntent.putExtra(EXTRA_OUTPUT, captureUri);
|
||||
captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||
Log.d(TAG, "captureUri path is " + captureUri.getPath());
|
||||
this.captureUri = captureUri;
|
||||
activity.startActivityForResult(captureIntent, requestCode);
|
||||
}
|
||||
} catch (IOException | NoExternalStorageException e) {
|
||||
throw new RuntimeException("Error creating image capture intent.", e);
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> {
|
||||
try {
|
||||
File captureFile = File.createTempFile("conversation-capture", ".jpg", ExternalStorageUtil.getImageDir(activity));
|
||||
Uri captureUri = FileProviderUtil.getUriFor(context, captureFile);
|
||||
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
captureIntent.putExtra(EXTRA_OUTPUT, captureUri);
|
||||
captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||
Log.d(TAG, "captureUri path is " + captureUri.getPath());
|
||||
this.captureUri = captureUri;
|
||||
activity.startActivityForResult(captureIntent, requestCode);
|
||||
}
|
||||
} catch (IOException | NoExternalStorageException e) {
|
||||
throw new RuntimeException("Error creating image capture intent.", e);
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
||||
@ -445,34 +335,6 @@ public class AttachmentManager {
|
||||
constraints.canResize(slide.asAttachment());
|
||||
}
|
||||
|
||||
private void previewImageDraft(final @NonNull Slide slide) {
|
||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull());
|
||||
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, true);
|
||||
intent.setDataAndType(slide.getUri(), slide.getContentType());
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private class ThumbnailClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (slide.isPresent()) previewImageDraft(slide.get());
|
||||
}
|
||||
}
|
||||
|
||||
private class RemoveButtonListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
cleanup();
|
||||
clear(GlideApp.with(context.getApplicationContext()), true);
|
||||
}
|
||||
}
|
||||
|
||||
public interface AttachmentListener {
|
||||
void onAttachmentChanged();
|
||||
}
|
||||
@ -513,6 +375,5 @@ public class AttachmentManager {
|
||||
|
||||
return DOCUMENT;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
|
||||
|
||||
open class BaseDialog : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
setContentView(builder)
|
||||
val result = builder.create()
|
||||
result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(requireContext())
|
||||
result.window?.setDimAmount(if (isLightMode) 0.1f else 0.75f)
|
||||
return result
|
||||
}
|
||||
|
||||
open fun setContentView(builder: AlertDialog.Builder) {
|
||||
// To be overridden by subclasses
|
||||
}
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import kotlinx.android.synthetic.main.thumbnail_view.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.utilities.Util.equals
|
||||
import org.session.libsignal.utilities.ListenableFuture
|
||||
import org.session.libsignal.utilities.SettableFuture
|
||||
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
|
||||
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.*
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||
|
||||
open class KThumbnailView: FrameLayout {
|
||||
|
||||
companion object {
|
||||
private const val WIDTH = 0
|
||||
private const val HEIGHT = 1
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize(null) }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
||||
|
||||
private val image by lazy { thumbnail_image }
|
||||
private val playOverlay by lazy { play_overlay }
|
||||
val loadIndicator: View by lazy { thumbnail_load_indicator }
|
||||
|
||||
private val dimensDelegate = ThumbnailDimensDelegate()
|
||||
|
||||
private var slide: Slide? = null
|
||||
private var radius: Int = 0
|
||||
|
||||
private fun initialize(attrs: AttributeSet?) {
|
||||
inflate(context, R.layout.thumbnail_view, this)
|
||||
if (attrs != null) {
|
||||
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
|
||||
|
||||
dimensDelegate.setBounds(typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0),
|
||||
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0),
|
||||
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0),
|
||||
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0))
|
||||
|
||||
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0)
|
||||
|
||||
typedArray.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val adjustedDimens = dimensDelegate.resourceSize()
|
||||
if (adjustedDimens[WIDTH] == 0 && adjustedDimens[HEIGHT] == 0) {
|
||||
return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
|
||||
val finalWidth: Int = adjustedDimens[WIDTH] + paddingLeft + paddingRight
|
||||
val finalHeight: Int = adjustedDimens[HEIGHT] + paddingTop + paddingBottom
|
||||
|
||||
super.onMeasure(
|
||||
MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDefaultWidth() = maxOf(layoutParams?.width ?: 0, 0)
|
||||
private fun getDefaultHeight() = maxOf(layoutParams?.height ?: 0, 0)
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord): ListenableFuture<Boolean> {
|
||||
return setImageResource(glide, slide, isPreview, 0, 0, mms)
|
||||
}
|
||||
|
||||
fun setImageResource(glide: GlideRequests, slide: Slide,
|
||||
isPreview: Boolean, naturalWidth: Int,
|
||||
naturalHeight: Int, mms: MmsMessageRecord): ListenableFuture<Boolean> {
|
||||
|
||||
val currentSlide = this.slide
|
||||
|
||||
playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
|
||||
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
||||
|
||||
if (equals(currentSlide, slide)) {
|
||||
// don't re-load slide
|
||||
return SettableFuture(false)
|
||||
}
|
||||
|
||||
|
||||
if (currentSlide != null && currentSlide.fastPreflightId != null && currentSlide.fastPreflightId == slide.fastPreflightId) {
|
||||
// not reloading slide for fast preflight
|
||||
this.slide = slide
|
||||
}
|
||||
|
||||
this.slide = slide
|
||||
|
||||
loadIndicator.isVisible = slide.isInProgress && !mms.isFailed
|
||||
|
||||
dimensDelegate.setDimens(naturalWidth, naturalHeight)
|
||||
invalidate()
|
||||
|
||||
val result = SettableFuture<Boolean>()
|
||||
|
||||
when {
|
||||
slide.thumbnailUri != null -> {
|
||||
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(image, result))
|
||||
}
|
||||
slide.hasPlaceholder() -> {
|
||||
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(image, result))
|
||||
}
|
||||
else -> {
|
||||
glide.clear(image)
|
||||
result.set(false)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun buildThumbnailGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Drawable> {
|
||||
|
||||
val dimens = dimensDelegate.resourceSize()
|
||||
|
||||
val request = glide.load(DecryptableUri(slide.thumbnailUri!!))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.let { request ->
|
||||
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
|
||||
request.override(getDefaultWidth(), getDefaultHeight())
|
||||
} else {
|
||||
request.override(dimens[WIDTH], dimens[HEIGHT])
|
||||
}
|
||||
}
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.centerCrop()
|
||||
|
||||
return if (slide.isInProgress) request else request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture))
|
||||
}
|
||||
|
||||
fun buildPlaceholderGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Bitmap> {
|
||||
|
||||
val dimens = dimensDelegate.resourceSize()
|
||||
|
||||
return glide.asBitmap()
|
||||
.load(slide.getPlaceholderRes(context.theme))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.let { request ->
|
||||
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
|
||||
request.override(getDefaultWidth(), getDefaultHeight())
|
||||
} else {
|
||||
request.override(dimens[WIDTH], dimens[HEIGHT])
|
||||
}
|
||||
}
|
||||
.fitCenter()
|
||||
}
|
||||
|
||||
open fun clear(glideRequests: GlideRequests) {
|
||||
glideRequests.clear(image)
|
||||
slide = null
|
||||
}
|
||||
|
||||
fun setImageResource(glideRequests: GlideRequests, uri: Uri): ListenableFuture<Boolean> {
|
||||
val future = SettableFuture<Boolean>()
|
||||
|
||||
var request: GlideRequest<Drawable> = glideRequests.load(DecryptableUri(uri))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
|
||||
request = if (radius > 0) {
|
||||
request.transforms(CenterCrop(), RoundedCorners(radius))
|
||||
} else {
|
||||
request.transforms(CenterCrop())
|
||||
}
|
||||
|
||||
request.into(GlideDrawableListeningTarget(image, future))
|
||||
|
||||
return future
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.content.Context
|
||||
import network.loki.messenger.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
object MessageBubbleUtilities {
|
||||
|
||||
fun calculateRadii(context: Context, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, isOutgoing: Boolean): IntArray {
|
||||
val roundedDimen = context.resources.getDimension(R.dimen.message_corner_radius).roundToInt()
|
||||
val collapsedDimen = context.resources.getDimension(R.dimen.message_corner_collapse_radius).roundToInt()
|
||||
val (tl, tr, bl, br) = when {
|
||||
// Single message
|
||||
isStartOfMessageCluster && isEndOfMessageCluster -> intArrayOf(roundedDimen, roundedDimen, roundedDimen, roundedDimen)
|
||||
// Start of message cluster; collapsed BL
|
||||
isStartOfMessageCluster -> intArrayOf(roundedDimen, roundedDimen, collapsedDimen, roundedDimen)
|
||||
// End of message cluster; collapsed TL
|
||||
isEndOfMessageCluster -> intArrayOf(collapsedDimen, roundedDimen, roundedDimen, roundedDimen)
|
||||
// In the middle; no rounding on the left
|
||||
else -> intArrayOf(collapsedDimen, roundedDimen, collapsedDimen, roundedDimen)
|
||||
}
|
||||
// TL, TR, BR, BL (CW direction)
|
||||
// Flip if the message is outgoing
|
||||
return intArrayOf(
|
||||
if (!isOutgoing) tl else tr, // TL
|
||||
if (!isOutgoing) tr else tl, // TR
|
||||
if (!isOutgoing) br else bl, // BR
|
||||
if (!isOutgoing) bl else br // BL
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.text.style.URLSpan
|
||||
import android.view.View
|
||||
|
||||
class ModalURLSpan(url: String, private val openModalCallback: (String)->Unit): URLSpan(url) {
|
||||
override fun onClick(widget: View) {
|
||||
openModalCallback(url)
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.view.MotionEvent
|
||||
import android.widget.TextView
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.toSpannable
|
||||
|
||||
object TextUtilities {
|
||||
|
||||
fun getIntrinsicHeight(text: CharSequence, paint: TextPaint, width: Int): Int {
|
||||
val builder = StaticLayout.Builder.obtain(text, 0, text.length, paint, width)
|
||||
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
||||
.setLineSpacing(0.0f, 1.0f)
|
||||
.setIncludePad(false)
|
||||
val layout = builder.build()
|
||||
return layout.height
|
||||
}
|
||||
|
||||
fun TextView.getIntersectedModalSpans(event: MotionEvent): List<ModalURLSpan> {
|
||||
val xInt = event.rawX.toInt()
|
||||
val yInt = event.rawY.toInt()
|
||||
val hitRect = Rect(xInt, yInt, xInt, yInt)
|
||||
return getIntersectedModalSpans(hitRect)
|
||||
}
|
||||
|
||||
fun TextView.getIntersectedModalSpans(hitRect: Rect): List<ModalURLSpan> {
|
||||
val textLayout = layout ?: return emptyList()
|
||||
val lineRect = Rect()
|
||||
val bodyTextRect = Rect()
|
||||
getGlobalVisibleRect(bodyTextRect)
|
||||
val textSpannable = text.toSpannable()
|
||||
return (0 until textLayout.lineCount).flatMap { line ->
|
||||
textLayout.getLineBounds(line, lineRect)
|
||||
lineRect.offset(bodyTextRect.left + totalPaddingLeft, bodyTextRect.top + totalPaddingTop)
|
||||
if ((Rect(lineRect)).contains(hitRect)) {
|
||||
// calculate the url span intersected with (if any)
|
||||
val off = textLayout.getOffsetForHorizontal(line, hitRect.left.toFloat()) // left and right will be the same
|
||||
textSpannable.getSpans<ModalURLSpan>(off, off).toList()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
class ThumbnailDimensDelegate {
|
||||
|
||||
companion object {
|
||||
// dimens array constants
|
||||
private const val WIDTH = 0
|
||||
private const val HEIGHT = 1
|
||||
private const val DIMENS_ARRAY_SIZE = 2
|
||||
|
||||
// bounds array constants
|
||||
private const val MIN_WIDTH = 0
|
||||
private const val MIN_HEIGHT = 1
|
||||
private const val MAX_WIDTH = 2
|
||||
private const val MAX_HEIGHT = 3
|
||||
private const val BOUNDS_ARRAY_SIZE = 4
|
||||
|
||||
// const zero int array
|
||||
private val EMPTY_DIMENS = intArrayOf(0,0)
|
||||
|
||||
}
|
||||
|
||||
private val measured: IntArray = IntArray(DIMENS_ARRAY_SIZE)
|
||||
private val dimens: IntArray = IntArray(DIMENS_ARRAY_SIZE)
|
||||
private val bounds: IntArray = IntArray(BOUNDS_ARRAY_SIZE)
|
||||
|
||||
fun resourceSize(): IntArray {
|
||||
if (dimens.all { it == 0 }) {
|
||||
// dimens are (0, 0), don't go any further
|
||||
return EMPTY_DIMENS
|
||||
}
|
||||
|
||||
val naturalWidth = dimens[WIDTH].toDouble()
|
||||
val naturalHeight = dimens[HEIGHT].toDouble()
|
||||
val minWidth = dimens[MIN_WIDTH]
|
||||
val maxWidth = dimens[MAX_WIDTH]
|
||||
val minHeight = dimens[MIN_HEIGHT]
|
||||
val maxHeight = dimens[MAX_HEIGHT]
|
||||
|
||||
// calculate actual measured
|
||||
var measuredWidth: Double = naturalWidth
|
||||
var measuredHeight: Double = naturalHeight
|
||||
|
||||
val widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth
|
||||
val heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight
|
||||
|
||||
if (!widthInBounds || !heightInBounds) {
|
||||
val minWidthRatio: Double = naturalWidth / minWidth
|
||||
val maxWidthRatio: Double = naturalWidth / maxWidth
|
||||
val minHeightRatio: Double = naturalHeight / minHeight
|
||||
val maxHeightRatio: Double = naturalHeight / maxHeight
|
||||
if (maxWidthRatio > 1 || maxHeightRatio > 1) {
|
||||
if (maxWidthRatio >= maxHeightRatio) {
|
||||
measuredWidth /= maxWidthRatio
|
||||
measuredHeight /= maxWidthRatio
|
||||
} else {
|
||||
measuredWidth /= maxHeightRatio
|
||||
measuredHeight /= maxHeightRatio
|
||||
}
|
||||
measuredWidth = Math.max(measuredWidth, minWidth.toDouble())
|
||||
measuredHeight = Math.max(measuredHeight, minHeight.toDouble())
|
||||
} else if (minWidthRatio < 1 || minHeightRatio < 1) {
|
||||
if (minWidthRatio <= minHeightRatio) {
|
||||
measuredWidth /= minWidthRatio
|
||||
measuredHeight /= minWidthRatio
|
||||
} else {
|
||||
measuredWidth /= minHeightRatio
|
||||
measuredHeight /= minHeightRatio
|
||||
}
|
||||
measuredWidth = Math.min(measuredWidth, maxWidth.toDouble())
|
||||
measuredHeight = Math.min(measuredHeight, maxHeight.toDouble())
|
||||
}
|
||||
}
|
||||
measured[WIDTH] = measuredWidth.toInt()
|
||||
measured[HEIGHT] = measuredHeight.toInt()
|
||||
return measured
|
||||
}
|
||||
|
||||
fun setBounds(minWidth: Int, minHeight: Int, maxWidth: Int, maxHeight: Int) {
|
||||
bounds[MIN_WIDTH] = minWidth
|
||||
bounds[MIN_HEIGHT] = minHeight
|
||||
bounds[MAX_WIDTH] = maxWidth
|
||||
bounds[MAX_HEIGHT] = maxHeight
|
||||
}
|
||||
|
||||
fun setDimens(width: Int, height: Int) {
|
||||
dimens[WIDTH] = width
|
||||
dimens[HEIGHT] = height
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Interpolator
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.os.SystemClock
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationSet
|
||||
import android.view.animation.AnimationUtils
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import network.loki.messenger.R
|
||||
import kotlin.math.sin
|
||||
|
||||
class ThumbnailProgressBar: View {
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
private val firstX: Double
|
||||
get() = sin(SystemClock.elapsedRealtime() / 300.0) * 1.5
|
||||
|
||||
private val secondX: Double
|
||||
get() = sin(SystemClock.elapsedRealtime() / 300.0 + (Math.PI/4)) * 1.5
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.FILL
|
||||
color = ResourcesCompat.getColor(resources, R.color.accent, null)
|
||||
}
|
||||
|
||||
private val objectRect = Rect()
|
||||
private val drawingRect = Rect()
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas?) {
|
||||
if (canvas == null) return
|
||||
|
||||
getDrawingRect(objectRect)
|
||||
drawingRect.set(objectRect)
|
||||
|
||||
val coercedFX = firstX
|
||||
val coercedSX = secondX
|
||||
|
||||
val firstMeasuredX = objectRect.left + (objectRect.width() * coercedFX)
|
||||
val secondMeasuredX = objectRect.left + (objectRect.width() * coercedSX)
|
||||
|
||||
drawingRect.set(
|
||||
(if (firstMeasuredX < secondMeasuredX) firstMeasuredX else secondMeasuredX).toInt(),
|
||||
objectRect.top,
|
||||
(if (firstMeasuredX < secondMeasuredX) secondMeasuredX else firstMeasuredX).toInt(),
|
||||
objectRect.bottom
|
||||
)
|
||||
|
||||
canvas.drawRect(drawingRect, paint)
|
||||
invalidate()
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
@ -24,6 +24,9 @@ import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget;
|
||||
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget;
|
||||
import org.thoughtcrime.securesms.components.TransferControlView;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@ -54,7 +57,6 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
private ImageView image;
|
||||
private View playOverlay;
|
||||
private View captionIcon;
|
||||
private View loadIndicator;
|
||||
private OnClickListener parentClickListener;
|
||||
|
||||
@ -67,7 +69,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
private SlidesClickedListener downloadClickListener = null;
|
||||
private Slide slide = null;
|
||||
|
||||
private int radius;
|
||||
public int radius;
|
||||
|
||||
public ThumbnailView(Context context) {
|
||||
this(context, null);
|
||||
@ -84,7 +86,6 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
this.image = findViewById(R.id.thumbnail_image);
|
||||
this.playOverlay = findViewById(R.id.play_overlay);
|
||||
this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
|
||||
this.loadIndicator = findViewById(R.id.thumbnail_load_indicator);
|
||||
super.setOnClickListener(new ThumbnailClickDispatcher());
|
||||
|
||||
@ -94,10 +95,10 @@ public class ThumbnailView extends FrameLayout {
|
||||
bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0);
|
||||
bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0);
|
||||
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
|
||||
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius));
|
||||
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0);
|
||||
typedArray.recycle();
|
||||
} else {
|
||||
radius = getResources().getDimensionPixelSize(R.dimen.message_corner_collapse_radius);
|
||||
radius = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,8 +276,6 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
this.slide = slide;
|
||||
|
||||
this.captionIcon.setVisibility(slide.getCaption().isPresent() ? VISIBLE : GONE);
|
||||
|
||||
dimens[WIDTH] = naturalWidth;
|
||||
dimens[HEIGHT] = naturalHeight;
|
||||
invalidate();
|
||||
@ -398,6 +397,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
|
||||
private class ThumbnailClickDispatcher implements View.OnClickListener {
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (thumbnailClickListener != null &&
|
||||
@ -413,9 +413,9 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
|
||||
private class DownloadClickDispatcher implements View.OnClickListener {
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Log.i(TAG, "onClick() for download button");
|
||||
if (downloadClickListener != null && slide != null) {
|
||||
downloadClickListener.onClick(view, Collections.singletonList(slide));
|
||||
} else {
|
@ -34,42 +34,40 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatchList;
|
||||
import org.session.libsession.utilities.NetworkFailure;
|
||||
import org.session.libsession.utilities.NetworkFailureList;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.RecipientFormattingException;
|
||||
import org.session.libsignal.utilities.JsonUtil;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsignal.utilities.ThreadUtils;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Quote;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.RecipientFormattingException;
|
||||
import org.session.libsignal.utilities.JsonUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
@ -884,9 +882,9 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
|
||||
public boolean delete(long messageId) {
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
attachmentDatabase.deleteAttachmentsForMessage(messageId);
|
||||
ThreadUtils.queue(() -> attachmentDatabase.deleteAttachmentsForMessage(messageId));
|
||||
|
||||
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
groupReceiptDatabase.deleteRowsForMessage(messageId);
|
||||
@ -1171,9 +1169,9 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
|
||||
|
||||
return new NotificationMmsMessageRecord(id, recipient, recipient,
|
||||
addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId,
|
||||
dateSent, dateReceived, deliveryReceiptCount, threadId,
|
||||
contentLocationBytes, messageSize, expiry, status,
|
||||
transactionIdBytes, mailbox, subscriptionId, slideDeck,
|
||||
transactionIdBytes, mailbox, slideDeck,
|
||||
readReceiptCount);
|
||||
}
|
||||
|
||||
|
@ -129,7 +129,17 @@ public class MmsSmsDatabase extends Database {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
|
||||
return queryTables(PROJECTION, selection, order, "1");
|
||||
return queryTables(PROJECTION, selection, order, "1");
|
||||
}
|
||||
|
||||
public long getLastMessageID(long threadId) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
|
||||
try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) {
|
||||
cursor.moveToFirst();
|
||||
return cursor.getLong(cursor.getColumnIndex(MmsSmsColumns.ID));
|
||||
}
|
||||
}
|
||||
|
||||
public Cursor getUnread() {
|
||||
|
@ -20,6 +20,8 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
@ -28,23 +30,21 @@ import com.annimon.stream.Stream;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsignal.utilities.JsonUtil;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatchList;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsignal.utilities.JsonUtil;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
@ -413,7 +413,6 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
notifyConversationListeners(threadId);
|
||||
|
||||
|
||||
return Optional.of(new InsertResult(messageId, threadId));
|
||||
}
|
||||
}
|
||||
@ -514,7 +513,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
public boolean deleteMessage(long messageId) {
|
||||
Log.i("MessageDatabase", "Deleting: " + messageId);
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
|
||||
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false);
|
||||
notifyConversationListeners(threadId);
|
||||
@ -641,10 +640,10 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
public MessageRecord getCurrent() {
|
||||
return new SmsMessageRecord(id, message.getMessageBody(),
|
||||
message.getRecipient(), message.getRecipient(),
|
||||
1, System.currentTimeMillis(), System.currentTimeMillis(),
|
||||
System.currentTimeMillis(), System.currentTimeMillis(),
|
||||
0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(),
|
||||
threadId, 0, new LinkedList<IdentityKeyMismatch>(),
|
||||
message.getSubscriptionId(), message.getExpiresIn(),
|
||||
message.getExpiresIn(),
|
||||
System.currentTimeMillis(), 0, false);
|
||||
}
|
||||
}
|
||||
@ -696,9 +695,8 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
return new SmsMessageRecord(messageId, body, recipient,
|
||||
recipient,
|
||||
addressDeviceId,
|
||||
dateSent, dateReceived, deliveryReceiptCount, type,
|
||||
threadId, status, mismatches, subscriptionId,
|
||||
threadId, status, mismatches,
|
||||
expiresIn, expireStarted, readReceiptCount, unidentified);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
@ -28,6 +27,7 @@ import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
|
||||
@ -105,7 +105,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
}
|
||||
else -> Optional.absent()
|
||||
}
|
||||
val pointerAttachments = attachments.mapNotNull {
|
||||
val pointers = attachments.mapNotNull {
|
||||
it.toSignalAttachment()
|
||||
}
|
||||
val targetAddress = if (isUserSender && !message.syncTarget.isNullOrEmpty()) {
|
||||
@ -121,7 +121,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val linkPreviews: Optional<List<LinkPreview>> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! })
|
||||
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
|
||||
val insertResult = if (message.sender == getUserPublicKey()) {
|
||||
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointerAttachments, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
|
||||
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
|
||||
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
|
||||
} else {
|
||||
// It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment
|
||||
@ -304,6 +304,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
}
|
||||
}
|
||||
|
||||
override fun markAsSending(timestamp: Long, author: String) {
|
||||
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||
if (messageRecord.isMms) {
|
||||
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
|
||||
mmsDatabase.markAsSending(messageRecord.getId())
|
||||
} else {
|
||||
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
||||
smsDatabase.markAsSending(messageRecord.getId())
|
||||
messageRecord.isPending
|
||||
}
|
||||
}
|
||||
|
||||
override fun markUnidentified(timestamp: Long, author: String) {
|
||||
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||
|
@ -582,7 +582,7 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
|
||||
private @Nullable Uri getAttachmentUriFor(MessageRecord record) {
|
||||
if (!record.isMms() || record.isMmsNotification() || record.isGroupAction()) return null;
|
||||
if (!record.isMms() || record.isMmsNotification()) return null;
|
||||
|
||||
SlideDeck slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
|
||||
Slide thumbnail = slideDeck.getThumbnailSlide();
|
||||
|
@ -17,12 +17,13 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.SpannableString;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
/**
|
||||
* The base class for all message record models. Encapsulates basic data
|
||||
@ -33,9 +34,7 @@ import org.session.libsession.utilities.recipients.Recipient;
|
||||
*/
|
||||
|
||||
public abstract class DisplayRecord {
|
||||
|
||||
protected final long type;
|
||||
|
||||
private final Recipient recipient;
|
||||
private final long dateSent;
|
||||
private final long dateReceived;
|
||||
@ -46,8 +45,8 @@ public abstract class DisplayRecord {
|
||||
private final int readReceiptCount;
|
||||
|
||||
DisplayRecord(String body, Recipient recipient, long dateSent,
|
||||
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
|
||||
long type, int readReceiptCount)
|
||||
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
|
||||
long type, int readReceiptCount)
|
||||
{
|
||||
this.threadId = threadId;
|
||||
this.recipient = recipient;
|
||||
@ -63,138 +62,63 @@ public abstract class DisplayRecord {
|
||||
public @NonNull String getBody() {
|
||||
return body == null ? "" : body;
|
||||
}
|
||||
public abstract SpannableString getDisplayBody(@NonNull Context context);
|
||||
public Recipient getRecipient() { return recipient; }
|
||||
public long getDateSent() { return dateSent; }
|
||||
public long getDateReceived() { return dateReceived; }
|
||||
public long getThreadId() { return threadId; }
|
||||
public int getDeliveryStatus() { return deliveryStatus; }
|
||||
public int getDeliveryReceiptCount() { return deliveryReceiptCount; }
|
||||
public int getReadReceiptCount() { return readReceiptCount; }
|
||||
|
||||
public boolean isDelivered() {
|
||||
return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE
|
||||
&& deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
|
||||
}
|
||||
|
||||
public boolean isSent() {
|
||||
return !isFailed() && !isPending();
|
||||
}
|
||||
|
||||
public boolean isFailed() {
|
||||
return
|
||||
MmsSmsColumns.Types.isFailedMessageType(type) ||
|
||||
MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type) ||
|
||||
deliveryStatus >= SmsDatabase.Status.STATUS_FAILED;
|
||||
return MmsSmsColumns.Types.isFailedMessageType(type)
|
||||
|| MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type)
|
||||
|| deliveryStatus >= SmsDatabase.Status.STATUS_FAILED;
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return MmsSmsColumns.Types.isPendingMessageType(type) &&
|
||||
!MmsSmsColumns.Types.isIdentityVerified(type) &&
|
||||
!MmsSmsColumns.Types.isIdentityDefault(type);
|
||||
return MmsSmsColumns.Types.isPendingMessageType(type)
|
||||
&& !MmsSmsColumns.Types.isIdentityVerified(type)
|
||||
&& !MmsSmsColumns.Types.isIdentityDefault(type);
|
||||
}
|
||||
|
||||
public boolean isRead() { return readReceiptCount > 0; }
|
||||
|
||||
public boolean isOutgoing() {
|
||||
return MmsSmsColumns.Types.isOutgoingMessageType(type);
|
||||
}
|
||||
|
||||
public abstract SpannableString getDisplayBody(@NonNull Context context);
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public long getDateSent() {
|
||||
return dateSent;
|
||||
}
|
||||
|
||||
public long getDateReceived() {
|
||||
return dateReceived;
|
||||
}
|
||||
|
||||
public long getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
public boolean isKeyExchange() {
|
||||
return SmsDatabase.Types.isKeyExchangeType(type);
|
||||
}
|
||||
|
||||
public boolean isEndSession() { return SmsDatabase.Types.isEndSessionType(type); }
|
||||
|
||||
public boolean isLokiSessionRestoreSent() { return SmsDatabase.Types.isLokiSessionRestoreSentType(type); }
|
||||
|
||||
public boolean isLokiSessionRestoreDone() { return SmsDatabase.Types.isLokiSessionRestoreDoneType(type); }
|
||||
|
||||
// TODO isGroupUpdate and isGroupQuit are kept for compatibility with old update messages, they can be removed later on
|
||||
public boolean isGroupUpdate() {
|
||||
return SmsDatabase.Types.isGroupUpdate(type);
|
||||
}
|
||||
|
||||
public boolean isGroupQuit() {
|
||||
return SmsDatabase.Types.isGroupQuit(type);
|
||||
}
|
||||
|
||||
public boolean isGroupUpdateMessage() {
|
||||
return SmsDatabase.Types.isGroupUpdateMessage(type);
|
||||
}
|
||||
|
||||
//TODO isGroupAction can be replaced by isGroupUpdateMessage in the code when the 2 functions above are removed
|
||||
public boolean isGroupAction() {
|
||||
return isGroupUpdate() || isGroupQuit() || isGroupUpdateMessage();
|
||||
}
|
||||
|
||||
public boolean isExpirationTimerUpdate() {
|
||||
return SmsDatabase.Types.isExpirationTimerUpdate(type);
|
||||
}
|
||||
|
||||
// Data extraction
|
||||
|
||||
public boolean isMediaSavedExtraction() {
|
||||
return MmsSmsColumns.Types.isMediaSavedExtraction(type);
|
||||
}
|
||||
|
||||
public boolean isScreenshotExtraction() {
|
||||
return MmsSmsColumns.Types.isScreenshotExtraction(type);
|
||||
}
|
||||
|
||||
public boolean isDataExtraction() {
|
||||
return isMediaSavedExtraction() || isScreenshotExtraction();
|
||||
}
|
||||
|
||||
public boolean isOpenGroupInvitation() {
|
||||
return MmsSmsColumns.Types.isOpenGroupInvitation(type);
|
||||
}
|
||||
|
||||
public boolean isExpirationTimerUpdate() { return SmsDatabase.Types.isExpirationTimerUpdate(type); }
|
||||
public boolean isMediaSavedNotification() { return MmsSmsColumns.Types.isMediaSavedExtraction(type); }
|
||||
public boolean isScreenshotNotification() { return MmsSmsColumns.Types.isScreenshotExtraction(type); }
|
||||
public boolean isDataExtractionNotification() { return isMediaSavedNotification() || isScreenshotNotification(); }
|
||||
public boolean isOpenGroupInvitation() { return MmsSmsColumns.Types.isOpenGroupInvitation(type); }
|
||||
public boolean isCallLog() {
|
||||
return SmsDatabase.Types.isCallLog(type);
|
||||
}
|
||||
|
||||
public boolean isJoined() {
|
||||
return SmsDatabase.Types.isJoinedType(type);
|
||||
}
|
||||
|
||||
public boolean isIncomingCall() {
|
||||
return SmsDatabase.Types.isIncomingCall(type);
|
||||
}
|
||||
|
||||
public boolean isOutgoingCall() {
|
||||
return SmsDatabase.Types.isOutgoingCall(type);
|
||||
}
|
||||
|
||||
public boolean isMissedCall() {
|
||||
return SmsDatabase.Types.isMissedCall(type);
|
||||
}
|
||||
|
||||
public boolean isVerificationStatusChange() {
|
||||
return SmsDatabase.Types.isIdentityDefault(type) || SmsDatabase.Types.isIdentityVerified(type);
|
||||
}
|
||||
|
||||
public int getDeliveryStatus() {
|
||||
return deliveryStatus;
|
||||
}
|
||||
|
||||
public int getDeliveryReceiptCount() {
|
||||
return deliveryReceiptCount;
|
||||
}
|
||||
|
||||
public int getReadReceiptCount() {
|
||||
return readReceiptCount;
|
||||
}
|
||||
|
||||
public boolean isDelivered() {
|
||||
return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE &&
|
||||
deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
|
||||
}
|
||||
|
||||
public boolean isRemoteRead() {
|
||||
return readReceiptCount > 0;
|
||||
}
|
||||
|
||||
public boolean isPendingInsecureSmsFallback() {
|
||||
return SmsDatabase.Types.isPendingInsecureSmsFallbackType(type);
|
||||
public boolean isControlMessage() {
|
||||
return isGroupUpdateMessage() || isExpirationTimerUpdate() || isDataExtractionNotification();
|
||||
}
|
||||
}
|
||||
|
@ -17,21 +17,19 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.SpannableString;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.SpannableString;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import java.util.List;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
* Represents the message record model for MMS messages that contain
|
||||
@ -42,26 +40,24 @@ import java.util.List;
|
||||
*/
|
||||
|
||||
public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
private final static String TAG = MediaMmsMessageRecord.class.getSimpleName();
|
||||
|
||||
private final int partCount;
|
||||
private final int partCount;
|
||||
|
||||
public MediaMmsMessageRecord(long id, Recipient conversationRecipient,
|
||||
Recipient individualRecipient, int recipientDeviceId,
|
||||
long dateSent, long dateReceived, int deliveryReceiptCount,
|
||||
long threadId, String body,
|
||||
@NonNull SlideDeck slideDeck,
|
||||
int partCount, long mailbox,
|
||||
List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> failures, int subscriptionId,
|
||||
long expiresIn, long expireStarted, int readReceiptCount,
|
||||
@Nullable Quote quote, @NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified)
|
||||
Recipient individualRecipient, int recipientDeviceId,
|
||||
long dateSent, long dateReceived, int deliveryReceiptCount,
|
||||
long threadId, String body,
|
||||
@NonNull SlideDeck slideDeck,
|
||||
int partCount, long mailbox,
|
||||
List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> failures, int subscriptionId,
|
||||
long expiresIn, long expireStarted, int readReceiptCount,
|
||||
@Nullable Quote quote, @NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified)
|
||||
{
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
|
||||
dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
subscriptionId, expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts,
|
||||
linkPreviews, unidentified);
|
||||
super(id, body, conversationRecipient, individualRecipient, dateSent,
|
||||
dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts,
|
||||
linkPreviews, unidentified);
|
||||
this.partCount = partCount;
|
||||
}
|
||||
|
||||
@ -82,8 +78,6 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
|
||||
} else if (MmsDatabase.Types.isNoRemoteSessionType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session));
|
||||
} else if (isLegacyMessage()) {
|
||||
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
|
||||
}
|
||||
|
||||
return super.getDisplayBody(context);
|
||||
|
@ -17,22 +17,18 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
|
||||
import org.session.libsession.messaging.utilities.UpdateMessageBuilder;
|
||||
import org.session.libsession.messaging.utilities.UpdateMessageData;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.NetworkFailure;
|
||||
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import java.util.List;
|
||||
@ -46,145 +42,79 @@ import java.util.List;
|
||||
*
|
||||
*/
|
||||
public abstract class MessageRecord extends DisplayRecord {
|
||||
|
||||
private final Recipient individualRecipient;
|
||||
private final int recipientDeviceId;
|
||||
public final long id;
|
||||
private final List<IdentityKeyMismatch> mismatches;
|
||||
private final List<NetworkFailure> networkFailures;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final long expireStarted;
|
||||
private final boolean unidentified;
|
||||
public final long id;
|
||||
|
||||
public abstract boolean isMms();
|
||||
public abstract boolean isMmsNotification();
|
||||
|
||||
MessageRecord(long id, String body, Recipient conversationRecipient,
|
||||
Recipient individualRecipient, int recipientDeviceId,
|
||||
long dateSent, long dateReceived, long threadId,
|
||||
int deliveryStatus, int deliveryReceiptCount, long type,
|
||||
List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> networkFailures,
|
||||
int subscriptionId, long expiresIn, long expireStarted,
|
||||
int readReceiptCount, boolean unidentified)
|
||||
Recipient individualRecipient,
|
||||
long dateSent, long dateReceived, long threadId,
|
||||
int deliveryStatus, int deliveryReceiptCount, long type,
|
||||
List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> networkFailures,
|
||||
long expiresIn, long expireStarted,
|
||||
int readReceiptCount, boolean unidentified)
|
||||
{
|
||||
super(body, conversationRecipient, dateSent, dateReceived,
|
||||
threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount);
|
||||
threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount);
|
||||
this.id = id;
|
||||
this.individualRecipient = individualRecipient;
|
||||
this.recipientDeviceId = recipientDeviceId;
|
||||
this.mismatches = mismatches;
|
||||
this.networkFailures = networkFailures;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expireStarted = expireStarted;
|
||||
this.unidentified = unidentified;
|
||||
}
|
||||
|
||||
public abstract boolean isMms();
|
||||
public abstract boolean isMmsNotification();
|
||||
|
||||
public boolean isSecure() {
|
||||
return MmsSmsColumns.Types.isSecureType(type);
|
||||
}
|
||||
|
||||
public boolean isLegacyMessage() {
|
||||
return MmsSmsColumns.Types.isLegacyType(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
if(isGroupUpdateMessage()) {
|
||||
UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(getBody());
|
||||
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
|
||||
} else if (isExpirationTimerUpdate()) {
|
||||
int seconds = (int) (getExpiresIn() / 1000);
|
||||
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
|
||||
} else if (isDataExtraction()) {
|
||||
if (isScreenshotExtraction()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize())));
|
||||
else if (isMediaSavedExtraction()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize())));
|
||||
}
|
||||
// TODO below lines are left here for compatibility with older group update messages, it can be deleted later on
|
||||
else if (isGroupUpdate() && isOutgoing()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group));
|
||||
} else if (isGroupUpdate()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_s_updated_group, getIndividualRecipient().toShortString()));
|
||||
} else if (isGroupQuit() && isOutgoing()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_left_group));
|
||||
} else if (isGroupQuit()) {
|
||||
return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString()));
|
||||
}
|
||||
|
||||
return new SpannableString(getBody());
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isPush() {
|
||||
return SmsDatabase.Types.isPushType(type) && !SmsDatabase.Types.isForcedSms(type);
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
if (getRecipient().getAddress().isOpenGroup()) {
|
||||
return getDateReceived();
|
||||
}
|
||||
if (isPush() && getDateSent() < getDateReceived()) {
|
||||
return getDateSent();
|
||||
}
|
||||
return getDateReceived();
|
||||
return getDateSent();
|
||||
}
|
||||
|
||||
public boolean isForcedSms() {
|
||||
return SmsDatabase.Types.isForcedSms(type);
|
||||
public Recipient getIndividualRecipient() {
|
||||
return individualRecipient;
|
||||
}
|
||||
|
||||
public boolean isIdentityVerified() {
|
||||
return SmsDatabase.Types.isIdentityVerified(type);
|
||||
public long getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public boolean isIdentityDefault() {
|
||||
return SmsDatabase.Types.isIdentityDefault(type);
|
||||
public List<NetworkFailure> getNetworkFailures() {
|
||||
return networkFailures;
|
||||
}
|
||||
|
||||
public boolean isBundleKeyExchange() {
|
||||
return SmsDatabase.Types.isBundleKeyExchange(type);
|
||||
}
|
||||
|
||||
public boolean isIdentityUpdate() {
|
||||
return SmsDatabase.Types.isIdentityUpdate(type);
|
||||
}
|
||||
|
||||
public boolean isCorruptedKeyExchange() {
|
||||
return SmsDatabase.Types.isCorruptedKeyExchange(type);
|
||||
}
|
||||
|
||||
public boolean isInvalidVersionKeyExchange() {
|
||||
return SmsDatabase.Types.isInvalidVersionKeyExchange(type);
|
||||
}
|
||||
|
||||
public boolean isUpdate() {
|
||||
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || isDataExtraction() ||
|
||||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isLokiSessionRestoreSent() || isLokiSessionRestoreDone();
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
public long getExpireStarted() { return expireStarted; }
|
||||
|
||||
public boolean isMediaPending() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public Recipient getIndividualRecipient() {
|
||||
return individualRecipient;
|
||||
public boolean isUpdate() {
|
||||
return isExpirationTimerUpdate() || isCallLog() || isDataExtractionNotification();
|
||||
}
|
||||
|
||||
public long getType() {
|
||||
return type;
|
||||
}
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
if (isGroupUpdateMessage()) {
|
||||
UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(getBody());
|
||||
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
|
||||
} else if (isExpirationTimerUpdate()) {
|
||||
int seconds = (int) (getExpiresIn() / 1000);
|
||||
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
|
||||
} else if (isDataExtractionNotification()) {
|
||||
if (isScreenshotNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize())));
|
||||
else if (isMediaSavedNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize())));
|
||||
}
|
||||
|
||||
public List<IdentityKeyMismatch> getIdentityKeyMismatches() {
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
public List<NetworkFailure> getNetworkFailures() {
|
||||
return networkFailures;
|
||||
return new SpannableString(getBody());
|
||||
}
|
||||
|
||||
protected SpannableString emphasisAdded(String sequence) {
|
||||
@ -196,25 +126,12 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
}
|
||||
|
||||
public boolean equals(Object other) {
|
||||
return other != null &&
|
||||
other instanceof MessageRecord &&
|
||||
((MessageRecord) other).getId() == getId() &&
|
||||
((MessageRecord) other).isMms() == isMms();
|
||||
return other instanceof MessageRecord
|
||||
&& ((MessageRecord) other).getId() == getId()
|
||||
&& ((MessageRecord) other).isMms() == isMms();
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return (int)getId();
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public long getExpireStarted() {
|
||||
return expireStarted;
|
||||
}
|
||||
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
}
|
||||
|
@ -1,43 +1,35 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class MmsMessageRecord extends MessageRecord {
|
||||
|
||||
private final @NonNull SlideDeck slideDeck;
|
||||
private final @Nullable Quote quote;
|
||||
private final @NonNull List<Contact> contacts = new LinkedList<>();
|
||||
private final @NonNull List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
|
||||
MmsMessageRecord(long id, String body, Recipient conversationRecipient,
|
||||
Recipient individualRecipient, int recipientDeviceId, long dateSent,
|
||||
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
|
||||
long type, List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> networkFailures, int subscriptionId, long expiresIn,
|
||||
long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount,
|
||||
@Nullable Quote quote, @NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified)
|
||||
Recipient individualRecipient, long dateSent,
|
||||
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
|
||||
long type, List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> networkFailures, long expiresIn,
|
||||
long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount,
|
||||
@Nullable Quote quote, @NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified)
|
||||
{
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified);
|
||||
|
||||
super(id, body, conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, expiresIn, expireStarted, readReceiptCount, unidentified);
|
||||
this.slideDeck = slideDeck;
|
||||
this.quote = quote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
this.linkPreviews.addAll(linkPreviews);
|
||||
}
|
||||
@ -66,15 +58,12 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
||||
public boolean containsMediaSlide() {
|
||||
return slideDeck.containsMediaSlide();
|
||||
}
|
||||
|
||||
public @Nullable Quote getQuote() {
|
||||
return quote;
|
||||
}
|
||||
|
||||
public @NonNull List<Contact> getSharedContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public @NonNull List<LinkPreview> getLinkPreviews() {
|
||||
return linkPreviews;
|
||||
}
|
||||
|
@ -17,19 +17,17 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.SpannableString;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
* Represents the message record model for MMS messages that are
|
||||
@ -40,7 +38,6 @@ import java.util.LinkedList;
|
||||
*/
|
||||
|
||||
public class NotificationMmsMessageRecord extends MmsMessageRecord {
|
||||
|
||||
private final byte[] contentLocation;
|
||||
private final long messageSize;
|
||||
private final long expiry;
|
||||
@ -48,16 +45,16 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
|
||||
private final byte[] transactionId;
|
||||
|
||||
public NotificationMmsMessageRecord(long id, Recipient conversationRecipient,
|
||||
Recipient individualRecipient, int recipientDeviceId,
|
||||
long dateSent, long dateReceived, int deliveryReceiptCount,
|
||||
long threadId, byte[] contentLocation, long messageSize,
|
||||
long expiry, int status, byte[] transactionId, long mailbox,
|
||||
int subscriptionId, SlideDeck slideDeck, int readReceiptCount)
|
||||
Recipient individualRecipient,
|
||||
long dateSent, long dateReceived, int deliveryReceiptCount,
|
||||
long threadId, byte[] contentLocation, long messageSize,
|
||||
long expiry, int status, byte[] transactionId, long mailbox,
|
||||
SlideDeck slideDeck, int readReceiptCount)
|
||||
{
|
||||
super(id, "", conversationRecipient, individualRecipient, recipientDeviceId,
|
||||
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
|
||||
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(), subscriptionId,
|
||||
0, 0, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false);
|
||||
super(id, "", conversationRecipient, individualRecipient,
|
||||
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
|
||||
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(),
|
||||
0, 0, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false);
|
||||
|
||||
this.contentLocation = contentLocation;
|
||||
this.messageSize = messageSize;
|
||||
@ -69,19 +66,15 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
|
||||
public byte[] getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public byte[] getContentLocation() {
|
||||
return contentLocation;
|
||||
}
|
||||
|
||||
public long getMessageSize() {
|
||||
return (messageSize + 1023) / 1024;
|
||||
}
|
||||
|
||||
public long getExpiration() {
|
||||
return expiry * 1000;
|
||||
}
|
||||
@ -91,11 +84,6 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPending() {
|
||||
return false;
|
||||
|
@ -19,17 +19,12 @@ package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.SpannableString;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
@ -41,20 +36,19 @@ import network.loki.messenger.R;
|
||||
public class SmsMessageRecord extends MessageRecord {
|
||||
|
||||
public SmsMessageRecord(long id,
|
||||
String body, Recipient recipient,
|
||||
Recipient individualRecipient,
|
||||
int recipientDeviceId,
|
||||
long dateSent, long dateReceived,
|
||||
int deliveryReceiptCount,
|
||||
long type, long threadId,
|
||||
int status, List<IdentityKeyMismatch> mismatches,
|
||||
int subscriptionId, long expiresIn, long expireStarted,
|
||||
int readReceiptCount, boolean unidentified)
|
||||
String body, Recipient recipient,
|
||||
Recipient individualRecipient,
|
||||
long dateSent, long dateReceived,
|
||||
int deliveryReceiptCount,
|
||||
long type, long threadId,
|
||||
int status, List<IdentityKeyMismatch> mismatches,
|
||||
long expiresIn, long expireStarted,
|
||||
int readReceiptCount, boolean unidentified)
|
||||
{
|
||||
super(id, body, recipient, individualRecipient, recipientDeviceId,
|
||||
dateSent, dateReceived, threadId, status, deliveryReceiptCount, type,
|
||||
mismatches, new LinkedList<>(), subscriptionId,
|
||||
expiresIn, expireStarted, readReceiptCount, unidentified);
|
||||
super(id, body, recipient, individualRecipient,
|
||||
dateSent, dateReceived, threadId, status, deliveryReceiptCount, type,
|
||||
mismatches, new LinkedList<>(),
|
||||
expiresIn, expireStarted, readReceiptCount, unidentified);
|
||||
}
|
||||
|
||||
public long getType() {
|
||||
@ -63,33 +57,12 @@ public class SmsMessageRecord extends MessageRecord {
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
Recipient recipient = getRecipient();
|
||||
if (SmsDatabase.Types.isFailedDecryptType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
|
||||
} else if (isCorruptedKeyExchange()) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_corrupted_key_exchange_message));
|
||||
} else if (isInvalidVersionKeyExchange()) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_key_exchange_message_for_invalid_protocol_version));
|
||||
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
|
||||
} else if (isBundleKeyExchange()) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_message_with_new_safety_number_tap_to_process));
|
||||
} else if (isKeyExchange() && isOutgoing()) {
|
||||
return new SpannableString("");
|
||||
} else if (isKeyExchange() && !isOutgoing()) {
|
||||
return emphasisAdded(context.getString(R.string.ConversationItem_received_key_exchange_message_tap_to_process));
|
||||
} else if (SmsDatabase.Types.isDuplicateMessageType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
|
||||
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
|
||||
} else if (isLokiSessionRestoreSent()) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset));
|
||||
} else if (isLokiSessionRestoreDone()) {
|
||||
return emphasisAdded(context.getString(R.string.view_reset_secure_session_done_message));
|
||||
} else if (isEndSession() && isOutgoing()) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset));
|
||||
} else if (isEndSession()) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset_s, getIndividualRecipient().toShortString()));
|
||||
} else {
|
||||
return super.getDisplayBody(context);
|
||||
}
|
||||
|
@ -73,22 +73,14 @@ public class ThreadRecord extends DisplayRecord {
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
Recipient recipient = getRecipient();
|
||||
if (isGroupUpdate() || isGroupUpdateMessage()) {
|
||||
if (isGroupUpdateMessage()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
||||
} else if (isGroupQuit()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group));
|
||||
} else if (isOpenGroupInvitation()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_open_group_invitation));
|
||||
} else if (isKeyExchange()) {
|
||||
return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message));
|
||||
} else if (SmsDatabase.Types.isFailedDecryptType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
|
||||
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
|
||||
} else if (isLokiSessionRestoreSent()) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset));
|
||||
} else if (isLokiSessionRestoreDone()) {
|
||||
return emphasisAdded(context.getString(R.string.view_reset_secure_session_done_message));
|
||||
} else if (SmsDatabase.Types.isEndSessionType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_secure_session_reset));
|
||||
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
|
||||
|
@ -95,7 +95,7 @@ public class LinkPreviewRepository implements InjectableType {
|
||||
|
||||
private @NonNull RequestController fetchMetadata(@NonNull String url, Callback<Metadata> callback) {
|
||||
Call call = client.newCall(new Request.Builder().url(url).removeHeader("User-Agent").addHeader("User-Agent",
|
||||
"WhatsApp").cacheControl(NO_CACHE).build());
|
||||
"WhatsApp").cacheControl(NO_CACHE).build());
|
||||
|
||||
call.enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
@ -186,18 +186,18 @@ public class LinkPreviewRepository implements InjectableType {
|
||||
byte[] bytes = baos.toByteArray();
|
||||
Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory();
|
||||
|
||||
return Optional.of(new UriAttachment(uri,
|
||||
uri,
|
||||
contentType,
|
||||
AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED,
|
||||
bytes.length,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight(),
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
null));
|
||||
return Optional.of(new UriAttachment(uri,
|
||||
uri,
|
||||
contentType,
|
||||
AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED,
|
||||
bytes.length,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight(),
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
null));
|
||||
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@ public class LinkPreviewViewModel extends ViewModel {
|
||||
activeRequest = null;
|
||||
}
|
||||
|
||||
if (!link.isPresent() || !isCursorPositionValid(text, link.get(), cursorStart, cursorEnd)) {
|
||||
if (!link.isPresent()) {
|
||||
activeUrl = null;
|
||||
linkPreviewState.setValue(LinkPreviewState.forEmpty());
|
||||
return;
|
||||
|
@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.loki.utilities.fadeOut
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
|
||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
||||
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||
@ -135,10 +136,10 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
|
||||
// region Convenience
|
||||
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
|
||||
val intent = Intent(context, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId)
|
||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
// endregion
|
@ -7,35 +7,44 @@ import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_create_private_chat.loader
|
||||
import kotlinx.android.synthetic.main.activity_create_private_chat.tabLayout
|
||||
import kotlinx.android.synthetic.main.activity_create_private_chat.viewPager
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import kotlinx.android.synthetic.main.activity_create_private_chat.*
|
||||
import kotlinx.android.synthetic.main.fragment_enter_public_key.*
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.DistributionTypes
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.PublicKeyValidation
|
||||
|
||||
|
||||
class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private val adapter = CreatePrivateChatActivityAdapter(this)
|
||||
private var isKeyboardShowing = false
|
||||
set(value) {
|
||||
val hasChanged = (field != value)
|
||||
field = value
|
||||
if (hasChanged) {
|
||||
adapter.isKeyboardShowing = value
|
||||
}
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
@ -47,11 +56,15 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
}
|
||||
rootLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_done, menu)
|
||||
return true
|
||||
override fun onGlobalLayout() {
|
||||
val diff = rootLayout.rootView.height - rootLayout.height
|
||||
val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics
|
||||
val estimatedKeyboardHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics)
|
||||
this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight)
|
||||
}
|
||||
})
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -73,13 +86,6 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when(item.itemId) {
|
||||
R.id.doneButton -> adapter.enterPublicKeyFragment.createPrivateChatIfPossible()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun handleQRCodeScanned(hexEncodedPublicKey: String) {
|
||||
createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
@ -106,12 +112,12 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
|
||||
|
||||
private fun createPrivateChat(hexEncodedPublicKey: String) {
|
||||
val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false)
|
||||
val intent = Intent(this, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
|
||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
|
||||
intent.setDataAndType(getIntent().data, getIntent().type)
|
||||
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
@ -122,6 +128,8 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
|
||||
// region Adapter
|
||||
private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
val enterPublicKeyFragment = EnterPublicKeyFragment()
|
||||
var isKeyboardShowing = false
|
||||
set(value) { field = value; enterPublicKeyFragment.isKeyboardShowing = isKeyboardShowing }
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
@ -152,6 +160,8 @@ private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatAc
|
||||
|
||||
// region Enter Public Key Fragment
|
||||
class EnterPublicKeyFragment : Fragment() {
|
||||
var isKeyboardShowing = false
|
||||
set(value) { field = value; handleIsKeyboardShowingChanged() }
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
get() {
|
||||
@ -182,6 +192,10 @@ class EnterPublicKeyFragment : Fragment() {
|
||||
createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() }
|
||||
}
|
||||
|
||||
private fun handleIsKeyboardShowingChanged() {
|
||||
optionalContentContainer.isVisible = !isKeyboardShowing
|
||||
}
|
||||
|
||||
private fun copyPublicKey() {
|
||||
val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", hexEncodedPublicKey)
|
||||
@ -197,9 +211,10 @@ class EnterPublicKeyFragment : Fragment() {
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun createPrivateChatIfPossible() {
|
||||
val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString() ?: ""
|
||||
(requireActivity() as CreatePrivateChatActivity).createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||
private fun createPrivateChatIfPossible() {
|
||||
val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString()
|
||||
val activity = requireActivity() as CreatePrivateChatActivity
|
||||
activity.createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
@ -38,6 +38,7 @@ import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
|
||||
@ -342,13 +343,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
}
|
||||
|
||||
private fun openConversation(thread: ThreadRecord) {
|
||||
val intent = Intent(this, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, thread.recipient.address)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, thread.threadId)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, thread.distributionType)
|
||||
intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis())
|
||||
intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, thread.lastSeen)
|
||||
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1)
|
||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||
@ -40,6 +41,7 @@ import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelega
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel
|
||||
import org.thoughtcrime.securesms.loki.viewmodel.State
|
||||
import java.util.*
|
||||
|
||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||
@ -126,10 +128,10 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
||||
|
||||
// region Convenience
|
||||
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
|
||||
val intent = Intent(context, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId)
|
||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
// endregion
|
||||
@ -179,6 +181,7 @@ class EnterChatURLFragment : Fragment() {
|
||||
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
||||
defaultRoomsContainer.isVisible = state is State.Success
|
||||
defaultRoomsLoaderContainer.isVisible = state is State.Loading
|
||||
defaultRoomsLoader.isVisible = state is State.Loading
|
||||
when (state) {
|
||||
State.Loading -> {
|
||||
@ -210,7 +213,6 @@ class EnterChatURLFragment : Fragment() {
|
||||
chip.setOnClickListener {
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
|
||||
}
|
||||
|
||||
defaultRoomsGridLayout.addView(chip)
|
||||
}
|
||||
if ((groups.size and 1) != 0) { // This checks that the number of rooms is even
|
||||
@ -222,7 +224,7 @@ class EnterChatURLFragment : Fragment() {
|
||||
private fun joinPublicChatIfPossible() {
|
||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
|
||||
val chatURL = chatURLEditText.text.trim().toString().toLowerCase()
|
||||
val chatURL = chatURLEditText.text.trim().toString().toLowerCase(Locale.US)
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
||||
}
|
||||
// endregion
|
||||
|
@ -7,7 +7,6 @@ import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
|
||||
class OpenGroupGuidelinesActivity : BaseActionBarActivity() {
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_open_group_guidelines)
|
||||
@ -49,5 +48,4 @@ class OpenGroupGuidelinesActivity : BaseActionBarActivity() {
|
||||
Trust only those with an admin crown in chat. No admin will ever DM you first. No admin will ever message you for Oxen coins.
|
||||
""".trimIndent()
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -26,6 +26,7 @@ import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.FileProviderUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
@ -53,12 +54,12 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperF
|
||||
fun createPrivateChatIfPossible(hexEncodedPublicKey: String) {
|
||||
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) { return Toast.makeText(this, R.string.invalid_session_id, Toast.LENGTH_SHORT).show() }
|
||||
val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false)
|
||||
val intent = Intent(this, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
|
||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
|
||||
intent.setDataAndType(getIntent().data, getIntent().type)
|
||||
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
|
@ -12,18 +12,16 @@ import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
|
||||
|
||||
class ClearAllDataDialog : DialogFragment() {
|
||||
class ClearAllDataDialog : BaseDialog() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null)
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.clearAllDataButton.setOnClickListener { clearAllData() }
|
||||
builder.setView(contentView)
|
||||
val result = builder.create()
|
||||
result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
return result
|
||||
}
|
||||
|
||||
private fun clearAllData() {
|
||||
|
@ -1,24 +1,20 @@
|
||||
package org.thoughtcrime.securesms.loki.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import kotlinx.android.synthetic.main.dialog_seed.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
|
||||
class SeedDialog : DialogFragment() {
|
||||
class SeedDialog : BaseDialog() {
|
||||
|
||||
private val seed by lazy {
|
||||
var hexEncodedSeed = IdentityKeyUtil.retrieve(requireContext(), IdentityKeyUtil.LOKI_SEED)
|
||||
@ -31,16 +27,12 @@ class SeedDialog : DialogFragment() {
|
||||
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_seed, null)
|
||||
contentView.seedTextView.text = seed
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.copyButton.setOnClickListener { copySeed() }
|
||||
builder.setView(contentView)
|
||||
val result = builder.create()
|
||||
result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
return result
|
||||
}
|
||||
|
||||
private fun copySeed() {
|
||||
|
@ -31,7 +31,7 @@ class ScanQRCodeWrapperFragment : Fragment(), ScanQRCodePlaceholderFragmentDeleg
|
||||
|
||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||
super.setUserVisibleHint(isVisibleToUser)
|
||||
enabled = isVisibleToUser
|
||||
enabled = isVisibleToUser
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@ -87,5 +87,6 @@ class ScanQRCodeWrapperFragment : Fragment(), ScanQRCodePlaceholderFragmentDeleg
|
||||
}
|
||||
|
||||
interface ScanQRCodeWrapperFragmentDelegate {
|
||||
|
||||
fun handleQRCodeScanned(string: String)
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
package org.thoughtcrime.securesms.loki.utilities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@ -52,4 +56,13 @@ fun AppCompatActivity.show(intent: Intent, isForResult: Boolean = false) {
|
||||
startActivity(intent)
|
||||
}
|
||||
overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out)
|
||||
}
|
||||
|
||||
interface ActivityDispatcher {
|
||||
companion object {
|
||||
const val SERVICE = "ActivityDispatcher_SERVICE"
|
||||
@SuppressLint("WrongConstant")
|
||||
fun get(context: Context) = context.getSystemService(SERVICE) as? ActivityDispatcher
|
||||
}
|
||||
fun dispatchIntent(body: (Context)->Intent?)
|
||||
}
|
@ -14,6 +14,19 @@ fun Resources.getColorWithID(@ColorRes id: Int, theme: Resources.Theme?): Int {
|
||||
}
|
||||
|
||||
fun toPx(dp: Int, resources: Resources): Int {
|
||||
val scale = resources.displayMetrics.density
|
||||
return (dp * scale).roundToInt()
|
||||
return toPx(dp.toFloat(), resources).roundToInt()
|
||||
}
|
||||
|
||||
fun toPx(dp: Float, resources: Resources): Float {
|
||||
val scale = resources.displayMetrics.density
|
||||
return (dp * scale)
|
||||
}
|
||||
|
||||
fun toDp(px: Int, resources: Resources): Int {
|
||||
return toDp(px.toFloat(), resources).roundToInt()
|
||||
}
|
||||
|
||||
fun toDp(px: Float, resources: Resources): Float {
|
||||
val scale = resources.displayMetrics.density
|
||||
return (px / scale)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Range
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.combine.Tuple2
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
@ -23,7 +24,7 @@ object MentionUtilities {
|
||||
|
||||
@JvmStatic
|
||||
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString {
|
||||
var text = text
|
||||
@Suppress("NAME_SHADOWING") var text = text
|
||||
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
||||
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
|
||||
val pattern = Pattern.compile("@[0-9a-fA-F]*")
|
||||
@ -38,7 +39,7 @@ object MentionUtilities {
|
||||
TextSecurePreferences.getProfileName(context)
|
||||
} else {
|
||||
val contact = DatabaseFactory.getSessionContactDatabase(context).getContactWithSessionID(publicKey)
|
||||
val context = if (isOpenGroup) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
|
||||
@Suppress("NAME_SHADOWING") val context = if (isOpenGroup) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
|
||||
contact?.displayName(context)
|
||||
}
|
||||
if (userDisplayName != null) {
|
||||
@ -54,10 +55,15 @@ object MentionUtilities {
|
||||
}
|
||||
}
|
||||
val result = SpannableString(text)
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||
for (mention in mentions) {
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||
val colorID = if (isLightMode && isOutgoingMessage) R.color.black else R.color.accent
|
||||
result.setSpan(ForegroundColorSpan(context.resources.getColorWithID(colorID, context.theme)), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
val colorID = if (isOutgoingMessage) {
|
||||
if (isLightMode) R.color.white else R.color.black
|
||||
} else {
|
||||
R.color.accent
|
||||
}
|
||||
val color = ResourcesCompat.getColor(context.resources, colorID, context.theme)
|
||||
result.setSpan(ForegroundColorSpan(color), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return result
|
||||
|
@ -21,9 +21,13 @@ val View.hitRect: Rect
|
||||
}
|
||||
|
||||
fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) {
|
||||
val layoutParams = this.layoutParams
|
||||
val startSize = resources.getDimension(startSizeID)
|
||||
val endSize = resources.getDimension(endSizeID)
|
||||
animateSizeChange(startSize, endSize)
|
||||
}
|
||||
|
||||
fun View.animateSizeChange(startSize: Float, endSize: Float, animationDuration: Long = 250) {
|
||||
val layoutParams = this.layoutParams
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), startSize, endSize)
|
||||
animation.duration = animationDuration
|
||||
animation.addUpdateListener { animator ->
|
||||
|
@ -1,17 +1,19 @@
|
||||
package org.thoughtcrime.securesms.loki.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Typeface
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.view_conversation.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities.highlightMentions
|
||||
@ -20,23 +22,17 @@ import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.*
|
||||
|
||||
class ConversationView : LinearLayout {
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
var thread: ThreadRecord? = null
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_conversation, this)
|
||||
layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -44,23 +40,30 @@ class ConversationView : LinearLayout {
|
||||
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
|
||||
this.thread = thread
|
||||
populateUserPublicKeyCacheIfNeeded(thread.threadId, context) // FIXME: This is a bad place to do this
|
||||
val unreadCount = thread.unreadCount
|
||||
if (thread.recipient.isBlocked) {
|
||||
accentView.setBackgroundResource(R.color.destructive)
|
||||
accentView.visibility = View.VISIBLE
|
||||
} else {
|
||||
accentView.setBackgroundResource(R.color.accent)
|
||||
accentView.visibility = if (thread.unreadCount > 0) View.VISIBLE else View.INVISIBLE
|
||||
accentView.visibility = if (unreadCount > 0) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
val formattedUnreadCount = if (unreadCount < 100) unreadCount.toString() else "99+"
|
||||
unreadCountTextView.text = formattedUnreadCount
|
||||
val textSize = if (unreadCount < 100) 12.0f else 9.0f
|
||||
unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
||||
unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL)
|
||||
unreadCountIndicator.isVisible = (unreadCount != 0)
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.update(thread.recipient, thread.threadId)
|
||||
val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString()
|
||||
btnGroupNameDisplay.text = senderDisplayName
|
||||
conversationViewDisplayNameTextView.text = senderDisplayName
|
||||
timestampTextView.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||
muteIndicatorImageView.visibility = if (thread.recipient.isMuted) VISIBLE else GONE
|
||||
val rawSnippet = thread.getDisplayBody(context)
|
||||
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
|
||||
snippetTextView.text = snippet
|
||||
snippetTextView.typeface = if (thread.unreadCount > 0) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
snippetTextView.typeface = if (unreadCount > 0) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||
if (isTyping) {
|
||||
typingIndicatorView.startAnimation()
|
||||
@ -71,9 +74,13 @@ class ConversationView : LinearLayout {
|
||||
statusIndicatorImageView.visibility = View.VISIBLE
|
||||
when {
|
||||
!thread.isOutgoing -> statusIndicatorImageView.visibility = View.GONE
|
||||
thread.isFailed -> statusIndicatorImageView.setImageResource(R.drawable.ic_error)
|
||||
thread.isFailed -> {
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_error)?.mutate()
|
||||
drawable?.setTint(ContextCompat.getColor(context,R.color.destructive))
|
||||
statusIndicatorImageView.setImageDrawable(drawable)
|
||||
}
|
||||
thread.isPending -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot)
|
||||
thread.isRemoteRead -> statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||
thread.isRead -> statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||
else -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,14 @@ import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
|
||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
interface GlowView {
|
||||
var mainColor: Int
|
||||
@ -155,4 +156,50 @@ class PathDotView : View, GlowView {
|
||||
super.onDraw(c)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
}
|
||||
|
||||
class InputBarButtonImageViewContainer : RelativeLayout, GlowView {
|
||||
@ColorInt override var mainColor: Int = 0
|
||||
set(newValue) { field = newValue; fillPaint.color = newValue }
|
||||
@ColorInt var strokeColor: Int = 0
|
||||
set(newValue) { field = newValue; strokePaint.color = newValue }
|
||||
@ColorInt override var sessionShadowColor: Int = 0 // Unused
|
||||
|
||||
private val fillPaint: Paint by lazy {
|
||||
val result = Paint()
|
||||
result.style = Paint.Style.FILL
|
||||
result.isAntiAlias = true
|
||||
result
|
||||
}
|
||||
|
||||
private val strokePaint: Paint by lazy {
|
||||
val result = Paint()
|
||||
result.style = Paint.Style.STROKE
|
||||
result.isAntiAlias = true
|
||||
result.strokeWidth = 1.0f
|
||||
result.alpha = (255 * 0.2f).roundToInt()
|
||||
result
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { }
|
||||
|
||||
init {
|
||||
setWillNotDraw(false)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
override fun onDraw(c: Canvas) {
|
||||
val w = width.toFloat()
|
||||
val h = height.toFloat()
|
||||
c.drawCircle(w / 2, h / 2, w / 2, fillPaint)
|
||||
if (strokeColor != 0) {
|
||||
c.drawCircle(w / 2, h / 2, w / 2, strokePaint)
|
||||
}
|
||||
super.onDraw(c)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr:
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
btnGroupNameDisplay.text = mentionCandidate.displayName
|
||||
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
||||
profilePictureView.publicKey = mentionCandidate.publicKey
|
||||
profilePictureView.displayName = mentionCandidate.displayName
|
||||
profilePictureView.additionalPublicKey = null
|
||||
|
@ -1,19 +1,15 @@
|
||||
package org.thoughtcrime.securesms.loki.views
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.animation.FloatEvaluator
|
||||
import android.animation.PointFEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.Context.VIBRATOR_SERVICE
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.PointF
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.VibrationEffect.DEFAULT_AMPLITUDE
|
||||
import android.os.Vibrator
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
@ -162,6 +158,7 @@ class NewConversationButtonSetView : RelativeLayout {
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
disableClipping()
|
||||
isHapticFeedbackEnabled = true
|
||||
// Set up session button
|
||||
addView(sessionButton)
|
||||
sessionButton.alpha = 0.0f
|
||||
@ -206,11 +203,10 @@ class NewConversationButtonSetView : RelativeLayout {
|
||||
isExpanded = true
|
||||
expand()
|
||||
}
|
||||
val vibrator = context.getSystemService(VIBRATOR_SERVICE) as Vibrator
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(50, DEFAULT_AMPLITUDE))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||
} else {
|
||||
vibrator.vibrate(50)
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
|
@ -31,23 +31,12 @@ class ProfilePictureView : RelativeLayout {
|
||||
private val profilePicturesCache = mutableMapOf<String, String?>()
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() }
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
private fun initialize() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_profile_picture, null)
|
||||
addView(contentView)
|
||||
|
@ -2,36 +2,20 @@ package org.thoughtcrime.securesms.longmessage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableString;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.TypedValue;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemFooter;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.Stub;
|
||||
|
||||
import java.util.Locale;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
@ -43,8 +27,7 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
private static final int MAX_DISPLAY_LENGTH = 64 * 1024;
|
||||
|
||||
private Stub<ViewGroup> sentBubble;
|
||||
private Stub<ViewGroup> receivedBubble;
|
||||
private TextView textBody;
|
||||
|
||||
private LongMessageViewModel viewModel;
|
||||
|
||||
@ -60,9 +43,7 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
setContentView(R.layout.longmessage_activity);
|
||||
|
||||
sentBubble = new Stub<>(findViewById(R.id.longmessage_sent_stub));
|
||||
receivedBubble = new Stub<>(findViewById(R.id.longmessage_received_stub));
|
||||
textBody = findViewById(R.id.longmessage_text);
|
||||
|
||||
initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), getIntent().getBooleanExtra(KEY_IS_MMS, false));
|
||||
}
|
||||
@ -93,36 +74,19 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (message.get().getMessageRecord().isOutgoing()) {
|
||||
getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_your_message));
|
||||
} else {
|
||||
Recipient recipient = message.get().getMessageRecord().getRecipient();
|
||||
String name = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize()) ;
|
||||
String name = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize());
|
||||
getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_message_from_s, name));
|
||||
}
|
||||
|
||||
ViewGroup bubble;
|
||||
String trimmedBody = getTrimmedBody(message.get().getFullBody());
|
||||
String mentionBody = MentionUtilities.highlightMentions(trimmedBody, message.get().getMessageRecord().getThreadId(), this);
|
||||
|
||||
if (message.get().getMessageRecord().isOutgoing()) {
|
||||
bubble = sentBubble.get();
|
||||
bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.message_sent_background_color), PorterDuff.Mode.MULTIPLY);
|
||||
} else {
|
||||
bubble = receivedBubble.get();
|
||||
bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.message_received_background_color), PorterDuff.Mode.MULTIPLY);
|
||||
}
|
||||
|
||||
TextView text = bubble.findViewById(R.id.longmessage_text);
|
||||
ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer);
|
||||
|
||||
String trimmedBody = getTrimmedBody(message.get().getFullBody());
|
||||
SpannableString styledBody = linkifyMessageBody(new SpannableString(trimmedBody));
|
||||
|
||||
bubble.setVisibility(View.VISIBLE);
|
||||
text.setText(styledBody);
|
||||
text.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(this));
|
||||
footer.setMessageRecord(message.get().getMessageRecord(), Locale.getDefault());
|
||||
textBody.setText(mentionBody);
|
||||
textBody.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
});
|
||||
}
|
||||
|
||||
@ -131,15 +95,4 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
|
||||
: text.substring(0, MAX_DISPLAY_LENGTH);
|
||||
}
|
||||
|
||||
private SpannableString linkifyMessageBody(SpannableString messageBody) {
|
||||
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
|
||||
boolean hasLinks = Linkify.addLinks(messageBody, linkPattern);
|
||||
|
||||
if (hasLinks) {
|
||||
Stream.of(messageBody.getSpans(0, messageBody.length(), URLSpan.class))
|
||||
.filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL()))
|
||||
.forEach(messageBody::removeSpan);
|
||||
}
|
||||
return messageBody;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import android.view.ViewGroup;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.StableIdGenerator;
|
||||
|
@ -31,7 +31,6 @@ import org.session.libsession.utilities.MediaTypes;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
|
||||
|
||||
public class AudioSlide extends Slide {
|
||||
|
||||
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) {
|
||||
|
@ -49,6 +49,7 @@ import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
@ -115,9 +116,9 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
if (visibleThread == threadId) {
|
||||
sendInThreadNotification(context, recipient);
|
||||
} else {
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
Intent intent = new Intent(context, ConversationActivityV2.class);
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress());
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
|
||||
intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
|
||||
|
||||
FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent);
|
||||
|
@ -81,7 +81,6 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
||||
|
||||
for (Address address : addressMap.keySet()) {
|
||||
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
|
||||
// Loki - Check whether we want to send a read receipt to this user
|
||||
if (!SessionMetaProtocol.shouldSendReadReceipt(address)) { continue; }
|
||||
ReadReceipt readReceipt = new ReadReceipt(timestamps);
|
||||
readReceipt.setSentTimestamp(System.currentTimeMillis());
|
||||
|
@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
@ -67,11 +68,11 @@ public class NotificationItem {
|
||||
}
|
||||
|
||||
public PendingIntent getPendingIntent(Context context) {
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
Intent intent = new Intent(context, ConversationActivityV2.class);
|
||||
Recipient notifyRecipients = threadRecipient != null ? threadRecipient : conversationRecipient;
|
||||
if (notifyRecipients != null) intent.putExtra(ConversationActivity.ADDRESS_EXTRA, notifyRecipients.getAddress());
|
||||
if (notifyRecipients != null) intent.putExtra(ConversationActivityV2.ADDRESS, notifyRecipients.getAddress());
|
||||
|
||||
intent.putExtra("thread_id", threadId);
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
|
||||
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
||||
|
||||
return TaskStackBuilder.create(context)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user