mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-19 07:08:32 +00:00
Merge branch 'dev' into security
This commit is contained in:
commit
5168e15640
@ -143,8 +143,8 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 182
|
def canonicalVersionCode = 200
|
||||||
def canonicalVersionName = "1.10.13"
|
def canonicalVersionName = "1.11.4"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
@ -194,8 +194,8 @@ android {
|
|||||||
versionCode canonicalVersionCode * postFixSize
|
versionCode canonicalVersionCode * postFixSize
|
||||||
versionName canonicalVersionName
|
versionName canonicalVersionName
|
||||||
|
|
||||||
minSdkVersion 21
|
minSdkVersion androidMinimumSdkVersion
|
||||||
targetSdkVersion 30
|
targetSdkVersion androidCompileSdkVersion
|
||||||
|
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="network.loki.messenger">
|
package="network.loki.messenger">
|
||||||
|
|
||||||
@ -7,7 +8,7 @@
|
|||||||
|
|
||||||
<permission
|
<permission
|
||||||
android:name="network.loki.messenger.ACCESS_SESSION_SECRETS"
|
android:name="network.loki.messenger.ACCESS_SESSION_SECRETS"
|
||||||
android:label="Access to TextSecure Secrets"
|
android:label="Access to Session secrets"
|
||||||
android:protectionLevel="signature" />
|
android:protectionLevel="signature" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
@ -36,32 +37,32 @@
|
|||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_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="android.permission.INSTALL_SHORTCUT" />
|
||||||
<uses-permission android:name="com.android.launcher.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.BROADCAST_STICKY" />
|
||||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
||||||
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
|
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<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.BLUETOOTH" tools:node="remove"/>
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" 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"/>
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/>
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<!-- The allowBackup="false" below is important to guard against potential malicious backups -->
|
<!-- The allowBackup="false" below is important to guard against potential malicious backups -->
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="org.thoughtcrime.securesms.ApplicationContext"
|
android:name="org.thoughtcrime.securesms.ApplicationContext"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
@ -73,7 +74,8 @@
|
|||||||
android:theme="@style/Theme.Session.DayNight"
|
android:theme="@style/Theme.Session.DayNight"
|
||||||
tools:replace="android:allowBackup">
|
tools:replace="android:allowBackup">
|
||||||
|
|
||||||
<!-- Disable analytics -->
|
<!-- Disable all analytics -->
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="firebase_analytics_collection_deactivated"
|
android:name="firebase_analytics_collection_deactivated"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
@ -90,88 +92,83 @@
|
|||||||
android:name="firebase_messaging_auto_init_enabled"
|
android:name="firebase_messaging_auto_init_enabled"
|
||||||
android:value="false" />
|
android:value="false" />
|
||||||
|
|
||||||
<!-- Session -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.LandingActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.LandingActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"/>
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.RegisterActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.RegisterActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"/>
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.RecoveryPhraseRestoreActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.RecoveryPhraseRestoreActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.BackupRestoreActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.LinkDeviceActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.LinkDeviceActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.DisplayNameActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.DisplayNameActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.PNModeActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"/>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.PNModeActivity"
|
android:name="org.thoughtcrime.securesms.home.HomeActivity"
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.HomeActivity"
|
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.SettingsActivity"
|
android:name="org.thoughtcrime.securesms.preferences.SettingsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:label="@string/activity_settings_title"/>
|
android:label="@string/activity_settings_title" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.PathActivity"
|
android:name="org.thoughtcrime.securesms.home.PathActivity"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.QRCodeActivity"
|
android:name="org.thoughtcrime.securesms.preferences.QRCodeActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"/>
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.CreatePrivateChatActivity"
|
android:name="org.thoughtcrime.securesms.dms.CreatePrivateChatActivity"
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"/>
|
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.CreateClosedGroupActivity"
|
|
||||||
android:screenOrientation="portrait" />
|
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity"
|
|
||||||
android:label="@string/activity_edit_closed_group_title"
|
|
||||||
android:screenOrientation="portrait" />
|
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.JoinPublicChatActivity"
|
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.SeedActivity"
|
android:name="org.thoughtcrime.securesms.groups.CreateClosedGroupActivity"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.SelectContactsActivity"
|
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
|
||||||
|
android:label="@string/activity_edit_closed_group_title"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.PrivacySettingsActivity"
|
android:name="org.thoughtcrime.securesms.groups.JoinPublicChatActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name="org.thoughtcrime.securesms.onboarding.SeedActivity"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name="org.thoughtcrime.securesms.contacts.SelectContactsActivity"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name="org.thoughtcrime.securesms.preferences.PrivacySettingsActivity"
|
||||||
android:label="@string/activity_privacy_settings_title"
|
android:label="@string/activity_privacy_settings_title"
|
||||||
android:screenOrientation="portrait"/>
|
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.NotificationSettingsActivity"
|
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.ChatSettingsActivity"
|
android:name="org.thoughtcrime.securesms.preferences.NotificationSettingsActivity"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<!-- Session -->
|
<activity
|
||||||
|
android:name="org.thoughtcrime.securesms.preferences.ChatSettingsActivity"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.ShareActivity"
|
android:name="org.thoughtcrime.securesms.ShareActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
@ -184,9 +181,7 @@
|
|||||||
android:windowSoftInputMode="stateHidden">
|
android:windowSoftInputMode="stateHidden">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="audio/*" />
|
<data android:mimeType="audio/*" />
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
@ -195,7 +190,6 @@
|
|||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
<data android:mimeType="*/*" />
|
<data android:mimeType="*/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.chooser.chooser_target_service"
|
android:name="android.service.chooser.chooser_target_service"
|
||||||
android:value=".service.DirectShareService" />
|
android:value=".service.DirectShareService" />
|
||||||
@ -204,14 +198,12 @@
|
|||||||
<activity-alias
|
<activity-alias
|
||||||
android:name=".RoutingActivity"
|
android:name=".RoutingActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:targetActivity="org.thoughtcrime.securesms.loki.activities.HomeActivity">
|
android:targetActivity="org.thoughtcrime.securesms.home.HomeActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.sec.minimode.icon.portrait.normal"
|
android:name="com.sec.minimode.icon.portrait.normal"
|
||||||
android:resource="@mipmap/ic_launcher" />
|
android:resource="@mipmap/ic_launcher" />
|
||||||
@ -221,40 +213,22 @@
|
|||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.conversation.ConversationActivity"
|
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.TextSecure.DayNight.NoActionBar"
|
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
|
||||||
android:parentActivityName="org.thoughtcrime.securesms.loki.activities.HomeActivity"
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar">
|
||||||
android:windowSoftInputMode="stateUnchanged">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" />
|
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity"
|
android:name="org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.TextSecure.DayNight"/>
|
android:theme="@style/Theme.TextSecure.DayNight" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
|
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
|
||||||
android:screenOrientation="portrait"
|
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"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:taskAffinity=""
|
|
||||||
android:windowSoftInputMode="stateVisible" />
|
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.MessageDetailsActivity"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
|
||||||
android:label="Message Details"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.TextSecure.DayNight"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:windowSoftInputMode="stateHidden" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.DatabaseUpgradeActivity"
|
android:name="org.thoughtcrime.securesms.DatabaseUpgradeActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
@ -264,7 +238,7 @@
|
|||||||
android:name="org.thoughtcrime.securesms.PassphrasePromptActivity"
|
android:name="org.thoughtcrime.securesms.PassphrasePromptActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/Theme.Session.DayNight.NoActionBar"/>
|
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.giph.ui.GiphyActivity"
|
android:name="org.thoughtcrime.securesms.giph.ui.GiphyActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
@ -306,7 +280,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.scribbles.StickerSelectActivity"
|
android:name="org.thoughtcrime.securesms.scribbles.StickerSelectActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:theme="@style/Theme.Session.ForceDark"/>
|
android:theme="@style/Theme.Session.ForceDark" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
@ -317,7 +291,7 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
||||||
<service
|
<service
|
||||||
android:name="org.thoughtcrime.securesms.loki.api.PushNotificationService"
|
android:name="org.thoughtcrime.securesms.notifications.PushNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@ -436,15 +410,13 @@
|
|||||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<!-- Session -->
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="org.thoughtcrime.securesms.loki.api.BackgroundPollWorker$BootBroadcastReceiver"
|
android:name="org.thoughtcrime.securesms.notifications.BackgroundPollWorker$BootBroadcastReceiver"
|
||||||
android:enabled="true">
|
android:enabled="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<!-- Session -->
|
|
||||||
<service
|
<service
|
||||||
android:name="org.thoughtcrime.securesms.jobmanager.JobSchedulerScheduler$SystemService"
|
android:name="org.thoughtcrime.securesms.jobmanager.JobSchedulerScheduler$SystemService"
|
||||||
android:enabled="@bool/enable_job_service"
|
android:enabled="@bool/enable_job_service"
|
||||||
@ -456,11 +428,9 @@
|
|||||||
<receiver
|
<receiver
|
||||||
android:name="org.thoughtcrime.securesms.jobmanager.AlarmManagerScheduler$RetryReceiver"
|
android:name="org.thoughtcrime.securesms.jobmanager.AlarmManagerScheduler$RetryReceiver"
|
||||||
android:enabled="@bool/enable_alarm_manager" /> <!-- Probably don't need this one -->
|
android:enabled="@bool/enable_alarm_manager" /> <!-- Probably don't need this one -->
|
||||||
|
|
||||||
<uses-library
|
<uses-library
|
||||||
android:name="com.sec.android.app.multiwindow"
|
android:name="com.sec.android.app.multiwindow"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.sec.android.support.multiwindow"
|
android:name="com.sec.android.support.multiwindow"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
@ -27,10 +27,10 @@ import androidx.lifecycle.DefaultLifecycleObserver;
|
|||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||||
import androidx.multidex.MultiDexApplication;
|
import androidx.multidex.MultiDexApplication;
|
||||||
|
|
||||||
import org.conscrypt.Conscrypt;
|
import org.conscrypt.Conscrypt;
|
||||||
import org.session.libsession.avatars.AvatarHelper;
|
import org.session.libsession.avatars.AvatarHelper;
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
||||||
import org.session.libsession.messaging.contacts.Contact;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2;
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2;
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||||
@ -42,11 +42,11 @@ import org.session.libsession.utilities.TextSecurePreferences;
|
|||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.session.libsignal.utilities.ThreadUtils;
|
import org.session.libsignal.utilities.ThreadUtils;
|
||||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||||
|
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
|
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
|
||||||
@ -58,17 +58,14 @@ import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
|||||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||||
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
||||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker;
|
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
|
||||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
|
import org.thoughtcrime.securesms.notifications.LokiPushNotificationManager;
|
||||||
import org.thoughtcrime.securesms.loki.api.OpenGroupManager;
|
import org.thoughtcrime.securesms.groups.OpenGroupManager;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
import org.thoughtcrime.securesms.util.Broadcaster;
|
||||||
import org.thoughtcrime.securesms.loki.database.SessionContactDatabase;
|
import org.thoughtcrime.securesms.notifications.FcmUtils;
|
||||||
import org.thoughtcrime.securesms.loki.utilities.Broadcaster;
|
import org.thoughtcrime.securesms.util.UiModeUtilities;
|
||||||
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities;
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.FcmUtils;
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
|
|
||||||
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
|
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
|
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
|
||||||
@ -84,12 +81,14 @@ import org.webrtc.PeerConnectionFactory;
|
|||||||
import org.webrtc.PeerConnectionFactory.InitializationOptions;
|
import org.webrtc.PeerConnectionFactory.InitializationOptions;
|
||||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import dagger.ObjectGraph;
|
import dagger.ObjectGraph;
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import kotlinx.coroutines.Job;
|
import kotlinx.coroutines.Job;
|
||||||
@ -154,8 +153,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
conversationListNotificationHandler = new Handler(Looper.getMainLooper());
|
conversationListNotificationHandler = new Handler(Looper.getMainLooper());
|
||||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||||
MessagingModuleConfiguration.Companion.configure(this,
|
MessagingModuleConfiguration.Companion.configure(this,
|
||||||
DatabaseFactory.getStorage(this),
|
DatabaseFactory.getStorage(this),
|
||||||
DatabaseFactory.getAttachmentProvider(this));
|
DatabaseFactory.getAttachmentProvider(this),
|
||||||
|
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this)
|
||||||
|
);
|
||||||
SnodeModule.Companion.configure(apiDB, broadcaster);
|
SnodeModule.Companion.configure(apiDB, broadcaster);
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||||
if (userPublicKey != null) {
|
if (userPublicKey != null) {
|
||||||
@ -181,27 +182,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
Log.i(TAG, "App is now visible.");
|
Log.i(TAG, "App is now visible.");
|
||||||
KeyCachingService.onAppForegrounded(this);
|
KeyCachingService.onAppForegrounded(this);
|
||||||
|
|
||||||
boolean hasPerformedContactMigration = TextSecurePreferences.INSTANCE.hasPerformedContactMigration(this);
|
|
||||||
if (!hasPerformedContactMigration) {
|
|
||||||
TextSecurePreferences.INSTANCE.setPerformedContactMigration(this);
|
|
||||||
Set<Recipient> allContacts = ContactUtilities.getAllContacts(this);
|
|
||||||
SessionContactDatabase contactDB = DatabaseFactory.getSessionContactDatabase(this);
|
|
||||||
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
|
||||||
for (Recipient recipient : allContacts) {
|
|
||||||
if (recipient.isGroupRecipient()) { continue; }
|
|
||||||
String sessionID = recipient.getAddress().serialize();
|
|
||||||
Contact contact = contactDB.getContactWithSessionID(sessionID);
|
|
||||||
if (contact == null) {
|
|
||||||
contact = new Contact(sessionID);
|
|
||||||
String name = userDB.getDisplayName(sessionID);
|
|
||||||
contact.setName(name);
|
|
||||||
contact.setProfilePictureURL(recipient.getProfileAvatar());
|
|
||||||
contact.setProfilePictureEncryptionKey(recipient.getProfileKey());
|
|
||||||
contact.setTrusted(true);
|
|
||||||
}
|
|
||||||
contactDB.setContact(contact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (poller != null) {
|
if (poller != null) {
|
||||||
poller.setCaughtUp(false);
|
poller.setCaughtUp(false);
|
||||||
}
|
}
|
||||||
@ -487,7 +467,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
|
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
|
||||||
TextSecurePreferences.clearAll(this);
|
TextSecurePreferences.clearAll(this);
|
||||||
if (isMigratingToV2KeyPair) {
|
if (isMigratingToV2KeyPair) {
|
||||||
TextSecurePreferences.setIsMigratingKeyPair(this, true);
|
|
||||||
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
|
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
|
||||||
TextSecurePreferences.setProfileName(this, displayName);
|
TextSecurePreferences.setProfileName(this, displayName);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ import android.widget.TextView;
|
|||||||
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
|
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.MediaDatabase.MediaRecord;
|
||||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
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.components.MediaView;
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
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.MediaPreviewViewModel;
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
@ -116,6 +118,22 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
|
|
||||||
private int restartItem = -1;
|
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")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Override
|
@Override
|
||||||
@ -171,7 +189,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
CharSequence relativeTimeSpan;
|
CharSequence relativeTimeSpan;
|
||||||
|
|
||||||
if (mediaItem.date > 0) {
|
if (mediaItem.date > 0) {
|
||||||
relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), mediaItem.date);
|
relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
|
||||||
} else {
|
} else {
|
||||||
relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
|
relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
|
||||||
}
|
}
|
||||||
|
@ -1,482 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Open Whisper Systems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import org.session.libsession.messaging.messages.visible.LinkPreview;
|
|
||||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
|
|
||||||
import org.session.libsession.messaging.messages.visible.Quote;
|
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
|
||||||
import org.session.libsession.messaging.utilities.UpdateMessageData;
|
|
||||||
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
|
|
||||||
import org.session.libsession.utilities.MaterialColor;
|
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
|
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.session.libsession.utilities.recipients.RecipientModifiedListener;
|
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
|
||||||
import org.session.libsession.utilities.ExpirationUtil;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.sql.Date;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Jake McGinty
|
|
||||||
*/
|
|
||||||
public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks<Cursor>, RecipientModifiedListener {
|
|
||||||
private final static String TAG = MessageDetailsActivity.class.getSimpleName();
|
|
||||||
|
|
||||||
public final static String MESSAGE_ID_EXTRA = "message_id";
|
|
||||||
public final static String THREAD_ID_EXTRA = "thread_id";
|
|
||||||
public final static String IS_PUSH_GROUP_EXTRA = "is_push_group";
|
|
||||||
public final static String TYPE_EXTRA = "type";
|
|
||||||
public final static String ADDRESS_EXTRA = "address";
|
|
||||||
|
|
||||||
private GlideRequests glideRequests;
|
|
||||||
private long threadId;
|
|
||||||
private boolean isPushGroup;
|
|
||||||
private ConversationItem conversationItem;
|
|
||||||
private ViewGroup itemParent;
|
|
||||||
private View metadataContainer;
|
|
||||||
private View expiresContainer;
|
|
||||||
private TextView errorText;
|
|
||||||
private View resendButton;
|
|
||||||
private TextView sentDate;
|
|
||||||
private TextView receivedDate;
|
|
||||||
private TextView expiresInText;
|
|
||||||
private View receivedContainer;
|
|
||||||
private TextView transport;
|
|
||||||
private TextView toFrom;
|
|
||||||
private View separator;
|
|
||||||
private ListView recipientsList;
|
|
||||||
private LayoutInflater inflater;
|
|
||||||
|
|
||||||
private boolean running;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle bundle, boolean ready) {
|
|
||||||
super.onCreate(bundle, ready);
|
|
||||||
setContentView(R.layout.message_details_activity);
|
|
||||||
running = true;
|
|
||||||
|
|
||||||
initializeResources();
|
|
||||||
initializeActionBar();
|
|
||||||
getSupportLoaderManager().initLoader(0, null, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
assert getSupportActionBar() != null;
|
|
||||||
getSupportActionBar().setTitle("Message Details");
|
|
||||||
|
|
||||||
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(-1L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeActionBar() {
|
|
||||||
assert getSupportActionBar() != null;
|
|
||||||
|
|
||||||
Recipient recipient = Recipient.from(this, getIntent().getParcelableExtra(ADDRESS_EXTRA), true);
|
|
||||||
recipient.addListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setActionBarColor(MaterialColor color) {
|
|
||||||
assert getSupportActionBar() != null;
|
|
||||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onModified(final Recipient recipient) {
|
|
||||||
Util.runOnMain(() -> setActionBarColor(recipient.getColor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeResources() {
|
|
||||||
inflater = LayoutInflater.from(this);
|
|
||||||
View header = inflater.inflate(R.layout.message_details_header, recipientsList, false);
|
|
||||||
|
|
||||||
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
|
|
||||||
isPushGroup = getIntent().getBooleanExtra(IS_PUSH_GROUP_EXTRA, false);
|
|
||||||
glideRequests = GlideApp.with(this);
|
|
||||||
itemParent = header.findViewById(R.id.item_container);
|
|
||||||
recipientsList = findViewById(R.id.recipients_list);
|
|
||||||
metadataContainer = header.findViewById(R.id.metadata_container);
|
|
||||||
errorText = header.findViewById(R.id.error_text);
|
|
||||||
resendButton = header.findViewById(R.id.resend_button);
|
|
||||||
sentDate = header.findViewById(R.id.sent_time);
|
|
||||||
receivedContainer = header.findViewById(R.id.received_container);
|
|
||||||
receivedDate = header.findViewById(R.id.received_time);
|
|
||||||
transport = header.findViewById(R.id.transport);
|
|
||||||
toFrom = header.findViewById(R.id.tofrom);
|
|
||||||
separator = header.findViewById(R.id.separator);
|
|
||||||
expiresContainer = header.findViewById(R.id.expires_container);
|
|
||||||
expiresInText = header.findViewById(R.id.expires_in);
|
|
||||||
recipientsList.setHeaderDividersEnabled(false);
|
|
||||||
recipientsList.addHeaderView(header, null, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateTransport(MessageRecord messageRecord) {
|
|
||||||
final String transportText;
|
|
||||||
if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
|
|
||||||
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 {
|
|
||||||
transportText = getString(R.string.ConversationFragment_sms);
|
|
||||||
}
|
|
||||||
|
|
||||||
transport.setText(transportText);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateTime(MessageRecord messageRecord) {
|
|
||||||
sentDate.setOnLongClickListener(null);
|
|
||||||
receivedDate.setOnLongClickListener(null);
|
|
||||||
|
|
||||||
if (messageRecord.isPending() || messageRecord.isFailed()) {
|
|
||||||
sentDate.setText("-");
|
|
||||||
receivedContainer.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
Locale dateLocale = Locale.getDefault();
|
|
||||||
SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(this, dateLocale);
|
|
||||||
sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent())));
|
|
||||||
sentDate.setOnLongClickListener(v -> {
|
|
||||||
copyToClipboard(String.valueOf(messageRecord.getDateSent()));
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) {
|
|
||||||
receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived())));
|
|
||||||
receivedDate.setOnLongClickListener(v -> {
|
|
||||||
copyToClipboard(String.valueOf(messageRecord.getDateReceived()));
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
receivedContainer.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
receivedContainer.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateExpirationTime(final MessageRecord messageRecord) {
|
|
||||||
if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) {
|
|
||||||
expiresContainer.setVisibility(View.GONE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
expiresContainer.setVisibility(View.VISIBLE);
|
|
||||||
Util.runOnMain(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
long elapsed = System.currentTimeMillis() - messageRecord.getExpireStarted();
|
|
||||||
long remaining = messageRecord.getExpiresIn() - elapsed;
|
|
||||||
|
|
||||||
String duration = ExpirationUtil.getExpirationDisplayValue(MessageDetailsActivity.this, Math.max((int)(remaining / 1000), 1));
|
|
||||||
expiresInText.setText(duration);
|
|
||||||
|
|
||||||
if (running) {
|
|
||||||
Util.runOnMainDelayed(this, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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()) {
|
|
||||||
toFromRes = R.string.message_details_header__to;
|
|
||||||
} else {
|
|
||||||
toFromRes = R.string.message_details_header__from;
|
|
||||||
}
|
|
||||||
toFrom.setText(toFromRes);
|
|
||||||
long threadID = messageRecord.getThreadId();
|
|
||||||
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID);
|
|
||||||
if (openGroup != null && messageRecord.isOutgoing()) {
|
|
||||||
toFrom.setVisibility(View.GONE);
|
|
||||||
separator.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), recipient, null, false);
|
|
||||||
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
|
|
||||||
}
|
|
||||||
|
|
||||||
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()) {
|
|
||||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent, itemParent, false);
|
|
||||||
} else {
|
|
||||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received, itemParent, false);
|
|
||||||
}
|
|
||||||
itemParent.addView(conversationItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private @Nullable MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
|
|
||||||
switch (type) {
|
|
||||||
case MmsSmsDatabase.SMS_TRANSPORT:
|
|
||||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
||||||
SmsDatabase.Reader reader = smsDatabase.readerFor(cursor);
|
|
||||||
return reader.getNext();
|
|
||||||
case MmsSmsDatabase.MMS_TRANSPORT:
|
|
||||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
|
||||||
MmsDatabase.Reader mmsReader = mmsDatabase.readerFor(cursor);
|
|
||||||
return mmsReader.getNext();
|
|
||||||
default:
|
|
||||||
throw new AssertionError("no valid message type specified");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void copyToClipboard(@NonNull String text) {
|
|
||||||
((ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
|
||||||
return new MessageDetailsLoader(this, getIntent().getStringExtra(TYPE_EXTRA),
|
|
||||||
getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
|
||||||
MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
|
|
||||||
|
|
||||||
if (messageRecord == null) {
|
|
||||||
finish();
|
|
||||||
} else {
|
|
||||||
new MessageRecipientAsyncTask(this, messageRecord).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
|
||||||
recipientsList.setAdapter(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
super.onOptionsItemSelected(item);
|
|
||||||
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case android.R.id.home: finish(); return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,List<RecipientDeliveryStatus>> {
|
|
||||||
|
|
||||||
private final WeakReference<Context> weakContext;
|
|
||||||
private final MessageRecord messageRecord;
|
|
||||||
|
|
||||||
MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
|
||||||
this.weakContext = new WeakReference<>(context);
|
|
||||||
this.messageRecord = messageRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Context getContext() {
|
|
||||||
return weakContext.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<RecipientDeliveryStatus> doInBackground(Void... voids) {
|
|
||||||
Context context = getContext();
|
|
||||||
|
|
||||||
if (context == null) {
|
|
||||||
Log.w(TAG, "associated context is destroyed, finishing early");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
} else {
|
|
||||||
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
|
|
||||||
|
|
||||||
if (receiptInfoList.isEmpty()) {
|
|
||||||
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().getAddress().toGroupString(), false);
|
|
||||||
|
|
||||||
for (Recipient recipient : group) {
|
|
||||||
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (GroupReceiptInfo info : receiptInfoList) {
|
|
||||||
recipients.add(new RecipientDeliveryStatus(Recipient.from(context, info.getAddress(), true),
|
|
||||||
getStatusFor(info.getStatus(), messageRecord.isPending(), messageRecord.isFailed()),
|
|
||||||
info.isUnidentified(),
|
|
||||||
info.getTimestamp()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return recipients;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPostExecute(List<RecipientDeliveryStatus> recipients) {
|
|
||||||
if (getContext() == null) {
|
|
||||||
Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inflateMessageViewIfAbsent(messageRecord);
|
|
||||||
updateRecipients(messageRecord, messageRecord.getRecipient(), recipients);
|
|
||||||
|
|
||||||
boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty();
|
|
||||||
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty();
|
|
||||||
|
|
||||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(getContext());
|
|
||||||
String errorMessage = lokiMessageDatabase.getErrorMessage(messageRecord.id);
|
|
||||||
if (errorMessage != null) {
|
|
||||||
errorText.setText(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGroupNetworkFailure || isIndividualNetworkFailure) {
|
|
||||||
errorText.setVisibility(View.VISIBLE);
|
|
||||||
resendButton.setVisibility(View.VISIBLE);
|
|
||||||
resendButton.setOnClickListener(this::onResendClicked);
|
|
||||||
metadataContainer.setVisibility(View.GONE);
|
|
||||||
} else if (messageRecord.isFailed()) {
|
|
||||||
errorText.setVisibility(View.VISIBLE);
|
|
||||||
resendButton.setVisibility(View.GONE);
|
|
||||||
resendButton.setOnClickListener(null);
|
|
||||||
metadataContainer.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
updateTransport(messageRecord);
|
|
||||||
updateTime(messageRecord);
|
|
||||||
updateExpirationTime(messageRecord);
|
|
||||||
errorText.setVisibility(View.GONE);
|
|
||||||
resendButton.setVisibility(View.GONE);
|
|
||||||
resendButton.setOnClickListener(null);
|
|
||||||
metadataContainer.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private RecipientDeliveryStatus.Status getStatusFor(int deliveryReceiptCount, int readReceiptCount, boolean pending) {
|
|
||||||
if (readReceiptCount > 0) return RecipientDeliveryStatus.Status.READ;
|
|
||||||
else if (deliveryReceiptCount > 0) return RecipientDeliveryStatus.Status.DELIVERED;
|
|
||||||
else if (!pending) return RecipientDeliveryStatus.Status.SENT;
|
|
||||||
else return RecipientDeliveryStatus.Status.PENDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RecipientDeliveryStatus.Status getStatusFor(int groupStatus, boolean pending, boolean failed) {
|
|
||||||
if (groupStatus == GroupReceiptDatabase.STATUS_READ) return RecipientDeliveryStatus.Status.READ;
|
|
||||||
else if (groupStatus == GroupReceiptDatabase.STATUS_DELIVERED) return RecipientDeliveryStatus.Status.DELIVERED;
|
|
||||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && failed) return RecipientDeliveryStatus.Status.UNKNOWN;
|
|
||||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && !pending) return RecipientDeliveryStatus.Status.SENT;
|
|
||||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED) return RecipientDeliveryStatus.Status.PENDING;
|
|
||||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNKNOWN) return RecipientDeliveryStatus.Status.UNKNOWN;
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onResendClicked(View v) {
|
|
||||||
Recipient recipient = messageRecord.getRecipient();
|
|
||||||
VisibleMessage message = new VisibleMessage();
|
|
||||||
message.setId(messageRecord.getId());
|
|
||||||
if (messageRecord.isOpenGroupInvitation()) {
|
|
||||||
OpenGroupInvitation openGroupInvitation = new OpenGroupInvitation();
|
|
||||||
UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(messageRecord.getBody());
|
|
||||||
if (updateMessageData.getKind() instanceof UpdateMessageData.Kind.OpenGroupInvitation) {
|
|
||||||
UpdateMessageData.Kind.OpenGroupInvitation data = (UpdateMessageData.Kind.OpenGroupInvitation)updateMessageData.getKind();
|
|
||||||
openGroupInvitation.setName(data.getGroupName());
|
|
||||||
openGroupInvitation.setUrl(data.getGroupUrl());
|
|
||||||
}
|
|
||||||
message.setOpenGroupInvitation(openGroupInvitation);
|
|
||||||
} else {
|
|
||||||
message.setText(messageRecord.getBody());
|
|
||||||
}
|
|
||||||
message.setSentTimestamp(messageRecord.getTimestamp());
|
|
||||||
if (recipient.isGroupRecipient()) {
|
|
||||||
message.setGroupPublicKey(recipient.getAddress().toGroupString());
|
|
||||||
} else {
|
|
||||||
message.setRecipient(messageRecord.getRecipient().getAddress().serialize());
|
|
||||||
}
|
|
||||||
message.setThreadID(messageRecord.getThreadId());
|
|
||||||
if (messageRecord.isMms()) {
|
|
||||||
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
|
|
||||||
if (!mmsMessageRecord.getLinkPreviews().isEmpty()) {
|
|
||||||
message.setLinkPreview(LinkPreview.Companion.from(mmsMessageRecord.getLinkPreviews().get(0)));
|
|
||||||
}
|
|
||||||
if (mmsMessageRecord.getQuote() != null) {
|
|
||||||
message.setQuote(Quote.Companion.from(mmsMessageRecord.getQuote().getQuoteModel()));
|
|
||||||
}
|
|
||||||
message.addSignalAttachments(mmsMessageRecord.getSlideDeck().asAttachments());
|
|
||||||
}
|
|
||||||
MessageSender.send(message, recipient.getAddress());
|
|
||||||
resendButton.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,7 +10,7 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.loki.views.UserView;
|
import org.thoughtcrime.securesms.contacts.UserView;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsession.utilities.Conversions;
|
import org.session.libsession.utilities.Conversions;
|
||||||
|
@ -39,7 +39,7 @@ import android.widget.ImageView;
|
|||||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||||
import androidx.core.os.CancellationSignal;
|
import androidx.core.os.CancellationSignal;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
|
||||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
|
@ -12,8 +12,8 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.loki.activities.LandingActivity;
|
import org.thoughtcrime.securesms.onboarding.LandingActivity;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
|
||||||
|
@ -37,13 +37,13 @@ import androidx.appcompat.widget.Toolbar;
|
|||||||
|
|
||||||
import org.session.libsession.utilities.DistributionTypes;
|
import org.session.libsession.utilities.DistributionTypes;
|
||||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.loki.fragments.ContactSelectionListFragment;
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListFragment;
|
||||||
import org.thoughtcrime.securesms.loki.fragments.ContactSelectionListLoader.DisplayMode;
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListLoader.DisplayMode;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
@ -53,7 +53,6 @@ import org.session.libsession.utilities.ViewUtil;
|
|||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
@ -215,10 +214,9 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createConversation(long threadId, Address address, int distributionType) {
|
private void createConversation(long threadId, Address address, int distributionType) {
|
||||||
final Intent intent = getBaseShareIntent(ConversationActivity.class);
|
final Intent intent = getBaseShareIntent(ConversationActivityV2.class);
|
||||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, address);
|
intent.putExtra(ConversationActivityV2.ADDRESS, address);
|
||||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
|
||||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
|
|
||||||
|
|
||||||
isPassingAlongMedia = true;
|
isPassingAlongMedia = true;
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
@ -226,11 +224,6 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
private Intent getBaseShareIntent(final @NonNull Class<?> target) {
|
private Intent getBaseShareIntent(final @NonNull Class<?> target) {
|
||||||
final Intent intent = new Intent(this, target);
|
final Intent intent = new Intent(this, target);
|
||||||
final String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
|
||||||
final ArrayList<Media> mediaExtra = getIntent().getParcelableArrayListExtra(ConversationActivity.MEDIA_EXTRA);
|
|
||||||
|
|
||||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, textExtra);
|
|
||||||
intent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaExtra);
|
|
||||||
|
|
||||||
if (resolvedExtra != null) intent.setDataAndType(resolvedExtra, mimeType);
|
if (resolvedExtra != null) intent.setDataAndType(resolvedExtra, mimeType);
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
|
|
||||||
|
@ -9,12 +9,13 @@ import org.session.libsession.messaging.sending_receiving.attachments.*
|
|||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.UploadResult
|
import org.session.libsession.utilities.UploadResult
|
||||||
import org.session.libsession.utilities.Util
|
import org.session.libsession.utilities.Util
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.messages.SignalServiceAttachment
|
import org.session.libsignal.messages.SignalServiceAttachment
|
||||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||||
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||||
import org.thoughtcrime.securesms.database.Database
|
import org.thoughtcrime.securesms.database.Database
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
@ -60,9 +61,9 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
return databaseAttachment.toSignalAttachmentPointer()
|
return databaseAttachment.toSignalAttachmentPointer()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long) {
|
override fun setAttachmentState(attachmentState: AttachmentState, attachmentId: AttachmentId, messageID: Long) {
|
||||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||||
attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value)
|
attachmentDatabase.setTransferState(messageID, attachmentId, attachmentState.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>? {
|
override fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>? {
|
||||||
@ -92,11 +93,39 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
return message.linkPreviews.firstOrNull()?.attachmentId?.rowId
|
return message.linkPreviews.firstOrNull()?.attachmentId?.rowId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getIndividualRecipientForMms(mmsId: Long): Recipient? {
|
||||||
|
val mmsDb = DatabaseFactory.getMmsDatabase(context)
|
||||||
|
val message = mmsDb.getMessage(mmsId).use {
|
||||||
|
mmsDb.readerFor(it).next
|
||||||
|
}
|
||||||
|
return message?.individualRecipient
|
||||||
|
}
|
||||||
|
|
||||||
override fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream: InputStream) {
|
override fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream: InputStream) {
|
||||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||||
attachmentDatabase.insertAttachmentsForPlaceholder(messageId, attachmentId, stream)
|
attachmentDatabase.insertAttachmentsForPlaceholder(messageId, attachmentId, stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateAudioAttachmentDuration(
|
||||||
|
attachmentId: AttachmentId,
|
||||||
|
durationMs: Long,
|
||||||
|
threadId: Long
|
||||||
|
) {
|
||||||
|
val attachmentDb = DatabaseFactory.getAttachmentDatabase(context)
|
||||||
|
attachmentDb.setAttachmentAudioExtras(DatabaseAttachmentAudioExtras(
|
||||||
|
attachmentId = attachmentId,
|
||||||
|
visualSamples = byteArrayOf(),
|
||||||
|
durationMs = durationMs
|
||||||
|
), threadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isMmsOutgoing(mmsMessageId: Long): Boolean {
|
||||||
|
val mmsDb = DatabaseFactory.getMmsDatabase(context)
|
||||||
|
return mmsDb.getMessage(mmsMessageId).use { cursor ->
|
||||||
|
mmsDb.readerFor(cursor).next
|
||||||
|
}.isOutgoing
|
||||||
|
}
|
||||||
|
|
||||||
override fun isOutgoingMessage(timestamp: Long): Boolean {
|
override fun isOutgoingMessage(timestamp: Long): Boolean {
|
||||||
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
||||||
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
|
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
|
||||||
|
@ -23,6 +23,7 @@ import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.LoadControl;
|
import com.google.android.exoplayer2.LoadControl;
|
||||||
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
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 {
|
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.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
|
||||||
this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment());
|
this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment());
|
||||||
this.startTime = System.currentTimeMillis();
|
this.startTime = System.currentTimeMillis();
|
||||||
@ -184,8 +185,6 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
public void onPlayerError(ExoPlaybackException error) {
|
public void onPlayerError(ExoPlaybackException error) {
|
||||||
Log.w(TAG, "MediaPlayer Error: " + error);
|
Log.w(TAG, "MediaPlayer Error: " + error);
|
||||||
|
|
||||||
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
synchronized (AudioSlidePlayer.this) {
|
synchronized (AudioSlidePlayer.this) {
|
||||||
mediaPlayer = null;
|
mediaPlayer = null;
|
||||||
|
|
||||||
@ -267,8 +266,17 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
return slide;
|
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) {
|
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
|
||||||
return new Pair<>(0D, 0);
|
return new Pair<>(0D, 0);
|
||||||
} else {
|
} 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() {
|
private void notifyOnStart() {
|
||||||
Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this));
|
Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this));
|
||||||
}
|
}
|
||||||
@ -383,7 +401,7 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Pair<Double, Integer> progress = player.getProgress();
|
Pair<Double, Integer> progress = player.getProgressTuple();
|
||||||
player.notifyOnProgress(progress.first, progress.second);
|
player.notifyOnProgress(progress.first, progress.second);
|
||||||
sendEmptyMessageDelayed(0, 50);
|
sendEmptyMessageDelayed(0, 50);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.activities
|
package org.thoughtcrime.securesms.backup;
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
@ -9,17 +9,13 @@ import android.os.Bundle
|
|||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.text.style.ClickableSpan
|
import android.text.style.ClickableSpan
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.widget.addTextChangedListener
|
|
||||||
import androidx.databinding.DataBindingUtil
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@ -28,18 +24,17 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.backup.FullBackupImporter
|
|
||||||
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
|
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
import org.thoughtcrime.securesms.loki.utilities.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil
|
import org.thoughtcrime.securesms.util.BackupUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.home.HomeActivity
|
||||||
|
|
||||||
class BackupRestoreActivity : BaseActionBarActivity() {
|
class BackupRestoreActivity : BaseActionBarActivity() {
|
||||||
|
|
||||||
@ -188,7 +183,6 @@ class BackupRestoreViewModel(application: Application): AndroidViewModel(applica
|
|||||||
TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
|
TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
|
||||||
TextSecurePreferences.setHasViewedSeed(context, true)
|
TextSecurePreferences.setHasViewedSeed(context, true)
|
||||||
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
|
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
|
||||||
val application = ApplicationContext.getInstance(context)
|
|
||||||
|
|
||||||
BackupRestoreResult.SUCCESS
|
BackupRestoreResult.SUCCESS
|
||||||
} catch (e: DatabaseDowngradeException) {
|
} catch (e: DatabaseDowngradeException) {
|
@ -21,8 +21,7 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
|
|||||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
||||||
import org.thoughtcrime.securesms.database.*
|
import org.thoughtcrime.securesms.database.*
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase
|
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil
|
import org.thoughtcrime.securesms.util.BackupUtil
|
||||||
import org.session.libsession.utilities.Util
|
import org.session.libsession.utilities.Util
|
||||||
import org.session.libsignal.crypto.kdf.HKDFv3
|
import org.session.libsignal.crypto.kdf.HKDFv3
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.content.Context;
|
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.util.AttributeSet;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import androidx.annotation.ColorInt;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
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.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||||
import org.session.libsession.utilities.Stub;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class AlbumThumbnailView extends FrameLayout {
|
public class AlbumThumbnailView extends FrameLayout {
|
||||||
|
|
||||||
private @Nullable SlideClickListener thumbnailClickListener;
|
private @Nullable SlideClickListener thumbnailClickListener;
|
||||||
@ -51,8 +53,8 @@ public class AlbumThumbnailView extends FrameLayout {
|
|||||||
private void initialize() {
|
private void initialize() {
|
||||||
inflate(getContext(), R.layout.album_thumbnail_view, this);
|
inflate(getContext(), R.layout.album_thumbnail_view, this);
|
||||||
|
|
||||||
albumCellContainer = findViewById(R.id.album_cell_container);
|
albumCellContainer = findViewById(R.id.albumCellContainer);
|
||||||
transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
|
transferControls = new Stub<>(findViewById(R.id.albumTransferControlsStub));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) {
|
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) {
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator;
|
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
|
|
||||||
|
@ -28,166 +28,166 @@ import org.session.libsession.utilities.TextSecurePreferences;
|
|||||||
|
|
||||||
public class ComposeText extends EmojiEditText {
|
public class ComposeText extends EmojiEditText {
|
||||||
|
|
||||||
private CharSequence hint;
|
private CharSequence hint;
|
||||||
private SpannableString subHint;
|
private SpannableString subHint;
|
||||||
|
|
||||||
@Nullable private InputPanel.MediaListener mediaListener;
|
@Nullable private InputPanel.MediaListener mediaListener;
|
||||||
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
|
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
|
||||||
|
|
||||||
public ComposeText(Context context) {
|
public ComposeText(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
initialize();
|
initialize();
|
||||||
}
|
|
||||||
|
|
||||||
public ComposeText(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComposeText(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTextTrimmed(){
|
|
||||||
return getText().toString().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
||||||
super.onLayout(changed, left, top, right, bottom);
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(hint)) {
|
|
||||||
if (!TextUtils.isEmpty(subHint)) {
|
|
||||||
setHint(new SpannableStringBuilder().append(ellipsizeToWidth(hint))
|
|
||||||
.append("\n")
|
|
||||||
.append(ellipsizeToWidth(subHint)));
|
|
||||||
} else {
|
|
||||||
setHint(ellipsizeToWidth(hint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSelectionChanged(int selStart, int selEnd) {
|
|
||||||
super.onSelectionChanged(selStart, selEnd);
|
|
||||||
|
|
||||||
if (cursorPositionChangedListener != null) {
|
|
||||||
cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CharSequence ellipsizeToWidth(CharSequence text) {
|
|
||||||
return TextUtils.ellipsize(text,
|
|
||||||
getPaint(),
|
|
||||||
getWidth() - getPaddingLeft() - getPaddingRight(),
|
|
||||||
TruncateAt.END);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHint(@NonNull String hint, @Nullable CharSequence subHint) {
|
|
||||||
this.hint = hint;
|
|
||||||
|
|
||||||
if (subHint != null) {
|
|
||||||
this.subHint = new SpannableString(subHint);
|
|
||||||
this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
|
||||||
} else {
|
|
||||||
this.subHint = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.subHint != null) {
|
public ComposeText(Context context, AttributeSet attrs) {
|
||||||
super.setHint(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint))
|
super(context, attrs);
|
||||||
.append("\n")
|
initialize();
|
||||||
.append(ellipsizeToWidth(this.subHint)));
|
|
||||||
} else {
|
|
||||||
super.setHint(ellipsizeToWidth(this.hint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCursorPositionChangedListener(@Nullable CursorPositionChangedListener listener) {
|
|
||||||
this.cursorPositionChangedListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTransport() {
|
|
||||||
final boolean useSystemEmoji = TextSecurePreferences.isSystemEmojiPreferred(getContext());
|
|
||||||
final boolean isIncognito = TextSecurePreferences.isIncognitoKeyboardEnabled(getContext());
|
|
||||||
|
|
||||||
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
|
|
||||||
int inputType = getInputType();
|
|
||||||
|
|
||||||
setImeActionLabel(null, 0);
|
|
||||||
|
|
||||||
if (useSystemEmoji) {
|
|
||||||
inputType = (inputType & ~InputType.TYPE_MASK_VARIATION) | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setInputType(inputType);
|
public ComposeText(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
if (isIncognito) {
|
super(context, attrs, defStyleAttr);
|
||||||
setImeOptions(imeOptions | 16777216);
|
initialize();
|
||||||
} else {
|
|
||||||
setImeOptions(imeOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
|
||||||
InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
|
|
||||||
|
|
||||||
if(TextSecurePreferences.isEnterSendsEnabled(getContext())) {
|
|
||||||
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < 21) return inputConnection;
|
public String getTextTrimmed(){
|
||||||
if (mediaListener == null) return inputConnection;
|
return getText().toString().trim();
|
||||||
if (inputConnection == null) return null;
|
|
||||||
|
|
||||||
EditorInfoCompat.setContentMimeTypes(editorInfo, new String[] {"image/jpeg", "image/png", "image/gif"});
|
|
||||||
return InputConnectionCompat.createWrapper(inputConnection, editorInfo, new CommitContentListener(mediaListener));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMediaListener(@Nullable InputPanel.MediaListener mediaListener) {
|
|
||||||
this.mediaListener = mediaListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
|
|
||||||
setImeOptions(getImeOptions() | 16777216);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR2)
|
|
||||||
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
|
|
||||||
|
|
||||||
private static final String TAG = CommitContentListener.class.getSimpleName();
|
|
||||||
|
|
||||||
private final InputPanel.MediaListener mediaListener;
|
|
||||||
|
|
||||||
private CommitContentListener(@NonNull InputPanel.MediaListener mediaListener) {
|
|
||||||
this.mediaListener = mediaListener;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||||
if (BuildCompat.isAtLeastNMR1() && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
try {
|
|
||||||
inputContentInfo.requestPermission();
|
if (!TextUtils.isEmpty(hint)) {
|
||||||
} catch (Exception e) {
|
if (!TextUtils.isEmpty(subHint)) {
|
||||||
Log.w(TAG, e);
|
setHint(new SpannableStringBuilder().append(ellipsizeToWidth(hint))
|
||||||
return false;
|
.append("\n")
|
||||||
|
.append(ellipsizeToWidth(subHint)));
|
||||||
|
} else {
|
||||||
|
setHint(ellipsizeToWidth(hint));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (inputContentInfo.getDescription().getMimeTypeCount() > 0) {
|
|
||||||
mediaListener.onMediaSelected(inputContentInfo.getContentUri(),
|
|
||||||
inputContentInfo.getDescription().getMimeType(0));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public interface CursorPositionChangedListener {
|
@Override
|
||||||
void onCursorPositionChanged(int start, int end);
|
protected void onSelectionChanged(int selStart, int selEnd) {
|
||||||
}
|
super.onSelectionChanged(selStart, selEnd);
|
||||||
|
|
||||||
|
if (cursorPositionChangedListener != null) {
|
||||||
|
cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharSequence ellipsizeToWidth(CharSequence text) {
|
||||||
|
return TextUtils.ellipsize(text,
|
||||||
|
getPaint(),
|
||||||
|
getWidth() - getPaddingLeft() - getPaddingRight(),
|
||||||
|
TruncateAt.END);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHint(@NonNull String hint, @Nullable CharSequence subHint) {
|
||||||
|
this.hint = hint;
|
||||||
|
|
||||||
|
if (subHint != null) {
|
||||||
|
this.subHint = new SpannableString(subHint);
|
||||||
|
this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||||
|
} else {
|
||||||
|
this.subHint = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.subHint != null) {
|
||||||
|
super.setHint(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint))
|
||||||
|
.append("\n")
|
||||||
|
.append(ellipsizeToWidth(this.subHint)));
|
||||||
|
} else {
|
||||||
|
super.setHint(ellipsizeToWidth(this.hint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCursorPositionChangedListener(@Nullable CursorPositionChangedListener listener) {
|
||||||
|
this.cursorPositionChangedListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTransport() {
|
||||||
|
final boolean useSystemEmoji = TextSecurePreferences.isSystemEmojiPreferred(getContext());
|
||||||
|
final boolean isIncognito = TextSecurePreferences.isIncognitoKeyboardEnabled(getContext());
|
||||||
|
|
||||||
|
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
|
||||||
|
int inputType = getInputType();
|
||||||
|
|
||||||
|
setImeActionLabel(null, 0);
|
||||||
|
|
||||||
|
if (useSystemEmoji) {
|
||||||
|
inputType = (inputType & ~InputType.TYPE_MASK_VARIATION) | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputType(inputType);
|
||||||
|
if (isIncognito) {
|
||||||
|
setImeOptions(imeOptions | 16777216);
|
||||||
|
} else {
|
||||||
|
setImeOptions(imeOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
||||||
|
InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
|
||||||
|
|
||||||
|
if(TextSecurePreferences.isEnterSendsEnabled(getContext())) {
|
||||||
|
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < 21) return inputConnection;
|
||||||
|
if (mediaListener == null) return inputConnection;
|
||||||
|
if (inputConnection == null) return null;
|
||||||
|
|
||||||
|
EditorInfoCompat.setContentMimeTypes(editorInfo, new String[] {"image/jpeg", "image/png", "image/gif"});
|
||||||
|
return InputConnectionCompat.createWrapper(inputConnection, editorInfo, new CommitContentListener(mediaListener));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMediaListener(@Nullable InputPanel.MediaListener mediaListener) {
|
||||||
|
this.mediaListener = mediaListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initialize() {
|
||||||
|
if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
|
||||||
|
setImeOptions(getImeOptions() | 16777216);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR2)
|
||||||
|
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
|
||||||
|
|
||||||
|
private static final String TAG = CommitContentListener.class.getSimpleName();
|
||||||
|
|
||||||
|
private final InputPanel.MediaListener mediaListener;
|
||||||
|
|
||||||
|
private CommitContentListener(@NonNull InputPanel.MediaListener mediaListener) {
|
||||||
|
this.mediaListener = mediaListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
|
||||||
|
if (BuildCompat.isAtLeastNMR1() && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
|
||||||
|
try {
|
||||||
|
inputContentInfo.requestPermission();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputContentInfo.getDescription().getMimeTypeCount() > 0) {
|
||||||
|
mediaListener.onMediaSelected(inputContentInfo.getContentUri(),
|
||||||
|
inputContentInfo.getDescription().getMimeType(0));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface CursorPositionChangedListener {
|
||||||
|
void onCursorPositionChanged(int start, int end);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,17 @@ import android.annotation.SuppressLint;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||||
@ -88,8 +90,6 @@ public class ConversationItemFooter extends LinearLayout {
|
|||||||
|
|
||||||
if (messageRecord.isFailed()) {
|
if (messageRecord.isFailed()) {
|
||||||
dateView.setText(R.string.ConversationItem_error_not_delivered);
|
dateView.setText(R.string.ConversationItem_error_not_delivered);
|
||||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
|
||||||
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
|
|
||||||
} else {
|
} else {
|
||||||
dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
|
dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
|
||||||
}
|
}
|
||||||
@ -131,14 +131,14 @@ public class ConversationItemFooter extends LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void presentInsecureIndicator(@NonNull MessageRecord messageRecord) {
|
private void presentInsecureIndicator(@NonNull MessageRecord messageRecord) {
|
||||||
insecureIndicatorView.setVisibility(messageRecord.isSecure() ? View.GONE : View.VISIBLE);
|
insecureIndicatorView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) {
|
private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) {
|
||||||
if (!messageRecord.isFailed() && !messageRecord.isPendingInsecureSmsFallback()) {
|
if (!messageRecord.isFailed()) {
|
||||||
if (!messageRecord.isOutgoing()) deliveryStatusView.setNone();
|
if (!messageRecord.isOutgoing()) deliveryStatusView.setNone();
|
||||||
else if (messageRecord.isPending()) deliveryStatusView.setPending();
|
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 if (messageRecord.isDelivered()) deliveryStatusView.setDelivered();
|
||||||
else deliveryStatusView.setSent();
|
else deliveryStatusView.setSent();
|
||||||
} else {
|
} else {
|
||||||
|
@ -12,6 +12,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
@ -27,7 +28,7 @@ import network.loki.messenger.R;
|
|||||||
|
|
||||||
public class ConversationItemThumbnail extends FrameLayout {
|
public class ConversationItemThumbnail extends FrameLayout {
|
||||||
|
|
||||||
private ThumbnailView thumbnail;
|
private ThumbnailView thumbnail;
|
||||||
private AlbumThumbnailView album;
|
private AlbumThumbnailView album;
|
||||||
private ImageView shade;
|
private ImageView shade;
|
||||||
private ConversationItemFooter footer;
|
private ConversationItemFooter footer;
|
||||||
@ -64,15 +65,10 @@ public class ConversationItemThumbnail extends FrameLayout {
|
|||||||
|
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
|
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();
|
typedArray.recycle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
|
||||||
@Override
|
@Override
|
||||||
protected void dispatchDraw(Canvas canvas) {
|
protected void dispatchDraw(Canvas canvas) {
|
||||||
super.dispatchDraw(canvas);
|
super.dispatchDraw(canvas);
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bottom navigation bar shown in the {@link org.thoughtcrime.securesms.conversation.ConversationActivity}
|
|
||||||
* when the user is searching within a conversation. Shows details about the results and allows the
|
|
||||||
* user to move between them.
|
|
||||||
*/
|
|
||||||
public class ConversationSearchBottomBar extends ConstraintLayout {
|
|
||||||
|
|
||||||
private View searchDown;
|
|
||||||
private View searchUp;
|
|
||||||
private TextView searchPositionText;
|
|
||||||
private View progressWheel;
|
|
||||||
|
|
||||||
private EventListener eventListener;
|
|
||||||
|
|
||||||
|
|
||||||
public ConversationSearchBottomBar(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConversationSearchBottomBar(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onFinishInflate() {
|
|
||||||
super.onFinishInflate();
|
|
||||||
|
|
||||||
this.searchUp = findViewById(R.id.conversation_search_up);
|
|
||||||
this.searchDown = findViewById(R.id.conversation_search_down);
|
|
||||||
this.searchPositionText = findViewById(R.id.conversation_search_position);
|
|
||||||
this.progressWheel = findViewById(R.id.conversation_search_progress_wheel);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setData(int position, int count) {
|
|
||||||
progressWheel.setVisibility(GONE);
|
|
||||||
|
|
||||||
searchUp.setOnClickListener(v -> {
|
|
||||||
if (eventListener != null) {
|
|
||||||
eventListener.onSearchMoveUpPressed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
searchDown.setOnClickListener(v -> {
|
|
||||||
if (eventListener != null) {
|
|
||||||
eventListener.onSearchMoveDownPressed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
searchPositionText.setText(getResources().getString(R.string.ConversationActivity_search_position, position + 1, count));
|
|
||||||
} else {
|
|
||||||
searchPositionText.setText(R.string.ConversationActivity_no_results);
|
|
||||||
}
|
|
||||||
|
|
||||||
setViewEnabled(searchUp, position < (count - 1));
|
|
||||||
setViewEnabled(searchDown, position > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showLoading() {
|
|
||||||
progressWheel.setVisibility(VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setViewEnabled(@NonNull View view, boolean enabled) {
|
|
||||||
view.setEnabled(enabled);
|
|
||||||
view.setAlpha(enabled ? 1f : 0.25f);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEventListener(@Nullable EventListener eventListener) {
|
|
||||||
this.eventListener = eventListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface EventListener {
|
|
||||||
void onSearchMoveUpPressed();
|
|
||||||
void onSearchMoveDownPressed();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.session.libsession.utilities.ThemeUtil;
|
|
||||||
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class ConversationTypingView extends LinearLayout {
|
|
||||||
|
|
||||||
private AvatarImageView avatar;
|
|
||||||
private View bubble;
|
|
||||||
private TypingIndicatorView indicator;
|
|
||||||
|
|
||||||
public ConversationTypingView(Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onFinishInflate() {
|
|
||||||
super.onFinishInflate();
|
|
||||||
|
|
||||||
avatar = findViewById(R.id.typing_avatar);
|
|
||||||
bubble = findViewById(R.id.typing_bubble);
|
|
||||||
indicator = findViewById(R.id.typing_indicator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTypists(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists, boolean isGroupThread) {
|
|
||||||
if (typists.isEmpty()) {
|
|
||||||
indicator.stopAnimation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Recipient typist = typists.get(0);
|
|
||||||
|
|
||||||
bubble.getBackground().setColorFilter(
|
|
||||||
ThemeUtil.getThemedColor(getContext(), R.attr.message_received_background_color),
|
|
||||||
PorterDuff.Mode.MULTIPLY);
|
|
||||||
|
|
||||||
if (isGroupThread) {
|
|
||||||
avatar.setAvatar(glideRequests, typist, false);
|
|
||||||
avatar.setVisibility(VISIBLE);
|
|
||||||
} else {
|
|
||||||
avatar.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
indicator.startAnimation();
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,443 +4,26 @@ import android.annotation.TargetApi;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import androidx.annotation.DimenRes;
|
|
||||||
import androidx.annotation.MainThread;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import android.text.format.DateUtils;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.animation.AlphaAnimation;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.AnimationSet;
|
|
||||||
import android.view.animation.Interpolator;
|
|
||||||
import android.view.animation.TranslateAnimation;
|
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
public class InputPanel extends LinearLayout {
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
public InputPanel(Context context) {
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
super(context);
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
|
|
||||||
import org.session.libsignal.utilities.ListenableFuture;
|
|
||||||
import org.session.libsignal.utilities.SettableFuture;
|
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class InputPanel extends LinearLayout
|
|
||||||
implements MicrophoneRecorderView.Listener,
|
|
||||||
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
|
||||||
EmojiKeyboardProvider.EmojiEventListener
|
|
||||||
{
|
|
||||||
|
|
||||||
private static final String TAG = InputPanel.class.getSimpleName();
|
|
||||||
|
|
||||||
private static final int FADE_TIME = 150;
|
|
||||||
|
|
||||||
private QuoteView quoteView;
|
|
||||||
private LinkPreviewView linkPreview;
|
|
||||||
private EmojiToggle mediaKeyboard;
|
|
||||||
public ComposeText composeText;
|
|
||||||
private View quickCameraToggle;
|
|
||||||
private View quickAudioToggle;
|
|
||||||
private View buttonToggle;
|
|
||||||
private View recordingContainer;
|
|
||||||
private View recordLockCancel;
|
|
||||||
|
|
||||||
private MicrophoneRecorderView microphoneRecorderView;
|
|
||||||
private SlideToCancel slideToCancel;
|
|
||||||
private RecordTime recordTime;
|
|
||||||
|
|
||||||
private @Nullable Listener listener;
|
|
||||||
private boolean emojiVisible;
|
|
||||||
|
|
||||||
public InputPanel(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputPanel(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
|
||||||
public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFinishInflate() {
|
|
||||||
super.onFinishInflate();
|
|
||||||
|
|
||||||
View quoteDismiss = findViewById(R.id.quote_dismiss);
|
|
||||||
|
|
||||||
this.quoteView = findViewById(R.id.quote_view);
|
|
||||||
this.linkPreview = findViewById(R.id.link_preview);
|
|
||||||
this.mediaKeyboard = findViewById(R.id.emoji_toggle);
|
|
||||||
this.composeText = findViewById(R.id.embedded_text_editor);
|
|
||||||
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
|
|
||||||
this.quickAudioToggle = findViewById(R.id.quick_audio_toggle);
|
|
||||||
this.buttonToggle = findViewById(R.id.button_toggle);
|
|
||||||
this.recordingContainer = findViewById(R.id.recording_container);
|
|
||||||
this.recordLockCancel = findViewById(R.id.record_cancel);
|
|
||||||
View slideToCancelView = findViewById(R.id.slide_to_cancel);
|
|
||||||
this.slideToCancel = new SlideToCancel(slideToCancelView);
|
|
||||||
this.microphoneRecorderView = findViewById(R.id.recorder_view);
|
|
||||||
this.microphoneRecorderView.setListener(this);
|
|
||||||
this.recordTime = new RecordTime(findViewById(R.id.record_time),
|
|
||||||
findViewById(R.id.microphone),
|
|
||||||
TimeUnit.HOURS.toSeconds(1),
|
|
||||||
() -> microphoneRecorderView.cancelAction());
|
|
||||||
|
|
||||||
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction());
|
|
||||||
|
|
||||||
if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
|
|
||||||
mediaKeyboard.setVisibility(View.GONE);
|
|
||||||
emojiVisible = false;
|
|
||||||
} else {
|
|
||||||
mediaKeyboard.setVisibility(View.VISIBLE);
|
|
||||||
emojiVisible = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
quoteDismiss.setOnClickListener(v -> clearQuote());
|
public InputPanel(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
linkPreview.setCloseClickedListener(() -> {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onLinkPreviewCanceled();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener(final @NonNull Listener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
|
|
||||||
mediaKeyboard.setOnClickListener(v -> listener.onEmojiToggle());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMediaListener(@NonNull MediaListener listener) {
|
|
||||||
composeText.setMediaListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setQuote(@NonNull GlideRequests glideRequests, long id, @NonNull Recipient author, @NonNull String body, @NonNull SlideDeck attachments, @NonNull Recipient conversationRecipient, long threadID) {
|
|
||||||
this.quoteView.setQuote(glideRequests, id, author, MentionUtilities.highlightMentions(body, threadID, getContext()), false, attachments, conversationRecipient);
|
|
||||||
this.quoteView.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
|
||||||
int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius);
|
|
||||||
this.linkPreview.setCorners(cornerRadius, cornerRadius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearQuote() {
|
|
||||||
this.quoteView.dismiss();
|
|
||||||
|
|
||||||
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
|
||||||
int cornerRadius = readDimen(R.dimen.message_corner_radius);
|
|
||||||
this.linkPreview.setCorners(cornerRadius, cornerRadius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<QuoteModel> getQuote() {
|
|
||||||
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
|
|
||||||
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getAddress(), quoteView.getBody(), false, quoteView.getAttachments()));
|
|
||||||
} else {
|
|
||||||
return Optional.absent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLinkPreviewLoading() {
|
|
||||||
this.linkPreview.setVisibility(View.VISIBLE);
|
|
||||||
this.linkPreview.setLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull Optional<LinkPreview> preview) {
|
|
||||||
if (preview.isPresent()) {
|
|
||||||
this.linkPreview.setVisibility(View.VISIBLE);
|
|
||||||
this.linkPreview.setLinkPreview(glideRequests, preview.get(), true);
|
|
||||||
} else {
|
|
||||||
this.linkPreview.setVisibility(View.GONE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int largeCornerRadius = (int)(16 * getResources().getDisplayMetrics().density);
|
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||||
int cornerRadius = quoteView.getVisibility() == VISIBLE ? readDimen(R.dimen.message_corner_collapse_radius) : largeCornerRadius;
|
public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
this.linkPreview.setCorners(cornerRadius, cornerRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMediaKeyboard(@NonNull MediaKeyboard mediaKeyboard) {
|
|
||||||
this.mediaKeyboard.attach(mediaKeyboard);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRecordPermissionRequired() {
|
|
||||||
if (listener != null) listener.onRecorderPermissionRequired();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRecordPressed() {
|
|
||||||
if (listener != null) listener.onRecorderStarted();
|
|
||||||
recordTime.display();
|
|
||||||
slideToCancel.display();
|
|
||||||
|
|
||||||
if (emojiVisible) ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE);
|
|
||||||
ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE);
|
|
||||||
ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE);
|
|
||||||
ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE);
|
|
||||||
buttonToggle.animate().alpha(0).setDuration(FADE_TIME).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRecordReleased() {
|
|
||||||
long elapsedTime = onRecordHideEvent();
|
|
||||||
|
|
||||||
if (listener != null) {
|
|
||||||
Log.d(TAG, "Elapsed time: " + elapsedTime);
|
|
||||||
if (elapsedTime > 1000) {
|
|
||||||
listener.onRecorderFinished();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(getContext(), R.string.InputPanel_tap_and_hold_to_record_a_voice_message_release_to_send, Toast.LENGTH_LONG).show();
|
|
||||||
listener.onRecorderCanceled();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRecordMoved(float offsetX, float absoluteX) {
|
|
||||||
slideToCancel.moveTo(offsetX);
|
|
||||||
|
|
||||||
int direction = ViewCompat.getLayoutDirection(this);
|
|
||||||
float position = absoluteX / recordingContainer.getWidth();
|
|
||||||
|
|
||||||
if (direction == ViewCompat.LAYOUT_DIRECTION_LTR && position <= 0.5 ||
|
|
||||||
direction == ViewCompat.LAYOUT_DIRECTION_RTL && position >= 0.6)
|
|
||||||
{
|
|
||||||
this.microphoneRecorderView.cancelAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRecordCanceled() {
|
|
||||||
onRecordHideEvent();
|
|
||||||
if (listener != null) listener.onRecorderCanceled();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRecordLocked() {
|
|
||||||
slideToCancel.hide();
|
|
||||||
recordLockCancel.setVisibility(View.VISIBLE);
|
|
||||||
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
|
|
||||||
if (listener != null) listener.onRecorderLocked();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPause() {
|
|
||||||
this.microphoneRecorderView.cancelAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEnabled(boolean enabled) {
|
|
||||||
composeText.setEnabled(enabled);
|
|
||||||
mediaKeyboard.setEnabled(enabled);
|
|
||||||
quickAudioToggle.setEnabled(enabled);
|
|
||||||
quickCameraToggle.setEnabled(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHint(@NonNull String hint) {
|
|
||||||
composeText.setHint(hint, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long onRecordHideEvent() {
|
|
||||||
recordLockCancel.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
ListenableFuture<Void> future = slideToCancel.hide();
|
|
||||||
long elapsedTime = recordTime.hide();
|
|
||||||
|
|
||||||
future.addListener(new AssertedSuccessListener<Void>() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(Void result) {
|
|
||||||
if (emojiVisible) ViewUtil.fadeIn(mediaKeyboard, FADE_TIME);
|
|
||||||
ViewUtil.fadeIn(composeText, FADE_TIME);
|
|
||||||
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
|
|
||||||
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
|
|
||||||
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return elapsedTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onKeyboardShown() {
|
|
||||||
mediaKeyboard.setToMedia();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onKeyEvent(KeyEvent keyEvent) {
|
|
||||||
composeText.dispatchKeyEvent(keyEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEmojiSelected(String emoji) {
|
|
||||||
composeText.insertEmoji(emoji);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int readDimen(@DimenRes int dimenRes) {
|
|
||||||
return getResources().getDimensionPixelSize(dimenRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRecordingInLockedMode() {
|
|
||||||
return microphoneRecorderView.isRecordingLocked();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void releaseRecordingLock() {
|
|
||||||
microphoneRecorderView.unlockAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Listener {
|
|
||||||
void onRecorderStarted();
|
|
||||||
void onRecorderLocked();
|
|
||||||
void onRecorderFinished();
|
|
||||||
void onRecorderCanceled();
|
|
||||||
void onRecorderPermissionRequired();
|
|
||||||
void onEmojiToggle();
|
|
||||||
void onLinkPreviewCanceled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SlideToCancel {
|
|
||||||
|
|
||||||
private final View slideToCancelView;
|
|
||||||
|
|
||||||
SlideToCancel(View slideToCancelView) {
|
|
||||||
this.slideToCancelView = slideToCancelView;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void display() {
|
public interface MediaListener {
|
||||||
ViewUtil.fadeIn(this.slideToCancelView, FADE_TIME);
|
void onMediaSelected(@NonNull Uri uri, String contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListenableFuture<Void> hide() {
|
|
||||||
final SettableFuture<Void> future = new SettableFuture<>();
|
|
||||||
|
|
||||||
AnimationSet animation = new AnimationSet(true);
|
|
||||||
animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, slideToCancelView.getTranslationX(),
|
|
||||||
Animation.ABSOLUTE, 0,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0));
|
|
||||||
animation.addAnimation(new AlphaAnimation(1, 0));
|
|
||||||
|
|
||||||
animation.setDuration(MicrophoneRecorderView.ANIMATION_DURATION);
|
|
||||||
animation.setFillBefore(true);
|
|
||||||
animation.setFillAfter(false);
|
|
||||||
|
|
||||||
slideToCancelView.postDelayed(() -> future.set(null), MicrophoneRecorderView.ANIMATION_DURATION);
|
|
||||||
slideToCancelView.setVisibility(View.GONE);
|
|
||||||
slideToCancelView.startAnimation(animation);
|
|
||||||
|
|
||||||
return future;
|
|
||||||
}
|
|
||||||
|
|
||||||
void moveTo(float offset) {
|
|
||||||
Animation animation = new TranslateAnimation(Animation.ABSOLUTE, offset,
|
|
||||||
Animation.ABSOLUTE, offset,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0);
|
|
||||||
|
|
||||||
animation.setDuration(0);
|
|
||||||
animation.setFillAfter(true);
|
|
||||||
animation.setFillBefore(true);
|
|
||||||
|
|
||||||
slideToCancelView.startAnimation(animation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class RecordTime implements Runnable {
|
|
||||||
|
|
||||||
private final @NonNull TextView recordTimeView;
|
|
||||||
private final @NonNull View microphone;
|
|
||||||
private final @NonNull Runnable onLimitHit;
|
|
||||||
private final long limitSeconds;
|
|
||||||
private long startTime;
|
|
||||||
|
|
||||||
private RecordTime(@NonNull TextView recordTimeView, @NonNull View microphone, long limitSeconds, @NonNull Runnable onLimitHit) {
|
|
||||||
this.recordTimeView = recordTimeView;
|
|
||||||
this.microphone = microphone;
|
|
||||||
this.limitSeconds = limitSeconds;
|
|
||||||
this.onLimitHit = onLimitHit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
public void display() {
|
|
||||||
this.startTime = System.currentTimeMillis();
|
|
||||||
this.recordTimeView.setText(DateUtils.formatElapsedTime(0));
|
|
||||||
ViewUtil.fadeIn(this.recordTimeView, FADE_TIME);
|
|
||||||
Util.runOnMainDelayed(this, TimeUnit.SECONDS.toMillis(1));
|
|
||||||
microphone.setVisibility(View.VISIBLE);
|
|
||||||
microphone.startAnimation(pulseAnimation());
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
public long hide() {
|
|
||||||
long elapsedTime = System.currentTimeMillis() - startTime;
|
|
||||||
this.startTime = 0;
|
|
||||||
ViewUtil.fadeOut(this.recordTimeView, FADE_TIME, View.INVISIBLE);
|
|
||||||
microphone.clearAnimation();
|
|
||||||
ViewUtil.fadeOut(this.microphone, FADE_TIME, View.INVISIBLE);
|
|
||||||
return elapsedTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@MainThread
|
|
||||||
public void run() {
|
|
||||||
long localStartTime = startTime;
|
|
||||||
if (localStartTime > 0) {
|
|
||||||
long elapsedTime = System.currentTimeMillis() - localStartTime;
|
|
||||||
long elapsedSeconds = TimeUnit.MILLISECONDS.toSeconds(elapsedTime);
|
|
||||||
if (elapsedSeconds >= limitSeconds) {
|
|
||||||
onLimitHit.run();
|
|
||||||
} else {
|
|
||||||
recordTimeView.setText(DateUtils.formatElapsedTime(elapsedSeconds));
|
|
||||||
Util.runOnMainDelayed(this, TimeUnit.SECONDS.toMillis(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Animation pulseAnimation() {
|
|
||||||
AlphaAnimation animation = new AlphaAnimation(0, 1);
|
|
||||||
|
|
||||||
animation.setInterpolator(pulseInterpolator());
|
|
||||||
animation.setRepeatCount(Animation.INFINITE);
|
|
||||||
animation.setDuration(1000);
|
|
||||||
|
|
||||||
return animation;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Interpolator pulseInterpolator() {
|
|
||||||
return input -> {
|
|
||||||
input *= 5;
|
|
||||||
if (input > 1) {
|
|
||||||
input = 4 - input;
|
|
||||||
}
|
|
||||||
return Math.max(0, Math.min(1, input));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface MediaListener {
|
|
||||||
void onMediaSelected(@NonNull Uri uri, String contentType);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.views
|
package org.thoughtcrime.securesms.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
@ -9,7 +9,7 @@ import android.view.LayoutInflater
|
|||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import kotlinx.android.synthetic.main.view_separator.view.*
|
import kotlinx.android.synthetic.main.view_separator.view.*
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import org.session.libsession.utilities.ThemeUtil
|
import org.session.libsession.utilities.ThemeUtil
|
||||||
|
|
||||||
class LabeledSeparatorView : RelativeLayout {
|
class LabeledSeparatorView : RelativeLayout {
|
@ -1,272 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.AnimationSet;
|
|
||||||
import android.view.animation.AnticipateOvershootInterpolator;
|
|
||||||
import android.view.animation.DecelerateInterpolator;
|
|
||||||
import android.view.animation.LinearInterpolator;
|
|
||||||
import android.view.animation.OvershootInterpolator;
|
|
||||||
import android.view.animation.ScaleAnimation;
|
|
||||||
import android.view.animation.TranslateAnimation;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public final class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener {
|
|
||||||
|
|
||||||
enum State {
|
|
||||||
NOT_RUNNING,
|
|
||||||
RUNNING_HELD,
|
|
||||||
RUNNING_LOCKED
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final int ANIMATION_DURATION = 200;
|
|
||||||
|
|
||||||
private FloatingRecordButton floatingRecordButton;
|
|
||||||
private LockDropTarget lockDropTarget;
|
|
||||||
private @Nullable Listener listener;
|
|
||||||
private @NonNull State state = State.NOT_RUNNING;
|
|
||||||
|
|
||||||
public MicrophoneRecorderView(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MicrophoneRecorderView(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFinishInflate() {
|
|
||||||
super.onFinishInflate();
|
|
||||||
|
|
||||||
floatingRecordButton = new FloatingRecordButton(getContext(), findViewById(R.id.quick_audio_fab));
|
|
||||||
lockDropTarget = new LockDropTarget (getContext(), findViewById(R.id.lock_drop_target));
|
|
||||||
|
|
||||||
View recordButton = ViewUtil.findById(this, R.id.quick_audio_toggle);
|
|
||||||
recordButton.setOnTouchListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelAction() {
|
|
||||||
if (state != State.NOT_RUNNING) {
|
|
||||||
state = State.NOT_RUNNING;
|
|
||||||
hideUi();
|
|
||||||
|
|
||||||
if (listener != null) listener.onRecordCanceled();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRecordingLocked() {
|
|
||||||
return state == State.RUNNING_LOCKED;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void lockAction() {
|
|
||||||
if (state == State.RUNNING_HELD) {
|
|
||||||
state = State.RUNNING_LOCKED;
|
|
||||||
hideUi();
|
|
||||||
|
|
||||||
if (listener != null) listener.onRecordLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockAction() {
|
|
||||||
if (state == State.RUNNING_LOCKED) {
|
|
||||||
state = State.NOT_RUNNING;
|
|
||||||
hideUi();
|
|
||||||
|
|
||||||
if (listener != null) listener.onRecordReleased();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideUi() {
|
|
||||||
floatingRecordButton.hide();
|
|
||||||
lockDropTarget.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouch(View v, final MotionEvent event) {
|
|
||||||
switch (event.getAction()) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
if (!Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO)) {
|
|
||||||
if (listener != null) listener.onRecordPermissionRequired();
|
|
||||||
} else {
|
|
||||||
state = State.RUNNING_HELD;
|
|
||||||
floatingRecordButton.display(event.getX(), event.getY());
|
|
||||||
lockDropTarget.display();
|
|
||||||
if (listener != null) listener.onRecordPressed();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
if (this.state == State.RUNNING_HELD) {
|
|
||||||
state = State.NOT_RUNNING;
|
|
||||||
hideUi();
|
|
||||||
if (listener != null) listener.onRecordReleased();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MotionEvent.ACTION_MOVE:
|
|
||||||
if (this.state == State.RUNNING_HELD) {
|
|
||||||
this.floatingRecordButton.moveTo(event.getX(), event.getY());
|
|
||||||
if (listener != null) listener.onRecordMoved(floatingRecordButton.lastOffsetX, event.getRawX());
|
|
||||||
|
|
||||||
int dimensionPixelSize = getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target);
|
|
||||||
if (floatingRecordButton.lastOffsetY <= dimensionPixelSize) {
|
|
||||||
lockAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener(@Nullable Listener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Listener {
|
|
||||||
void onRecordPressed();
|
|
||||||
void onRecordReleased();
|
|
||||||
void onRecordCanceled();
|
|
||||||
void onRecordLocked();
|
|
||||||
void onRecordMoved(float offsetX, float absoluteX);
|
|
||||||
void onRecordPermissionRequired();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class FloatingRecordButton {
|
|
||||||
|
|
||||||
private final ImageView recordButtonFab;
|
|
||||||
|
|
||||||
private float startPositionX;
|
|
||||||
private float startPositionY;
|
|
||||||
private float lastOffsetX;
|
|
||||||
private float lastOffsetY;
|
|
||||||
|
|
||||||
FloatingRecordButton(Context context, ImageView recordButtonFab) {
|
|
||||||
this.recordButtonFab = recordButtonFab;
|
|
||||||
this.recordButtonFab.getBackground().setColorFilter(context.getResources()
|
|
||||||
.getColor(R.color.destructive),
|
|
||||||
PorterDuff.Mode.SRC_IN);
|
|
||||||
}
|
|
||||||
|
|
||||||
void display(float x, float y) {
|
|
||||||
this.startPositionX = x;
|
|
||||||
this.startPositionY = y;
|
|
||||||
|
|
||||||
recordButtonFab.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
AnimationSet animation = new AnimationSet(true);
|
|
||||||
animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, 0,
|
|
||||||
Animation.ABSOLUTE, 0,
|
|
||||||
Animation.ABSOLUTE, 0,
|
|
||||||
Animation.ABSOLUTE, 0));
|
|
||||||
|
|
||||||
animation.addAnimation(new ScaleAnimation(.5f, 1f, .5f, 1f,
|
|
||||||
Animation.RELATIVE_TO_SELF, .5f,
|
|
||||||
Animation.RELATIVE_TO_SELF, .5f));
|
|
||||||
|
|
||||||
animation.setDuration(ANIMATION_DURATION);
|
|
||||||
animation.setInterpolator(new OvershootInterpolator());
|
|
||||||
|
|
||||||
recordButtonFab.startAnimation(animation);
|
|
||||||
}
|
|
||||||
|
|
||||||
void moveTo(float x, float y) {
|
|
||||||
lastOffsetX = getXOffset(x);
|
|
||||||
lastOffsetY = getYOffset(y);
|
|
||||||
|
|
||||||
if (Math.abs(lastOffsetX) > Math.abs(lastOffsetY)) {
|
|
||||||
lastOffsetY = 0;
|
|
||||||
} else {
|
|
||||||
lastOffsetX = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
recordButtonFab.setTranslationX(lastOffsetX);
|
|
||||||
recordButtonFab.setTranslationY(lastOffsetY);
|
|
||||||
}
|
|
||||||
|
|
||||||
void hide() {
|
|
||||||
recordButtonFab.setTranslationX(0);
|
|
||||||
recordButtonFab.setTranslationY(0);
|
|
||||||
if (recordButtonFab.getVisibility() != VISIBLE) return;
|
|
||||||
|
|
||||||
AnimationSet animation = new AnimationSet(false);
|
|
||||||
Animation scaleAnimation = new ScaleAnimation(1, 0.5f, 1, 0.5f,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0.5f,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0.5f);
|
|
||||||
|
|
||||||
Animation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, lastOffsetX,
|
|
||||||
Animation.ABSOLUTE, 0,
|
|
||||||
Animation.ABSOLUTE, lastOffsetY,
|
|
||||||
Animation.ABSOLUTE, 0);
|
|
||||||
|
|
||||||
scaleAnimation.setInterpolator(new AnticipateOvershootInterpolator(1.5f));
|
|
||||||
translateAnimation.setInterpolator(new DecelerateInterpolator());
|
|
||||||
animation.addAnimation(scaleAnimation);
|
|
||||||
animation.addAnimation(translateAnimation);
|
|
||||||
animation.setDuration(ANIMATION_DURATION);
|
|
||||||
animation.setInterpolator(new AnticipateOvershootInterpolator(1.5f));
|
|
||||||
|
|
||||||
recordButtonFab.setVisibility(View.GONE);
|
|
||||||
recordButtonFab.clearAnimation();
|
|
||||||
recordButtonFab.startAnimation(animation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private float getXOffset(float x) {
|
|
||||||
return ViewCompat.getLayoutDirection(recordButtonFab) == ViewCompat.LAYOUT_DIRECTION_LTR ?
|
|
||||||
-Math.max(0, this.startPositionX - x) : Math.max(0, x - this.startPositionX);
|
|
||||||
}
|
|
||||||
|
|
||||||
private float getYOffset(float y) {
|
|
||||||
return Math.min(0, y - this.startPositionY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class LockDropTarget {
|
|
||||||
|
|
||||||
private final View lockDropTarget;
|
|
||||||
private final int dropTargetPosition;
|
|
||||||
|
|
||||||
LockDropTarget(Context context, View lockDropTarget) {
|
|
||||||
this.lockDropTarget = lockDropTarget;
|
|
||||||
this.dropTargetPosition = context.getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target);
|
|
||||||
}
|
|
||||||
|
|
||||||
void display() {
|
|
||||||
lockDropTarget.setScaleX(1);
|
|
||||||
lockDropTarget.setScaleY(1);
|
|
||||||
lockDropTarget.setAlpha(0);
|
|
||||||
lockDropTarget.setTranslationY(0);
|
|
||||||
lockDropTarget.setVisibility(VISIBLE);
|
|
||||||
lockDropTarget.animate()
|
|
||||||
.setStartDelay(ANIMATION_DURATION * 2)
|
|
||||||
.setDuration(ANIMATION_DURATION)
|
|
||||||
.setInterpolator(new DecelerateInterpolator())
|
|
||||||
.translationY(dropTargetPosition)
|
|
||||||
.alpha(1)
|
|
||||||
.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void hide() {
|
|
||||||
lockDropTarget.animate()
|
|
||||||
.setStartDelay(0)
|
|
||||||
.setDuration(ANIMATION_DURATION)
|
|
||||||
.setInterpolator(new LinearInterpolator())
|
|
||||||
.scaleX(0).scaleY(0)
|
|
||||||
.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,6 +5,7 @@ import android.graphics.Canvas;
|
|||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import org.session.libsession.utilities.ThemeUtil;
|
import org.session.libsession.utilities.ThemeUtil;
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
@ -28,7 +29,6 @@ public class OutlinedThumbnailView extends ThumbnailView {
|
|||||||
outliner = new Outliner();
|
outliner = new Outliner();
|
||||||
|
|
||||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||||
setRadius(0);
|
|
||||||
setWillNotDraw(false);
|
setWillNotDraw(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.views
|
package org.thoughtcrime.securesms.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@ -17,7 +17,7 @@ import org.session.libsession.utilities.Address
|
|||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator
|
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class ProfilePictureView : RelativeLayout {
|
class ProfilePictureView : RelativeLayout {
|
||||||
@ -31,23 +31,12 @@ class ProfilePictureView : RelativeLayout {
|
|||||||
private val profilePicturesCache = mutableMapOf<String, String?>()
|
private val profilePicturesCache = mutableMapOf<String, String?>()
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) {
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
setUpViewHierarchy()
|
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) {
|
private fun initialize() {
|
||||||
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() {
|
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
val contentView = inflater.inflate(R.layout.view_profile_picture, null)
|
val contentView = inflater.inflate(R.layout.view_profile_picture, null)
|
||||||
addView(contentView)
|
addView(contentView)
|
@ -22,8 +22,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|||||||
import org.session.libsession.messaging.contacts.Contact;
|
import org.session.libsession.messaging.contacts.Contact;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.loki.database.SessionContactDatabase;
|
import org.thoughtcrime.securesms.database.SessionContactDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
|
import org.thoughtcrime.securesms.util.UiModeUtilities;
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
|
@ -16,7 +16,7 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.appcompat.widget.SearchView;
|
import androidx.appcompat.widget.SearchView;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import android.util.AttributeSet;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
|
@ -8,7 +8,7 @@ import org.session.libsession.messaging.messages.control.TypingIndicator;
|
|||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
|
||||||
@ -79,8 +79,7 @@ public class TypingStatusSender {
|
|||||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||||
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
|
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
|
||||||
if (recipient == null) { return; }
|
if (recipient == null) { return; }
|
||||||
// Loki - Check whether we want to send a typing indicator to this user
|
if (!SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; }
|
||||||
if (recipient != null && !SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; }
|
|
||||||
TypingIndicator typingIndicator;
|
TypingIndicator typingIndicator;
|
||||||
if (typingStarted) {
|
if (typingStarted) {
|
||||||
typingIndicator = new TypingIndicator(TypingIndicator.Kind.STARTED);
|
typingIndicator = new TypingIndicator(TypingIndicator.Kind.STARTED);
|
||||||
|
@ -11,7 +11,6 @@ import android.text.SpannableStringBuilder;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
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.libsession.utilities.Util;
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
|
|
||||||
|
|
||||||
public class EmojiTextView extends AppCompatTextView {
|
public class EmojiTextView extends AppCompatTextView {
|
||||||
|
|
||||||
private final boolean scaleEmojis;
|
private final boolean scaleEmojis;
|
||||||
|
|
||||||
private static final char ELLIPSIS = '…';
|
private static final char ELLIPSIS = '…';
|
||||||
@ -46,14 +43,9 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
|
|
||||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
|
scaleEmojis = true;
|
||||||
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
|
maxLength = 1000;
|
||||||
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
|
originalFontSize = getResources().getDimension(R.dimen.small_font_size);
|
||||||
a.recycle();
|
|
||||||
|
|
||||||
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
|
|
||||||
originalFontSize = a.getDimensionPixelSize(0, 0);
|
|
||||||
a.recycle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void setText(@Nullable CharSequence text, BufferType type) {
|
@Override public void setText(@Nullable CharSequence text, BufferType type) {
|
||||||
@ -182,8 +174,11 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidateDrawable(@NonNull Drawable drawable) {
|
public void invalidateDrawable(@NonNull Drawable drawable) {
|
||||||
if (drawable instanceof EmojiDrawable) invalidate();
|
if (drawable instanceof EmojiDrawable) {
|
||||||
else super.invalidateDrawable(drawable);
|
invalidate();
|
||||||
|
} else {
|
||||||
|
super.invalidateDrawable(drawable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.fragments
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@ -7,7 +7,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import kotlinx.android.synthetic.main.contact_selection_list_divider.view.*
|
import kotlinx.android.synthetic.main.contact_selection_list_divider.view.*
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.loki.views.UserView
|
import org.thoughtcrime.securesms.contacts.UserView
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.fragments
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@ -11,10 +11,11 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import kotlinx.android.synthetic.main.contact_selection_list_fragment.*
|
import kotlinx.android.synthetic.main.contact_selection_list_fragment.*
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem
|
||||||
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListLoader
|
||||||
|
|
||||||
class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener {
|
class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener {
|
||||||
private var cursorFilter: String? = null
|
private var cursorFilter: String? = null
|
||||||
@ -98,7 +99,7 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
|||||||
update(listOf())
|
update(listOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun update(items: List<ContactSelectionListItem>) {
|
private fun update(items: List<ContactSelectionListItem>) {
|
||||||
if (activity?.isDestroyed == true) {
|
if (activity?.isDestroyed == true) {
|
||||||
Log.e(ContactSelectionListFragment::class.java.name,
|
Log.e(ContactSelectionListFragment::class.java.name,
|
||||||
"Received a loader callback after the fragment was detached from the activity.",
|
"Received a loader callback after the fragment was detached from the activity.",
|
@ -1,8 +1,8 @@
|
|||||||
package org.thoughtcrime.securesms.loki.fragments
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
|
import org.thoughtcrime.securesms.util.ContactUtilities
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.AsyncLoader
|
import org.thoughtcrime.securesms.util.AsyncLoader
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.activities
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@ -40,8 +40,8 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
|||||||
setContentView(R.layout.activity_select_contacts)
|
setContentView(R.layout.activity_select_contacts)
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
|
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
|
||||||
|
|
||||||
usersToExclude = intent.getStringArrayExtra(Companion.usersToExcludeKey)?.toSet() ?: setOf()
|
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
|
||||||
val emptyStateText = intent.getStringExtra(Companion.emptyStateTextKey)
|
val emptyStateText = intent.getStringExtra(emptyStateTextKey)
|
||||||
if (emptyStateText != null) {
|
if (emptyStateText != null) {
|
||||||
emptyStateMessageTextView.text = emptyStateText
|
emptyStateMessageTextView.text = emptyStateText
|
||||||
}
|
}
|
@ -1,10 +1,9 @@
|
|||||||
package org.thoughtcrime.securesms.loki.activities
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.thoughtcrime.securesms.loki.views.UserView
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.loki.activities
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
|
import org.thoughtcrime.securesms.util.ContactUtilities
|
||||||
import org.thoughtcrime.securesms.util.AsyncLoader
|
import org.thoughtcrime.securesms.util.AsyncLoader
|
||||||
|
|
||||||
class SelectContactsLoader(context: Context, val usersToExclude: Set<String>) : AsyncLoader<List<String>>(context) {
|
class SelectContactsLoader(context: Context, val usersToExclude: Set<String>) : AsyncLoader<List<String>>(context) {
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.views
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@ -10,7 +10,7 @@ import kotlinx.android.synthetic.main.view_user.view.*
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
@ -1,532 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2011 Whisper Systems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* 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.conversation;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import androidx.annotation.LayoutRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import android.util.SparseArray;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.database.FastCursorRecyclerViewAdapter;
|
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
|
||||||
import org.thoughtcrime.securesms.util.LRUCache;
|
|
||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
|
||||||
import org.session.libsession.utilities.Conversions;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import java.lang.ref.SoftReference;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A cursor adapter for a conversation thread. Ultimately
|
|
||||||
* used by ComposeMessageActivity to display a conversation
|
|
||||||
* thread in a ListActivity.
|
|
||||||
*
|
|
||||||
* @author Moxie Marlinspike
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class ConversationAdapter <V extends View & BindableConversationItem>
|
|
||||||
extends FastCursorRecyclerViewAdapter<ConversationAdapter.ViewHolder, MessageRecord>
|
|
||||||
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
|
|
||||||
{
|
|
||||||
|
|
||||||
private static final int MAX_CACHE_SIZE = 1000;
|
|
||||||
private static final String TAG = ConversationAdapter.class.getSimpleName();
|
|
||||||
private final Map<String,SoftReference<MessageRecord>> messageRecordCache =
|
|
||||||
Collections.synchronizedMap(new LRUCache<String, SoftReference<MessageRecord>>(MAX_CACHE_SIZE));
|
|
||||||
private final SparseArray<String> positionToCacheRef = new SparseArray<>();
|
|
||||||
|
|
||||||
private static final int MESSAGE_TYPE_OUTGOING = 0;
|
|
||||||
private static final int MESSAGE_TYPE_INCOMING = 1;
|
|
||||||
private static final int MESSAGE_TYPE_UPDATE = 2;
|
|
||||||
private static final int MESSAGE_TYPE_AUDIO_OUTGOING = 3;
|
|
||||||
private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4;
|
|
||||||
private static final int MESSAGE_TYPE_THUMBNAIL_OUTGOING = 5;
|
|
||||||
private static final int MESSAGE_TYPE_THUMBNAIL_INCOMING = 6;
|
|
||||||
private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7;
|
|
||||||
private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8;
|
|
||||||
private static final int MESSAGE_TYPE_INVITATION_OUTGOING = 9;
|
|
||||||
private static final int MESSAGE_TYPE_INVITATION_INCOMING = 10;
|
|
||||||
|
|
||||||
private final Set<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>());
|
|
||||||
|
|
||||||
private final @Nullable ItemClickListener clickListener;
|
|
||||||
private final @NonNull
|
|
||||||
GlideRequests glideRequests;
|
|
||||||
private final @NonNull Locale locale;
|
|
||||||
private final @NonNull Recipient recipient;
|
|
||||||
private final @NonNull MmsSmsDatabase db;
|
|
||||||
private final @NonNull LayoutInflater inflater;
|
|
||||||
private final @NonNull Calendar calendar;
|
|
||||||
private final @NonNull MessageDigest digest;
|
|
||||||
|
|
||||||
private MessageRecord recordToPulseHighlight;
|
|
||||||
private String searchQuery;
|
|
||||||
|
|
||||||
protected static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
|
|
||||||
super(itemView);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public <V extends View & BindableConversationItem> V getView() {
|
|
||||||
return (V)itemView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
TextView textView;
|
|
||||||
|
|
||||||
HeaderViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
textView = ViewUtil.findById(itemView, R.id.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
HeaderViewHolder(TextView textView) {
|
|
||||||
super(textView);
|
|
||||||
this.textView = textView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setText(CharSequence text) {
|
|
||||||
textView.setText(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface ItemClickListener extends BindableConversationItem.EventListener {
|
|
||||||
void onItemClick(MessageRecord item);
|
|
||||||
void onItemLongClick(MessageRecord item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@VisibleForTesting
|
|
||||||
ConversationAdapter(Context context, Cursor cursor) {
|
|
||||||
super(context, cursor);
|
|
||||||
try {
|
|
||||||
this.glideRequests = null;
|
|
||||||
this.locale = null;
|
|
||||||
this.clickListener = null;
|
|
||||||
this.recipient = null;
|
|
||||||
this.inflater = null;
|
|
||||||
this.db = null;
|
|
||||||
this.calendar = null;
|
|
||||||
this.digest = MessageDigest.getInstance("SHA1");
|
|
||||||
} catch (NoSuchAlgorithmException nsae) {
|
|
||||||
throw new AssertionError("SHA1 isn't supported!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConversationAdapter(@NonNull Context context,
|
|
||||||
@NonNull GlideRequests glideRequests,
|
|
||||||
@NonNull Locale locale,
|
|
||||||
@Nullable ItemClickListener clickListener,
|
|
||||||
@Nullable Cursor cursor,
|
|
||||||
@NonNull Recipient recipient)
|
|
||||||
{
|
|
||||||
super(context, cursor);
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.glideRequests = glideRequests;
|
|
||||||
this.locale = locale;
|
|
||||||
this.clickListener = clickListener;
|
|
||||||
this.recipient = recipient;
|
|
||||||
this.inflater = LayoutInflater.from(context);
|
|
||||||
this.db = DatabaseFactory.getMmsSmsDatabase(context);
|
|
||||||
this.calendar = Calendar.getInstance();
|
|
||||||
this.digest = MessageDigest.getInstance("SHA1");
|
|
||||||
|
|
||||||
setHasStableIds(true);
|
|
||||||
} catch (NoSuchAlgorithmException nsae) {
|
|
||||||
throw new AssertionError("SHA1 isn't supported!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void changeCursor(Cursor cursor) {
|
|
||||||
messageRecordCache.clear();
|
|
||||||
positionToCacheRef.clear();
|
|
||||||
super.cleanFastRecords();
|
|
||||||
super.changeCursor(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onBindItemViewHolder(ViewHolder viewHolder, @NonNull MessageRecord messageRecord) {
|
|
||||||
int adapterPosition = viewHolder.getAdapterPosition();
|
|
||||||
|
|
||||||
String prevCachedId = positionToCacheRef.get(adapterPosition + 1,null);
|
|
||||||
String nextCachedId = positionToCacheRef.get(adapterPosition - 1, null);
|
|
||||||
|
|
||||||
MessageRecord previousRecord = null;
|
|
||||||
if (adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1)) {
|
|
||||||
if (prevCachedId != null && messageRecordCache.containsKey(prevCachedId)) {
|
|
||||||
SoftReference<MessageRecord> prevSoftRecord = messageRecordCache.get(prevCachedId);
|
|
||||||
MessageRecord prevCachedRecord = prevSoftRecord.get();
|
|
||||||
if (prevCachedRecord != null) {
|
|
||||||
previousRecord = prevCachedRecord;
|
|
||||||
} else {
|
|
||||||
previousRecord = getRecordForPositionOrThrow(adapterPosition + 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
previousRecord = getRecordForPositionOrThrow(adapterPosition + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageRecord nextRecord = null;
|
|
||||||
if (adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1)) {
|
|
||||||
if (nextCachedId != null && messageRecordCache.containsKey(nextCachedId)) {
|
|
||||||
SoftReference<MessageRecord> nextSoftRecord = messageRecordCache.get(nextCachedId);
|
|
||||||
MessageRecord nextCachedRecord = nextSoftRecord.get();
|
|
||||||
if (nextCachedRecord != null) {
|
|
||||||
nextRecord = nextCachedRecord;
|
|
||||||
} else {
|
|
||||||
nextRecord = getRecordForPositionOrThrow(adapterPosition - 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nextRecord = getRecordForPositionOrThrow(adapterPosition - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewHolder.getView().bind(messageRecord,
|
|
||||||
Optional.fromNullable(previousRecord),
|
|
||||||
Optional.fromNullable(nextRecord),
|
|
||||||
glideRequests,
|
|
||||||
locale,
|
|
||||||
batchSelected,
|
|
||||||
recipient,
|
|
||||||
searchQuery,
|
|
||||||
messageRecord == recordToPulseHighlight);
|
|
||||||
|
|
||||||
if (messageRecord == recordToPulseHighlight) {
|
|
||||||
recordToPulseHighlight = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType));
|
|
||||||
itemView.setOnClickListener(view -> {
|
|
||||||
if (clickListener != null) {
|
|
||||||
clickListener.onItemClick(itemView.getMessageRecord());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
itemView.setOnLongClickListener(view -> {
|
|
||||||
if (clickListener != null) {
|
|
||||||
clickListener.onItemLongClick(itemView.getMessageRecord());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
itemView.setEventListener(clickListener);
|
|
||||||
Log.d(TAG, "Inflate time: " + (System.currentTimeMillis() - start));
|
|
||||||
return new ViewHolder(itemView);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemViewRecycled(ViewHolder holder) {
|
|
||||||
holder.getView().unbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
private @LayoutRes int getLayoutForViewType(int viewType) {
|
|
||||||
switch (viewType) {
|
|
||||||
case MESSAGE_TYPE_AUDIO_OUTGOING:
|
|
||||||
case MESSAGE_TYPE_THUMBNAIL_OUTGOING:
|
|
||||||
case MESSAGE_TYPE_DOCUMENT_OUTGOING:
|
|
||||||
case MESSAGE_TYPE_INVITATION_OUTGOING:
|
|
||||||
case MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent;
|
|
||||||
case MESSAGE_TYPE_AUDIO_INCOMING:
|
|
||||||
case MESSAGE_TYPE_THUMBNAIL_INCOMING:
|
|
||||||
case MESSAGE_TYPE_DOCUMENT_INCOMING:
|
|
||||||
case MESSAGE_TYPE_INVITATION_INCOMING:
|
|
||||||
case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received;
|
|
||||||
case MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update;
|
|
||||||
default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(@NonNull MessageRecord messageRecord) {
|
|
||||||
if (messageRecord.isUpdate()) {
|
|
||||||
return MESSAGE_TYPE_UPDATE;
|
|
||||||
} else if (messageRecord.isOpenGroupInvitation()) {
|
|
||||||
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_INVITATION_OUTGOING;
|
|
||||||
else return MESSAGE_TYPE_INVITATION_INCOMING;
|
|
||||||
} else if (hasAudio(messageRecord)) {
|
|
||||||
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_AUDIO_OUTGOING;
|
|
||||||
else return MESSAGE_TYPE_AUDIO_INCOMING;
|
|
||||||
} else if (hasDocument(messageRecord)) {
|
|
||||||
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_DOCUMENT_OUTGOING;
|
|
||||||
else return MESSAGE_TYPE_DOCUMENT_INCOMING;
|
|
||||||
} else if (hasThumbnail(messageRecord)) {
|
|
||||||
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_THUMBNAIL_OUTGOING;
|
|
||||||
else return MESSAGE_TYPE_THUMBNAIL_INCOMING;
|
|
||||||
} else if (messageRecord.isOutgoing()) {
|
|
||||||
return MESSAGE_TYPE_OUTGOING;
|
|
||||||
} else {
|
|
||||||
return MESSAGE_TYPE_INCOMING;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isRecordForId(@NonNull MessageRecord record, long id) {
|
|
||||||
return record.getId() == id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(@NonNull Cursor cursor) {
|
|
||||||
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachment(cursor);
|
|
||||||
List<DatabaseAttachment> messageAttachments = Stream.of(attachments).filterNot(DatabaseAttachment::isQuote).toList();
|
|
||||||
|
|
||||||
if (messageAttachments.size() > 0 && messageAttachments.get(0).getFastPreflightId() != null) {
|
|
||||||
return Long.valueOf(messageAttachments.get(0).getFastPreflightId());
|
|
||||||
}
|
|
||||||
|
|
||||||
final String unique = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsColumns.UNIQUE_ROW_ID));
|
|
||||||
final byte[] bytes = digest.digest(unique.getBytes());
|
|
||||||
return Conversions.byteArrayToLong(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected long getItemId(@NonNull MessageRecord record) {
|
|
||||||
if (record.isOutgoing() && record.isMms()) {
|
|
||||||
MmsMessageRecord mmsRecord = (MmsMessageRecord) record;
|
|
||||||
SlideDeck slideDeck = mmsRecord.getSlideDeck();
|
|
||||||
|
|
||||||
if (slideDeck.getThumbnailSlide() != null && slideDeck.getThumbnailSlide().getFastPreflightId() != null) {
|
|
||||||
return Long.valueOf(slideDeck.getThumbnailSlide().getFastPreflightId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return record.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected MessageRecord getRecordFromCursor(@NonNull Cursor cursor) {
|
|
||||||
long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID));
|
|
||||||
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
|
|
||||||
|
|
||||||
final SoftReference<MessageRecord> reference = messageRecordCache.get(type + messageId);
|
|
||||||
if (reference != null) {
|
|
||||||
final MessageRecord record = reference.get();
|
|
||||||
if (record != null) return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
final MessageRecord messageRecord = db.readerFor(cursor).getCurrent();
|
|
||||||
messageRecordCache.put(type + messageId, new SoftReference<>(messageRecord));
|
|
||||||
|
|
||||||
return messageRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
getCursor().close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int findLastSeenPosition(long lastSeen) {
|
|
||||||
if (lastSeen <= 0) return -1;
|
|
||||||
if (!isActiveCursor()) return -1;
|
|
||||||
|
|
||||||
int count = getItemCount() - (hasFooterView() ? 1 : 0);
|
|
||||||
|
|
||||||
for (int i=(hasHeaderView() ? 1 : 0);i<count;i++) {
|
|
||||||
MessageRecord messageRecord = getRecordForPositionOrThrow(i);
|
|
||||||
|
|
||||||
if (messageRecord.isOutgoing() || messageRecord.getDateReceived() <= lastSeen) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void toggleSelection(MessageRecord messageRecord) {
|
|
||||||
if (!batchSelected.remove(messageRecord)) {
|
|
||||||
batchSelected.add(messageRecord);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearSelection() {
|
|
||||||
batchSelected.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<MessageRecord> getSelectedItems() {
|
|
||||||
return Collections.unmodifiableSet(new HashSet<>(batchSelected));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void pulseHighlightItem(int position) {
|
|
||||||
if (position < getItemCount()) {
|
|
||||||
recordToPulseHighlight = getRecordForPositionOrThrow(position);
|
|
||||||
notifyItemChanged(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onSearchQueryUpdated(@Nullable String query) {
|
|
||||||
this.searchQuery = query;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasAudio(MessageRecord messageRecord) {
|
|
||||||
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasDocument(MessageRecord messageRecord) {
|
|
||||||
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasThumbnail(MessageRecord messageRecord) {
|
|
||||||
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getHeaderId(int position) {
|
|
||||||
if (!isActiveCursor()) return -1;
|
|
||||||
if (isHeaderPosition(position)) return -1;
|
|
||||||
if (isFooterPosition(position)) return -1;
|
|
||||||
if (position >= getItemCount()) return -1;
|
|
||||||
if (position < 0) return -1;
|
|
||||||
|
|
||||||
MessageRecord record = getRecordForPositionOrThrow(position);
|
|
||||||
if (record.getRecipient().getAddress().isOpenGroup()) {
|
|
||||||
calendar.setTime(new Date(record.getDateReceived()));
|
|
||||||
} else {
|
|
||||||
calendar.setTime(new Date(record.getDateSent()));
|
|
||||||
}
|
|
||||||
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getReceivedTimestamp(int position) {
|
|
||||||
if (!isActiveCursor()) return 0;
|
|
||||||
if (isHeaderPosition(position)) return 0;
|
|
||||||
if (isFooterPosition(position)) return 0;
|
|
||||||
if (position >= getItemCount()) return 0;
|
|
||||||
if (position < 0) return 0;
|
|
||||||
|
|
||||||
MessageRecord messageRecord = getRecordForPositionOrThrow(position);
|
|
||||||
|
|
||||||
if (messageRecord.isOutgoing()) return 0;
|
|
||||||
else return messageRecord.getDateReceived();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
|
|
||||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public HeaderViewHolder onCreateLastSeenViewHolder(ViewGroup parent) {
|
|
||||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_last_seen, parent, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
|
|
||||||
MessageRecord messageRecord = getRecordForPositionOrThrow(position);
|
|
||||||
long timestamp = messageRecord.getDateReceived();
|
|
||||||
if (recipient.getAddress().isOpenGroup()) { timestamp = messageRecord.getTimestamp(); }
|
|
||||||
viewHolder.setText(DateUtils.getRelativeDate(getContext(), locale, timestamp));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onBindLastSeenViewHolder(HeaderViewHolder viewHolder, int position) {
|
|
||||||
viewHolder.setText(getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
static class LastSeenHeader extends StickyHeaderDecoration {
|
|
||||||
|
|
||||||
private final ConversationAdapter adapter;
|
|
||||||
private final long lastSeenTimestamp;
|
|
||||||
|
|
||||||
LastSeenHeader(ConversationAdapter adapter, long lastSeenTimestamp) {
|
|
||||||
super(adapter, false, false);
|
|
||||||
this.adapter = adapter;
|
|
||||||
this.lastSeenTimestamp = lastSeenTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
|
|
||||||
if (!adapter.isActiveCursor()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastSeenTimestamp <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
long currentRecordTimestamp = adapter.getReceivedTimestamp(position);
|
|
||||||
long previousRecordTimestamp = adapter.getReceivedTimestamp(position + 1);
|
|
||||||
|
|
||||||
return currentRecordTimestamp > lastSeenTimestamp && previousRecordTimestamp < lastSeenTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) {
|
|
||||||
return parent.getLayoutManager().getDecoratedTop(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected HeaderViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
|
|
||||||
HeaderViewHolder viewHolder = adapter.onCreateLastSeenViewHolder(parent);
|
|
||||||
adapter.onBindLastSeenViewHolder(viewHolder, position);
|
|
||||||
|
|
||||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
|
|
||||||
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
|
|
||||||
|
|
||||||
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width);
|
|
||||||
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height);
|
|
||||||
|
|
||||||
viewHolder.itemView.measure(childWidth, childHeight);
|
|
||||||
viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight());
|
|
||||||
|
|
||||||
return viewHolder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,119 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Build.VERSION;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.core.app.ActivityOptionsCompat;
|
|
||||||
import android.view.Display;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.session.libsignal.utilities.ListenableFuture;
|
|
||||||
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class ConversationPopupActivity extends ConversationActivity {
|
|
||||||
|
|
||||||
private static final String TAG = ConversationPopupActivity.class.getSimpleName();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreCreate() {
|
|
||||||
super.onPreCreate();
|
|
||||||
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle bundle, boolean ready) {
|
|
||||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND,
|
|
||||||
WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
|
||||||
|
|
||||||
WindowManager.LayoutParams params = getWindow().getAttributes();
|
|
||||||
params.alpha = 1.0f;
|
|
||||||
params.dimAmount = 0.1f;
|
|
||||||
params.gravity = Gravity.TOP;
|
|
||||||
getWindow().setAttributes(params);
|
|
||||||
|
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
|
||||||
int width = display.getWidth();
|
|
||||||
int height = display.getHeight();
|
|
||||||
|
|
||||||
if (height > width) getWindow().setLayout((int) (width * .85), (int) (height * .5));
|
|
||||||
else getWindow().setLayout((int) (width * .7), (int) (height * .75));
|
|
||||||
|
|
||||||
super.onCreate(bundle, ready);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
composeText.requestFocus();
|
|
||||||
quickAttachmentToggle.disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
if (isFinishing()) overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
|
||||||
MenuInflater inflater = this.getMenuInflater();
|
|
||||||
menu.clear();
|
|
||||||
|
|
||||||
inflater.inflate(R.menu.conversation_popup, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.menu_expand:
|
|
||||||
saveDraft().addListener(new ListenableFuture.Listener<Long>() {
|
|
||||||
@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);
|
|
||||||
|
|
||||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
|
|
||||||
startActivity(intent, transition.toBundle());
|
|
||||||
} else {
|
|
||||||
startActivity(intent);
|
|
||||||
overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
|
|
||||||
}
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(ExecutionException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void initializeActionBar() {
|
|
||||||
super.initializeActionBar();
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void sendComplete(long threadId) {
|
|
||||||
super.sendComplete(threadId);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,146 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
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 org.session.libsession.utilities.Debouncer;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsession.utilities.concurrent.SignalExecutors;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ConversationSearchViewModel extends AndroidViewModel {
|
|
||||||
|
|
||||||
private final SearchRepository searchRepository;
|
|
||||||
private final CloseableLiveData<SearchResult> result;
|
|
||||||
private final Debouncer debouncer;
|
|
||||||
|
|
||||||
private boolean firstSearch;
|
|
||||||
private boolean searchOpen;
|
|
||||||
private String activeQuery;
|
|
||||||
private long activeThreadId;
|
|
||||||
|
|
||||||
public ConversationSearchViewModel(@NonNull Application application) {
|
|
||||||
super(application);
|
|
||||||
Context context = application.getApplicationContext();
|
|
||||||
result = new CloseableLiveData<>();
|
|
||||||
debouncer = new Debouncer(500);
|
|
||||||
searchRepository = new SearchRepository(context,
|
|
||||||
DatabaseFactory.getSearchDatabase(context),
|
|
||||||
DatabaseFactory.getThreadDatabase(context),
|
|
||||||
ContactAccessor.getInstance(),
|
|
||||||
SignalExecutors.SERIAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
LiveData<SearchResult> getSearchResults() {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onQueryUpdated(@NonNull String query, long threadId) {
|
|
||||||
if (firstSearch && query.length() < 2) {
|
|
||||||
result.postValue(new SearchResult(CursorList.emptyList(), 0));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.equals(activeQuery)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateQuery(query, threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMissingResult() {
|
|
||||||
if (activeQuery != null) {
|
|
||||||
updateQuery(activeQuery, activeThreadId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMoveUp() {
|
|
||||||
debouncer.clear();
|
|
||||||
|
|
||||||
CursorList<MessageResult> messages = (CursorList<MessageResult>) result.getValue().getResults();
|
|
||||||
int position = Math.min(result.getValue().getPosition() + 1, messages.size() - 1);
|
|
||||||
|
|
||||||
result.setValue(new SearchResult(messages, position), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMoveDown() {
|
|
||||||
debouncer.clear();
|
|
||||||
|
|
||||||
CursorList<MessageResult> messages = (CursorList<MessageResult>) result.getValue().getResults();
|
|
||||||
int position = Math.max(result.getValue().getPosition() - 1, 0);
|
|
||||||
|
|
||||||
result.setValue(new SearchResult(messages, position), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void onSearchOpened() {
|
|
||||||
searchOpen = true;
|
|
||||||
firstSearch = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onSearchClosed() {
|
|
||||||
searchOpen = false;
|
|
||||||
debouncer.clear();
|
|
||||||
result.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCleared() {
|
|
||||||
super.onCleared();
|
|
||||||
result.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateQuery(@NonNull String query, long threadId) {
|
|
||||||
activeQuery = query;
|
|
||||||
activeThreadId = threadId;
|
|
||||||
|
|
||||||
debouncer.publish(() -> {
|
|
||||||
firstSearch = false;
|
|
||||||
|
|
||||||
searchRepository.query(query, threadId, messages -> {
|
|
||||||
Util.runOnMain(() -> {
|
|
||||||
if (searchOpen && query.equals(activeQuery)) {
|
|
||||||
result.setValue(new SearchResult(messages, 0));
|
|
||||||
} else {
|
|
||||||
messages.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static class SearchResult implements Closeable {
|
|
||||||
|
|
||||||
private final CursorList<MessageResult> results;
|
|
||||||
private final int position;
|
|
||||||
|
|
||||||
SearchResult(CursorList<MessageResult> results, int position) {
|
|
||||||
this.results = results;
|
|
||||||
this.position = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MessageResult> getResults() {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPosition() {
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
results.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,289 +0,0 @@
|
|||||||
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;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
|
|
||||||
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;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
//TODO Remove this class.
|
|
||||||
public class ConversationUpdateItem extends LinearLayout
|
|
||||||
implements RecipientModifiedListener, BindableConversationItem
|
|
||||||
{
|
|
||||||
private static final String TAG = ConversationUpdateItem.class.getSimpleName();
|
|
||||||
|
|
||||||
private Set<MessageRecord> batchSelected;
|
|
||||||
|
|
||||||
private ImageView icon;
|
|
||||||
private TextView title;
|
|
||||||
private TextView body;
|
|
||||||
private TextView date;
|
|
||||||
private Recipient sender;
|
|
||||||
private MessageRecord messageRecord;
|
|
||||||
private Locale locale;
|
|
||||||
|
|
||||||
public ConversationUpdateItem(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConversationUpdateItem(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFinishInflate() {
|
|
||||||
super.onFinishInflate();
|
|
||||||
|
|
||||||
this.icon = findViewById(R.id.conversation_update_icon);
|
|
||||||
this.title = findViewById(R.id.conversation_update_title);
|
|
||||||
this.body = findViewById(R.id.conversation_update_body);
|
|
||||||
this.date = findViewById(R.id.conversation_update_date);
|
|
||||||
|
|
||||||
this.setOnClickListener(new InternalClickListener(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bind(@NonNull MessageRecord messageRecord,
|
|
||||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
|
||||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
|
||||||
@NonNull GlideRequests glideRequests,
|
|
||||||
@NonNull Locale locale,
|
|
||||||
@NonNull Set<MessageRecord> batchSelected,
|
|
||||||
@NonNull Recipient conversationRecipient,
|
|
||||||
@Nullable String searchQuery,
|
|
||||||
boolean pulseUpdate)
|
|
||||||
{
|
|
||||||
this.batchSelected = batchSelected;
|
|
||||||
|
|
||||||
bind(messageRecord, locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEventListener(@Nullable EventListener listener) {
|
|
||||||
// No events to report yet
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MessageRecord getMessageRecord() {
|
|
||||||
return messageRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void bind(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
|
||||||
this.messageRecord = messageRecord;
|
|
||||||
this.sender = messageRecord.getIndividualRecipient();
|
|
||||||
this.locale = locale;
|
|
||||||
|
|
||||||
this.sender.addListener(this);
|
|
||||||
|
|
||||||
if (messageRecord.isGroupAction()) setGroupRecord(messageRecord);
|
|
||||||
else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
|
|
||||||
else if (messageRecord.isJoined()) setJoinedRecord(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 throw new AssertionError("Neither group nor log nor joined.");
|
|
||||||
|
|
||||||
if (batchSelected.contains(messageRecord)) setSelected(true);
|
|
||||||
else setSelected(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCallRecord(MessageRecord messageRecord) {
|
|
||||||
if (messageRecord.isIncomingCall()) icon.setImageResource(R.drawable.ic_call_received_grey600_24dp);
|
|
||||||
else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp);
|
|
||||||
else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp);
|
|
||||||
|
|
||||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
|
||||||
date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateReceived()));
|
|
||||||
|
|
||||||
title.setVisibility(GONE);
|
|
||||||
body.setVisibility(VISIBLE);
|
|
||||||
date.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setTimerRecord(final MessageRecord messageRecord) {
|
|
||||||
@ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getContext().getTheme());
|
|
||||||
if (messageRecord.getExpiresIn() > 0) {
|
|
||||||
icon.setImageResource(R.drawable.ic_timer);
|
|
||||||
icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
|
|
||||||
} else {
|
|
||||||
icon.setImageResource(R.drawable.ic_timer_disabled);
|
|
||||||
icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
|
|
||||||
}
|
|
||||||
|
|
||||||
title.setText(ExpirationUtil.getExpirationDisplayValue(getContext(), (int)(messageRecord.getExpiresIn() / 1000)));
|
|
||||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
|
||||||
|
|
||||||
title.setVisibility(VISIBLE);
|
|
||||||
body.setVisibility(VISIBLE);
|
|
||||||
date.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setDataExtractionRecord(final MessageRecord messageRecord, DataExtractionNotificationInfoMessage.Kind kind) {
|
|
||||||
@ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getContext().getTheme());
|
|
||||||
if (kind == DataExtractionNotificationInfoMessage.Kind.SCREENSHOT) {
|
|
||||||
icon.setImageResource(R.drawable.quick_camera_dark);
|
|
||||||
} else if (kind == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) {
|
|
||||||
icon.setImageResource(R.drawable.ic_file_download_white_36dp);
|
|
||||||
}
|
|
||||||
icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
|
|
||||||
|
|
||||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
|
||||||
|
|
||||||
title.setVisibility(VISIBLE);
|
|
||||||
body.setVisibility(VISIBLE);
|
|
||||||
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()));
|
|
||||||
|
|
||||||
icon.setVisibility(GONE);
|
|
||||||
title.setVisibility(GONE);
|
|
||||||
body.setVisibility(VISIBLE);
|
|
||||||
date.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onModified(Recipient recipient) {
|
|
||||||
Util.runOnMain(() -> bind(messageRecord, locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setOnClickListener(View.OnClickListener l) {
|
|
||||||
super.setOnClickListener(new InternalClickListener(l));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unbind() {
|
|
||||||
if (sender != null) {
|
|
||||||
sender.removeListener(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class InternalClickListener implements View.OnClickListener {
|
|
||||||
|
|
||||||
@Nullable private final View.OnClickListener parent;
|
|
||||||
|
|
||||||
InternalClickListener(@Nullable View.OnClickListener parent) {
|
|
||||||
this.parent = parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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,143 @@
|
|||||||
|
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.viewHolderIndex = position
|
||||||
|
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.util.disableClipping
|
||||||
|
import org.thoughtcrime.securesms.util.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,189 @@
|
|||||||
|
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.messaging.jobs.AttachmentDownloadJob
|
||||||
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
|
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.util.ActivityDispatcher
|
||||||
|
import org.thoughtcrime.securesms.longmessage.LongMessageActivity
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
|
import org.thoughtcrime.securesms.video.exo.AttachmentDataSource
|
||||||
|
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.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) {
|
||||||
|
// restart download here
|
||||||
|
(slide.asAttachment() as? DatabaseAttachment)?.let { attachment ->
|
||||||
|
val attachmentId = attachment.attachmentId.rowId
|
||||||
|
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mms.getId()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 android.content.Context;
|
||||||
import androidx.annotation.NonNull;
|
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));
|
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.util.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.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@ -8,7 +8,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.BaseAdapter
|
import android.widget.BaseAdapter
|
||||||
import android.widget.ListView
|
import android.widget.ListView
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.session.libsession.messaging.mentions.Mention
|
import org.session.libsession.messaging.mentions.Mention
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.views
|
package org.thoughtcrime.securesms.conversation.v2.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@ -30,7 +30,7 @@ class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun update() {
|
private fun update() {
|
||||||
btnGroupNameDisplay.text = mentionCandidate.displayName
|
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
||||||
profilePictureView.publicKey = mentionCandidate.publicKey
|
profilePictureView.publicKey = mentionCandidate.publicKey
|
||||||
profilePictureView.displayName = mentionCandidate.displayName
|
profilePictureView.displayName = mentionCandidate.displayName
|
||||||
profilePictureView.additionalPublicKey = null
|
profilePictureView.additionalPublicKey = null
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.views
|
package org.thoughtcrime.securesms.conversation.v2.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@ -7,30 +7,22 @@ import android.view.LayoutInflater
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import kotlinx.android.synthetic.main.view_open_group_guidelines.view.*
|
import kotlinx.android.synthetic.main.view_open_group_guidelines.view.*
|
||||||
import network.loki.messenger.R
|
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.groups.OpenGroupGuidelinesActivity
|
||||||
import org.thoughtcrime.securesms.loki.utilities.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
|
|
||||||
class OpenGroupGuidelinesView : FrameLayout {
|
class OpenGroupGuidelinesView : FrameLayout {
|
||||||
|
|
||||||
constructor(context: Context) : super(context) {
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
setUpViewHierarchy()
|
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) {
|
private fun initialize() {
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUpViewHierarchy() {
|
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null)
|
val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null)
|
||||||
addView(contentView)
|
addView(contentView)
|
||||||
readButton.setOnClickListener {
|
readButton.setOnClickListener {
|
||||||
val activity = context as ConversationActivity
|
val activity = context as ConversationActivityV2
|
||||||
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
|
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
|
||||||
activity.push(intent)
|
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.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
@ -13,18 +13,14 @@ import android.widget.LinearLayout;
|
|||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class TypingIndicatorView extends LinearLayout {
|
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 CYCLE_DURATION = 1500;
|
||||||
private static final long DOT_DURATION = 600;
|
private static final long DOT_DURATION = 600;
|
||||||
private static final float MIN_ALPHA = 0.4f;
|
private static final float MIN_ALPHA = 0.4f;
|
||||||
private static final float MIN_SCALE = 0.75f;
|
private static final float MIN_SCALE = 0.75f;
|
||||||
|
|
||||||
private boolean isActive;
|
|
||||||
private long startTime;
|
|
||||||
|
|
||||||
private View dot1;
|
private View dot1;
|
||||||
private View dot2;
|
private View dot2;
|
||||||
private View dot3;
|
private View dot3;
|
||||||
@ -40,7 +36,7 @@ public class TypingIndicatorView extends LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initialize(@Nullable AttributeSet attrs) {
|
private void initialize(@Nullable AttributeSet attrs) {
|
||||||
inflate(getContext(), R.layout.typing_indicator_view, this);
|
inflate(getContext(), R.layout.view_typing_indicator, this);
|
||||||
|
|
||||||
setWillNotDraw(false);
|
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,49 @@
|
|||||||
|
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.messaging.jobs.AttachmentDownloadJob
|
||||||
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
|
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() {
|
||||||
|
val contactDB = DatabaseFactory.getSessionContactDatabase(requireContext())
|
||||||
|
val sessionID = recipient.address.toString()
|
||||||
|
val contact = contactDB.getContactWithSessionID(sessionID) ?: return
|
||||||
|
val threadID = DatabaseFactory.getThreadDatabase(requireContext()).getThreadIdIfExistsFor(recipient)
|
||||||
|
contactDB.setContactIsTrusted(contact, true, threadID)
|
||||||
|
JobQueue.shared.resumePendingJobs(AttachmentDownloadJob.KEY)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
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.utilities.OpenGroupUrlParser
|
||||||
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||||
|
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||||
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
|
|
||||||
|
/** 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)
|
||||||
|
ConfigurationMessageUtilities.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.util.toDp
|
||||||
|
import org.thoughtcrime.securesms.util.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, false, 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,174 @@
|
|||||||
|
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.util.*
|
||||||
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
|
import org.thoughtcrime.securesms.util.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.util.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.util.animateSizeChange
|
||||||
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
|
import org.thoughtcrime.securesms.util.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.util.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,102 @@
|
|||||||
|
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 thread = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(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 =
|
||||||
|
(thread.isGroupRecipient && 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.groups.EditClosedGroupActivity
|
||||||
|
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
|
||||||
|
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
||||||
|
import org.thoughtcrime.securesms.util.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.util.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,197 @@
|
|||||||
|
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 android.widget.RelativeLayout
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
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.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import org.thoughtcrime.securesms.util.*
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
|
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,
|
||||||
|
isOriginalMissing: Boolean, 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()) && !isOriginalMissing
|
||||||
|
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 if (attachments != null) {
|
||||||
|
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,55 @@
|
|||||||
|
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.core.content.ContextCompat
|
||||||
|
import kotlinx.android.synthetic.main.view_untrusted_attachment.view.*
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
|
||||||
|
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class UntrustedAttachmentView: LinearLayout {
|
||||||
|
|
||||||
|
enum class AttachmentType {
|
||||||
|
AUDIO,
|
||||||
|
DOCUMENT,
|
||||||
|
MEDIA
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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_untrusted_attachment, this)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Updating
|
||||||
|
fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int) {
|
||||||
|
val (iconRes, stringRes) = when (attachmentType) {
|
||||||
|
AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.Slide_audio
|
||||||
|
AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.document
|
||||||
|
AttachmentType.MEDIA -> R.drawable.ic_image_white_24dp to R.string.media
|
||||||
|
}
|
||||||
|
val iconDrawable = ContextCompat.getDrawable(context,iconRes)!!
|
||||||
|
iconDrawable.mutate().setTint(textColor)
|
||||||
|
val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT))
|
||||||
|
|
||||||
|
untrustedAttachmentIcon.setImageDrawable(iconDrawable)
|
||||||
|
untrustedAttachmentTitle.text = text
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Interaction
|
||||||
|
fun showTrustDialog(recipient: Recipient) {
|
||||||
|
ActivityDispatcher.get(context)?.showDialog(DownloadDialog(recipient))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,252 @@
|
|||||||
|
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.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 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.ConversationActivityV2
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||||
|
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.util.*
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
|
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
|
||||||
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class VisibleMessageContentView : LinearLayout {
|
||||||
|
var onContentClick: ((event: MotionEvent) -> Unit)? = null
|
||||||
|
var onContentDoubleTap: (() -> Unit)? = null
|
||||||
|
var delegate: VisibleMessageContentViewDelegate? = null
|
||||||
|
var viewHolderIndex: Int = -1
|
||||||
|
|
||||||
|
// 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?, contactIsTrusted: Boolean) {
|
||||||
|
// 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 - 2
|
||||||
|
// times the horizontal margin. This unfortunately has to be calculated manually
|
||||||
|
// here to get the layout right.
|
||||||
|
val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - 2 * toPx(16, resources)).roundToInt()
|
||||||
|
val quoteText = if (quote.isOriginalMissing) {
|
||||||
|
context.getString(R.string.QuoteView_original_missing)
|
||||||
|
} else {
|
||||||
|
quote.text
|
||||||
|
}
|
||||||
|
quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
|
||||||
|
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId,
|
||||||
|
quote.isOriginalMissing, 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) {
|
||||||
|
// Audio attachment
|
||||||
|
if (contactIsTrusted || message.isOutgoing) {
|
||||||
|
val voiceMessageView = VoiceMessageView(context)
|
||||||
|
voiceMessageView.index = viewHolderIndex
|
||||||
|
voiceMessageView.delegate = context as? ConversationActivityV2
|
||||||
|
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 {
|
||||||
|
val untrustedView = UntrustedAttachmentView(context)
|
||||||
|
untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
|
||||||
|
mainContainer.addView(untrustedView)
|
||||||
|
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
||||||
|
}
|
||||||
|
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
|
||||||
|
// Document attachment
|
||||||
|
if (contactIsTrusted || message.isOutgoing) {
|
||||||
|
val documentView = DocumentView(context)
|
||||||
|
documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||||
|
mainContainer.addView(documentView)
|
||||||
|
} else {
|
||||||
|
val untrustedView = UntrustedAttachmentView(context)
|
||||||
|
untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
|
||||||
|
mainContainer.addView(untrustedView)
|
||||||
|
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
||||||
|
}
|
||||||
|
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
|
||||||
|
// Images/Video attachment
|
||||||
|
if (contactIsTrusted || message.isOutgoing) {
|
||||||
|
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 {
|
||||||
|
val untrustedView = UntrustedAttachmentView(context)
|
||||||
|
untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
||||||
|
mainContainer.addView(untrustedView)
|
||||||
|
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
||||||
|
}
|
||||||
|
} 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,366 @@
|
|||||||
|
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.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.util.*
|
||||||
|
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 viewHolderIndex: Int = -1
|
||||||
|
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) ?: return
|
||||||
|
val contactDB = DatabaseFactory.getSessionContactDatabase(context)
|
||||||
|
val contact = contactDB.getContactWithSessionID(senderSessionID)
|
||||||
|
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) ?: return
|
||||||
|
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 = contact?.displayName(context) ?: senderSessionID
|
||||||
|
} else {
|
||||||
|
profilePictureContainer.visibility = View.GONE
|
||||||
|
senderNameTextView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
// Date break
|
||||||
|
val showDateBreak = (previous == null || !DateUtils.isSameHour(message.timestamp, previous.timestamp))
|
||||||
|
dateBreakTextView.isVisible = showDateBreak
|
||||||
|
dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else ""
|
||||||
|
// Timestamp
|
||||||
|
messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(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.viewHolderIndex = viewHolderIndex
|
||||||
|
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false))
|
||||||
|
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.isSameHour(current.timestamp, next.timestamp)
|
||||||
|
|| current.recipient.address != next.recipient.address
|
||||||
|
} else {
|
||||||
|
next == null || next.isUpdate || !DateUtils.isSameHour(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,142 @@
|
|||||||
|
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.LayoutInflater
|
||||||
|
import android.view.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.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
|
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.DatabaseFactory
|
||||||
|
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
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
renderIcon()
|
||||||
|
}
|
||||||
|
private var progress = 0.0
|
||||||
|
private var duration = 0L
|
||||||
|
private var player: AudioSlidePlayer? = null
|
||||||
|
var delegate: VoiceMessageViewDelegate? = null
|
||||||
|
var index = -1
|
||||||
|
|
||||||
|
// 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!!
|
||||||
|
voiceMessageViewLoader.isVisible = audio.isInProgress
|
||||||
|
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])
|
||||||
|
|
||||||
|
// only process audio if downloaded
|
||||||
|
if (audio.isPendingDownload || audio.isInProgress) {
|
||||||
|
this.player = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val player = AudioSlidePlayer.createFor(context, audio, this)
|
||||||
|
this.player = player
|
||||||
|
|
||||||
|
(audio.asAttachment() as? DatabaseAttachment)?.let { attachment ->
|
||||||
|
DatabaseFactory.getAttachmentDatabase(context).getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras ->
|
||||||
|
if (audioExtras.durationMs > 0) {
|
||||||
|
duration = audioExtras.durationMs
|
||||||
|
voiceMessageViewDurationTextView.visibility = View.VISIBLE
|
||||||
|
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||||
|
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
|
||||||
|
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayerStart(player: AudioSlidePlayer) {}
|
||||||
|
|
||||||
|
override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, unused: Long) {
|
||||||
|
if (progress == 1.0) {
|
||||||
|
togglePlayback()
|
||||||
|
handleProgressChanged(0.0)
|
||||||
|
delegate?.playNextAudioIfPossible(index)
|
||||||
|
} 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) {
|
||||||
|
Log.d("Loki", "Player stopped")
|
||||||
|
isPlaying = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchDraw(canvas: Canvas) {
|
||||||
|
super.dispatchDraw(canvas)
|
||||||
|
cornerMask.mask(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderIcon() {
|
||||||
|
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
|
||||||
|
voiceMessagePlaybackImageView.setImageResource(iconID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Interaction
|
||||||
|
fun togglePlayback() {
|
||||||
|
val player = this.player ?: return
|
||||||
|
isPlaying = !isPlaying
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VoiceMessageViewDelegate {
|
||||||
|
|
||||||
|
fun playNextAudioIfPossible(current: Int)
|
||||||
|
}
|
@ -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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* 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.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@ -23,29 +23,31 @@ import android.content.ActivityNotFoundException;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.provider.ContactsContract;
|
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
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.session.libsignal.utilities.NoExternalStorageException;
|
||||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
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.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.session.libsignal.utilities.ExternalStorageUtil;
|
import org.session.libsignal.utilities.ExternalStorageUtil;
|
||||||
@ -53,13 +55,8 @@ import org.thoughtcrime.securesms.util.FileProviderUtil;
|
|||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
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.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;
|
||||||
import org.session.libsignal.utilities.ListenableFuture.Listener;
|
|
||||||
import org.session.libsignal.utilities.SettableFuture;
|
import org.session.libsignal.utilities.SettableFuture;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -67,26 +64,18 @@ import java.io.IOException;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
import static android.provider.MediaStore.EXTRA_OUTPUT;
|
import static android.provider.MediaStore.EXTRA_OUTPUT;
|
||||||
|
|
||||||
|
|
||||||
public class AttachmentManager {
|
public class AttachmentManager {
|
||||||
|
|
||||||
private final static String TAG = AttachmentManager.class.getSimpleName();
|
private final static String TAG = AttachmentManager.class.getSimpleName();
|
||||||
|
|
||||||
private final @NonNull Context context;
|
private final @NonNull Context context;
|
||||||
private final @NonNull Stub<View> attachmentViewStub;
|
|
||||||
private final @NonNull AttachmentListener attachmentListener;
|
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 List<Uri> garbage = new LinkedList<>();
|
||||||
private @NonNull Optional<Slide> slide = Optional.absent();
|
private @NonNull Optional<Slide> slide = Optional.absent();
|
||||||
private @Nullable Uri captureUri;
|
private @Nullable Uri captureUri;
|
||||||
@ -94,51 +83,12 @@ public class AttachmentManager {
|
|||||||
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
|
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
|
||||||
this.context = activity;
|
this.context = activity;
|
||||||
this.attachmentListener = listener;
|
this.attachmentListener = listener;
|
||||||
this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void inflateStub() {
|
public void clear() {
|
||||||
if (!attachmentViewStub.resolved()) {
|
markGarbage(getSlideUri());
|
||||||
View root = attachmentViewStub.get();
|
slide = Optional.absent();
|
||||||
|
attachmentListener.onAttachmentChanged();
|
||||||
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 cleanup() {
|
public void cleanup() {
|
||||||
@ -190,16 +140,12 @@ public class AttachmentManager {
|
|||||||
final int width,
|
final int width,
|
||||||
final int height)
|
final int height)
|
||||||
{
|
{
|
||||||
inflateStub();
|
|
||||||
|
|
||||||
final SettableFuture<Boolean> result = new SettableFuture<>();
|
final SettableFuture<Boolean> result = new SettableFuture<>();
|
||||||
|
|
||||||
new AsyncTask<Void, Void, Slide>() {
|
new AsyncTask<Void, Void, Slide>() {
|
||||||
@Override
|
@Override
|
||||||
protected void onPreExecute() {
|
protected void onPreExecute() {
|
||||||
thumbnail.clear(glideRequests);
|
|
||||||
thumbnail.showProgressSpinner();
|
|
||||||
attachmentViewStub.get().setVisibility(View.VISIBLE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -222,35 +168,12 @@ public class AttachmentManager {
|
|||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(@Nullable final Slide slide) {
|
protected void onPostExecute(@Nullable final Slide slide) {
|
||||||
if (slide == null) {
|
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);
|
result.set(false);
|
||||||
} else if (!areConstraintsSatisfied(context, slide, constraints)) {
|
} 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);
|
result.set(false);
|
||||||
} else {
|
} else {
|
||||||
setSlide(slide);
|
setSlide(slide);
|
||||||
attachmentViewStub.get().setVisibility(View.VISIBLE);
|
result.set(true);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
attachmentListener.onAttachmentChanged();
|
attachmentListener.onAttachmentChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -317,11 +240,8 @@ public class AttachmentManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAttachmentPresent() {
|
public @NonNull
|
||||||
return attachmentViewStub.resolved() && attachmentViewStub.get().getVisibility() == View.VISIBLE;
|
SlideDeck buildSlideDeck() {
|
||||||
}
|
|
||||||
|
|
||||||
public @NonNull SlideDeck buildSlideDeck() {
|
|
||||||
SlideDeck deck = new SlideDeck();
|
SlideDeck deck = new SlideDeck();
|
||||||
if (slide.isPresent()) deck.addSlide(slide.get());
|
if (slide.isPresent()) deck.addSlide(slide.get());
|
||||||
return deck;
|
return deck;
|
||||||
@ -333,43 +253,16 @@ public class AttachmentManager {
|
|||||||
|
|
||||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
.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))
|
.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))
|
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void selectAudio(Activity activity, int requestCode) {
|
public static void selectAudio(Activity activity, int requestCode) {
|
||||||
selectMediaType(activity, "audio/*", null, 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) {
|
public static void selectGif(Activity activity, int requestCode) {
|
||||||
Intent intent = new Intent(activity, GiphyActivity.class);
|
Intent intent = new Intent(activity, GiphyActivity.class);
|
||||||
intent.putExtra(GiphyActivity.EXTRA_IS_MMS, false);
|
intent.putExtra(GiphyActivity.EXTRA_IS_MMS, false);
|
||||||
@ -386,28 +279,25 @@ public class AttachmentManager {
|
|||||||
|
|
||||||
public void capturePhoto(Activity activity, int requestCode) {
|
public void capturePhoto(Activity activity, int requestCode) {
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.CAMERA)
|
.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))
|
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
try {
|
try {
|
||||||
File captureFile = File.createTempFile(
|
File captureFile = File.createTempFile("conversation-capture", ".jpg", ExternalStorageUtil.getImageDir(activity));
|
||||||
"conversation-capture",
|
Uri captureUri = FileProviderUtil.getUriFor(context, captureFile);
|
||||||
".jpg",
|
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||||
ExternalStorageUtil.getImageDir(activity));
|
captureIntent.putExtra(EXTRA_OUTPUT, captureUri);
|
||||||
Uri captureUri = FileProviderUtil.getUriFor(context, captureFile);
|
captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||||
captureIntent.putExtra(EXTRA_OUTPUT, captureUri);
|
Log.d(TAG, "captureUri path is " + captureUri.getPath());
|
||||||
captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
this.captureUri = captureUri;
|
||||||
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
activity.startActivityForResult(captureIntent, requestCode);
|
||||||
Log.d(TAG, "captureUri path is " + captureUri.getPath());
|
}
|
||||||
this.captureUri = captureUri;
|
} catch (IOException | NoExternalStorageException e) {
|
||||||
activity.startActivityForResult(captureIntent, requestCode);
|
throw new RuntimeException("Error creating image capture intent.", e);
|
||||||
}
|
}
|
||||||
} catch (IOException | NoExternalStorageException e) {
|
})
|
||||||
throw new RuntimeException("Error creating image capture intent.", e);
|
.execute();
|
||||||
}
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
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());
|
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 {
|
public interface AttachmentListener {
|
||||||
void onAttachmentChanged();
|
void onAttachmentChanged();
|
||||||
}
|
}
|
||||||
@ -513,6 +375,5 @@ public class AttachmentManager {
|
|||||||
|
|
||||||
return DOCUMENT;
|
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.util.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,196 @@
|
|||||||
|
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 }
|
||||||
|
val downloadIndicator: View by lazy { thumbnail_download_icon }
|
||||||
|
|
||||||
|
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
|
||||||
|
downloadIndicator.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.utilities
|
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.utilities
|
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
@ -7,11 +7,13 @@ import android.text.SpannableString
|
|||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import android.util.Range
|
import android.util.Range
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import nl.komponents.kovenant.combine.Tuple2
|
import nl.komponents.kovenant.combine.Tuple2
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
object MentionUtilities {
|
object MentionUtilities {
|
||||||
@ -23,7 +25,7 @@ object MentionUtilities {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString {
|
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 threadDB = DatabaseFactory.getThreadDatabase(context)
|
||||||
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
|
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
|
||||||
val pattern = Pattern.compile("@[0-9a-fA-F]*")
|
val pattern = Pattern.compile("@[0-9a-fA-F]*")
|
||||||
@ -38,7 +40,7 @@ object MentionUtilities {
|
|||||||
TextSecurePreferences.getProfileName(context)
|
TextSecurePreferences.getProfileName(context)
|
||||||
} else {
|
} else {
|
||||||
val contact = DatabaseFactory.getSessionContactDatabase(context).getContactWithSessionID(publicKey)
|
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)
|
contact?.displayName(context)
|
||||||
}
|
}
|
||||||
if (userDisplayName != null) {
|
if (userDisplayName != null) {
|
||||||
@ -54,10 +56,15 @@ object MentionUtilities {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val result = SpannableString(text)
|
val result = SpannableString(text)
|
||||||
|
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||||
for (mention in mentions) {
|
for (mention in mentions) {
|
||||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
val colorID = if (isOutgoingMessage) {
|
||||||
val colorID = if (isLightMode && isOutgoingMessage) R.color.black else R.color.accent
|
if (isLightMode) R.color.white else R.color.black
|
||||||
result.setSpan(ForegroundColorSpan(context.resources.getColorWithID(colorID, context.theme)), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
} 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)
|
result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
@ -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.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
@ -24,6 +24,9 @@ import com.bumptech.glide.request.RequestOptions;
|
|||||||
|
|
||||||
import network.loki.messenger.R;
|
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.DecryptableStreamUriLoader.DecryptableUri;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
@ -54,7 +57,6 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
|
|
||||||
private ImageView image;
|
private ImageView image;
|
||||||
private View playOverlay;
|
private View playOverlay;
|
||||||
private View captionIcon;
|
|
||||||
private View loadIndicator;
|
private View loadIndicator;
|
||||||
private OnClickListener parentClickListener;
|
private OnClickListener parentClickListener;
|
||||||
|
|
||||||
@ -67,7 +69,7 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
private SlidesClickedListener downloadClickListener = null;
|
private SlidesClickedListener downloadClickListener = null;
|
||||||
private Slide slide = null;
|
private Slide slide = null;
|
||||||
|
|
||||||
private int radius;
|
public int radius;
|
||||||
|
|
||||||
public ThumbnailView(Context context) {
|
public ThumbnailView(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@ -84,7 +86,6 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
|
|
||||||
this.image = findViewById(R.id.thumbnail_image);
|
this.image = findViewById(R.id.thumbnail_image);
|
||||||
this.playOverlay = findViewById(R.id.play_overlay);
|
this.playOverlay = findViewById(R.id.play_overlay);
|
||||||
this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
|
|
||||||
this.loadIndicator = findViewById(R.id.thumbnail_load_indicator);
|
this.loadIndicator = findViewById(R.id.thumbnail_load_indicator);
|
||||||
super.setOnClickListener(new ThumbnailClickDispatcher());
|
super.setOnClickListener(new ThumbnailClickDispatcher());
|
||||||
|
|
||||||
@ -94,10 +95,10 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0);
|
bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0);
|
||||||
bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0);
|
bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0);
|
||||||
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 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();
|
typedArray.recycle();
|
||||||
} else {
|
} 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.slide = slide;
|
||||||
|
|
||||||
this.captionIcon.setVisibility(slide.getCaption().isPresent() ? VISIBLE : GONE);
|
|
||||||
|
|
||||||
dimens[WIDTH] = naturalWidth;
|
dimens[WIDTH] = naturalWidth;
|
||||||
dimens[HEIGHT] = naturalHeight;
|
dimens[HEIGHT] = naturalHeight;
|
||||||
invalidate();
|
invalidate();
|
||||||
@ -398,6 +397,7 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class ThumbnailClickDispatcher implements View.OnClickListener {
|
private class ThumbnailClickDispatcher implements View.OnClickListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (thumbnailClickListener != null &&
|
if (thumbnailClickListener != null &&
|
||||||
@ -413,9 +413,9 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class DownloadClickDispatcher implements View.OnClickListener {
|
private class DownloadClickDispatcher implements View.OnClickListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
Log.i(TAG, "onClick() for download button");
|
|
||||||
if (downloadClickListener != null && slide != null) {
|
if (downloadClickListener != null && slide != null) {
|
||||||
downloadClickListener.onClick(view, Collections.singletonList(slide));
|
downloadClickListener.onClick(view, Collections.singletonList(slide));
|
||||||
} else {
|
} else {
|
@ -15,21 +15,22 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.session.libsession.utilities;
|
package org.thoughtcrime.securesms.crypto;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.SharedPreferences.Editor;
|
import android.content.SharedPreferences.Editor;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.session.libsignal.crypto.ecc.ECPublicKey;
|
|
||||||
import org.session.libsignal.crypto.IdentityKey;
|
import org.session.libsignal.crypto.IdentityKey;
|
||||||
import org.session.libsignal.crypto.IdentityKeyPair;
|
import org.session.libsignal.crypto.IdentityKeyPair;
|
||||||
import org.session.libsignal.exceptions.InvalidKeyException;
|
|
||||||
import org.session.libsignal.crypto.ecc.Curve;
|
import org.session.libsignal.crypto.ecc.Curve;
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair;
|
import org.session.libsignal.crypto.ecc.ECKeyPair;
|
||||||
import org.session.libsignal.crypto.ecc.ECPrivateKey;
|
import org.session.libsignal.crypto.ecc.ECPrivateKey;
|
||||||
|
import org.session.libsignal.crypto.ecc.ECPublicKey;
|
||||||
|
import org.session.libsignal.exceptions.InvalidKeyException;
|
||||||
import org.session.libsignal.utilities.Base64;
|
import org.session.libsignal.utilities.Base64;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -45,19 +46,41 @@ public class IdentityKeyUtil {
|
|||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
|
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
|
||||||
|
private static final String ENCRYPTED_SUFFIX = "_encrypted";
|
||||||
|
|
||||||
public static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
|
public static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
|
||||||
public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
|
public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
|
||||||
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
|
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
|
||||||
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
|
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
|
||||||
public static final String LOKI_SEED = "loki_seed";
|
public static final String LOKI_SEED = "loki_seed";
|
||||||
|
public static final String HAS_MIGRATED_KEY = "has_migrated_keys";
|
||||||
|
|
||||||
|
private static SharedPreferences getSharedPreferences(Context context) {
|
||||||
|
return context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean hasIdentityKey(Context context) {
|
public static boolean hasIdentityKey(Context context) {
|
||||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
SharedPreferences preferences = getSharedPreferences(context);
|
||||||
|
|
||||||
return
|
return (preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
|
||||||
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
|
preferences.contains(IDENTITY_PRIVATE_KEY_PREF))
|
||||||
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
|
|| (preferences.contains(IDENTITY_PUBLIC_KEY_PREF+ENCRYPTED_SUFFIX) &&
|
||||||
|
preferences.contains(IDENTITY_PRIVATE_KEY_PREF+ENCRYPTED_SUFFIX));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void checkUpdate(Context context) {
|
||||||
|
SharedPreferences preferences = getSharedPreferences(context);
|
||||||
|
// check if any keys are not migrated
|
||||||
|
if (hasIdentityKey(context) && !preferences.getBoolean(HAS_MIGRATED_KEY, false)) {
|
||||||
|
// this will retrieve and force upgrade if possible
|
||||||
|
// retrieve will force upgrade if available
|
||||||
|
retrieve(context,IDENTITY_PUBLIC_KEY_PREF);
|
||||||
|
retrieve(context,IDENTITY_PRIVATE_KEY_PREF);
|
||||||
|
retrieve(context,ED25519_PUBLIC_KEY);
|
||||||
|
retrieve(context,ED25519_SECRET_KEY);
|
||||||
|
retrieve(context,LOKI_SEED);
|
||||||
|
preferences.edit().putBoolean(HAS_MIGRATED_KEY, true).apply();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
|
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
|
||||||
@ -94,14 +117,56 @@ public class IdentityKeyUtil {
|
|||||||
|
|
||||||
public static String retrieve(Context context, String key) {
|
public static String retrieve(Context context, String key) {
|
||||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||||
return preferences.getString(key, null);
|
|
||||||
|
String unencryptedSecret = preferences.getString(key, null);
|
||||||
|
String encryptedSecret = preferences.getString(key+ENCRYPTED_SUFFIX, null);
|
||||||
|
|
||||||
|
if (unencryptedSecret != null) return getUnencryptedSecret(key, unencryptedSecret, context);
|
||||||
|
else if (encryptedSecret != null) return getEncryptedSecret(encryptedSecret);
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String getUnencryptedSecret(String key, String unencryptedSecret, Context context) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||||
|
return unencryptedSecret;
|
||||||
|
} else {
|
||||||
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(unencryptedSecret.getBytes());
|
||||||
|
|
||||||
|
// save the encrypted suffix secret "key_encrypted"
|
||||||
|
save(context,key+ENCRYPTED_SUFFIX,encryptedSecret.serialize());
|
||||||
|
// delete the regular secret "key"
|
||||||
|
delete(context,key);
|
||||||
|
|
||||||
|
return unencryptedSecret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getEncryptedSecret(String encryptedSecret) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||||
|
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
|
||||||
|
} else {
|
||||||
|
KeyStoreHelper.SealedData sealedData = KeyStoreHelper.SealedData.fromString(encryptedSecret);
|
||||||
|
return new String(KeyStoreHelper.unseal(sealedData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void save(Context context, String key, String value) {
|
public static void save(Context context, String key, String value) {
|
||||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||||
Editor preferencesEditor = preferences.edit();
|
Editor preferencesEditor = preferences.edit();
|
||||||
|
|
||||||
preferencesEditor.putString(key, value);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
boolean isEncryptedSuffix = key.endsWith(ENCRYPTED_SUFFIX);
|
||||||
|
if (isEncryptedSuffix) {
|
||||||
|
preferencesEditor.putString(key, value);
|
||||||
|
} else {
|
||||||
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(value.getBytes());
|
||||||
|
preferencesEditor.putString(key+ENCRYPTED_SUFFIX, encryptedSecret.serialize());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
preferencesEditor.putString(key, value);
|
||||||
|
}
|
||||||
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
|
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package org.session.libsession.utilities
|
package org.thoughtcrime.securesms.crypto
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.goterl.lazysodium.LazySodiumAndroid
|
import com.goterl.lazysodium.LazySodiumAndroid
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.utilities
|
package org.thoughtcrime.securesms.crypto
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -44,8 +44,8 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt
|
|||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras;
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras;
|
||||||
import org.session.libsession.utilities.MediaTypes;
|
import org.session.libsession.utilities.MediaTypes;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
import org.session.libsignal.utilities.JsonUtil;
|
|
||||||
import org.session.libsignal.utilities.ExternalStorageUtil;
|
import org.session.libsignal.utilities.ExternalStorageUtil;
|
||||||
|
import org.session.libsignal.utilities.JsonUtil;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
||||||
@ -820,7 +820,7 @@ public class AttachmentDatabase extends Database {
|
|||||||
* @return true if the update operation was successful.
|
* @return true if the update operation was successful.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras) {
|
public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras, long threadId) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(AUDIO_VISUAL_SAMPLES, extras.getVisualSamples());
|
values.put(AUDIO_VISUAL_SAMPLES, extras.getVisualSamples());
|
||||||
values.put(AUDIO_DURATION, extras.getDurationMs());
|
values.put(AUDIO_DURATION, extras.getDurationMs());
|
||||||
@ -830,9 +830,22 @@ public class AttachmentDatabase extends Database {
|
|||||||
PART_ID_WHERE + " AND " + PART_AUDIO_ONLY_WHERE,
|
PART_ID_WHERE + " AND " + PART_AUDIO_ONLY_WHERE,
|
||||||
extras.getAttachmentId().toStrings());
|
extras.getAttachmentId().toStrings());
|
||||||
|
|
||||||
|
if (threadId >= 0) {
|
||||||
|
notifyConversationListeners(threadId);
|
||||||
|
}
|
||||||
|
|
||||||
return alteredRows > 0;
|
return alteredRows > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates audio extra columns for the "audio/*" mime type attachments only.
|
||||||
|
* @return true if the update operation was successful.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras) {
|
||||||
|
return setAttachmentAudioExtras(extras, -1); // -1 for no update
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
class ThumbnailFetchCallable implements Callable<InputStream> {
|
class ThumbnailFetchCallable implements Callable<InputStream> {
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.database
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import java.util.*
|
import java.util.*
|
@ -25,13 +25,8 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
|||||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase;
|
import org.thoughtcrime.securesms.database.SessionJobDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
|
||||||
import org.thoughtcrime.securesms.loki.database.SessionJobDatabase;
|
|
||||||
import org.thoughtcrime.securesms.loki.database.SessionContactDatabase;
|
|
||||||
|
|
||||||
public class DatabaseFactory {
|
public class DatabaseFactory {
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.loki.utilities
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
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