forked from GitHub-Mirror/riotX-android
Merge branch 'develop' into feature/dagger [WIP]
This commit is contained in:
@ -2,14 +2,24 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.vector.riotredesign">
|
||||
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application>
|
||||
|
||||
<receiver android:name=".receiver.OnApplicationUpgradeReceiver">
|
||||
<receiver android:name=".receiver.OnApplicationUpgradeOrRebootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".core.services.AlarmSyncBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -14,24 +14,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.push.fcm;
|
||||
package im.vector.riotredesign.push.fcm
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import im.vector.riotredesign.core.pushers.PushersManager
|
||||
|
||||
public class FcmHelper {
|
||||
object FcmHelper {
|
||||
|
||||
fun isPushSupported(): Boolean = false
|
||||
|
||||
/**
|
||||
* Retrieves the FCM registration token.
|
||||
*
|
||||
* @return the FCM token or null if not received from FCM
|
||||
*/
|
||||
@Nullable
|
||||
public static String getFcmToken(Context context) {
|
||||
return null;
|
||||
fun getFcmToken(context: Context): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@ -40,8 +40,7 @@ public class FcmHelper {
|
||||
* @param context android context
|
||||
* @param token the token to store
|
||||
*/
|
||||
public static void storeFcmToken(@NonNull Context context,
|
||||
@Nullable String token) {
|
||||
fun storeFcmToken(context: Context, token: String?) {
|
||||
// No op
|
||||
}
|
||||
|
||||
@ -50,7 +49,7 @@ public class FcmHelper {
|
||||
*
|
||||
* @param activity the first launch Activity
|
||||
*/
|
||||
public static void ensureFcmTokenIsRetrieved(final Activity activity) {
|
||||
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager) {
|
||||
// No op
|
||||
}
|
||||
}
|
@ -16,12 +16,8 @@
|
||||
package im.vector.riotredesign.push.fcm
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.fragments.troubleshoot.TestAccountSettings
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
|
||||
import im.vector.riotredesign.features.settings.troubleshoot.TestBingRulesSettings
|
||||
import im.vector.riotredesign.features.settings.troubleshoot.TestDeviceSettings
|
||||
import im.vector.riotredesign.features.settings.troubleshoot.TestSystemSettings
|
||||
import im.vector.riotredesign.features.settings.troubleshoot.*
|
||||
import im.vector.riotredesign.push.fcm.troubleshoot.TestAutoStartBoot
|
||||
import im.vector.riotredesign.push.fcm.troubleshoot.TestBackgroundRestrictions
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,21 +15,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.receiver;
|
||||
package im.vector.riotredesign.receiver
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import im.vector.riotredesign.core.services.AlarmSyncBroadcastReceiver
|
||||
import timber.log.Timber
|
||||
|
||||
import timber.log.Timber;
|
||||
class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() {
|
||||
|
||||
public class OnApplicationUpgradeReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Timber.v("## onReceive() : Application has been upgraded, restart event stream service.");
|
||||
|
||||
// Start Event stream
|
||||
// TODO EventStreamServiceX.Companion.onApplicationUpgrade(context);
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Timber.v("## onReceive() ${intent.action}")
|
||||
AlarmSyncBroadcastReceiver.scheduleAlarm(context, 10)
|
||||
}
|
||||
}
|
@ -10,14 +10,10 @@
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:912726360885:android:448c9b63161abc9c",
|
||||
"android_client_info": {
|
||||
"package_name": "im.vector.riotredesign"
|
||||
"package_name": "im.vector.alpha"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
@ -29,15 +25,100 @@
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:912726360885:android:3120c24f6ef22f2b",
|
||||
"android_client_info": {
|
||||
"package_name": "im.vector.app"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:912726360885:android:25ef253beaff462e",
|
||||
"android_client_info": {
|
||||
"package_name": "im.vector.riotredesign"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:912726360885:android:bb204b7a7b08a10b",
|
||||
"android_client_info": {
|
||||
"package_name": "im.veon"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.push.fcm;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import com.google.android.gms.tasks.OnFailureListener;
|
||||
import com.google.android.gms.tasks.OnSuccessListener;
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
import com.google.firebase.iid.InstanceIdResult;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import im.vector.riotredesign.R;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class store the FCM token in SharedPrefs and ensure this token is retrieved.
|
||||
* It has an alter ego in the fdroid variant.
|
||||
*/
|
||||
public class FcmHelper {
|
||||
private static final String LOG_TAG = FcmHelper.class.getSimpleName();
|
||||
|
||||
private static final String PREFS_KEY_FCM_TOKEN = "FCM_TOKEN";
|
||||
|
||||
/**
|
||||
* Retrieves the FCM registration token.
|
||||
*
|
||||
* @return the FCM token or null if not received from FCM
|
||||
*/
|
||||
@Nullable
|
||||
public static String getFcmToken(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getString(PREFS_KEY_FCM_TOKEN, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store FCM token to the SharedPrefs
|
||||
*
|
||||
* @param context android context
|
||||
* @param token the token to store
|
||||
*/
|
||||
public static void storeFcmToken(@NonNull Context context,
|
||||
@Nullable String token) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString(PREFS_KEY_FCM_TOKEN, token)
|
||||
.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* onNewToken may not be called on application upgrade, so ensure my shared pref is set
|
||||
*
|
||||
* @param activity the first launch Activity
|
||||
*/
|
||||
public static void ensureFcmTokenIsRetrieved(final Activity activity) {
|
||||
if (TextUtils.isEmpty(getFcmToken(activity))) {
|
||||
|
||||
|
||||
//vfe: according to firebase doc
|
||||
//'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
||||
if (checkPlayServices(activity)) {
|
||||
try {
|
||||
FirebaseInstanceId.getInstance().getInstanceId()
|
||||
.addOnSuccessListener(activity, new OnSuccessListener<InstanceIdResult>() {
|
||||
@Override
|
||||
public void onSuccess(InstanceIdResult instanceIdResult) {
|
||||
storeFcmToken(activity, instanceIdResult.getToken());
|
||||
}
|
||||
})
|
||||
.addOnFailureListener(activity, new OnFailureListener() {
|
||||
@Override
|
||||
public void onFailure(@NonNull Exception e) {
|
||||
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show();
|
||||
Timber.e("No valid Google Play Services found. Cannot use FCM.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the device to make sure it has the Google Play Services APK. If
|
||||
* it doesn't, display a dialog that allows users to download the APK from
|
||||
* the Google Play Store or enable it in the device's system settings.
|
||||
*/
|
||||
private static boolean checkPlayServices(Activity activity) {
|
||||
GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
|
||||
int resultCode = apiAvailability.isGooglePlayServicesAvailable(activity);
|
||||
if (resultCode != ConnectionResult.SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
102
vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.kt
Executable file
102
vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.kt
Executable file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2014 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.push.fcm
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import android.widget.Toast
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.pushers.PushersManager
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* This class store the FCM token in SharedPrefs and ensure this token is retrieved.
|
||||
* It has an alter ego in the fdroid variant.
|
||||
*/
|
||||
object FcmHelper {
|
||||
private val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
|
||||
|
||||
|
||||
fun isPushSupported(): Boolean = true
|
||||
|
||||
/**
|
||||
* Retrieves the FCM registration token.
|
||||
*
|
||||
* @return the FCM token or null if not received from FCM
|
||||
*/
|
||||
fun getFcmToken(context: Context): String? {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getString(PREFS_KEY_FCM_TOKEN, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store FCM token to the SharedPrefs
|
||||
* TODO Store in realm
|
||||
*
|
||||
* @param context android context
|
||||
* @param token the token to store
|
||||
*/
|
||||
fun storeFcmToken(context: Context,
|
||||
token: String?) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString(PREFS_KEY_FCM_TOKEN, token)
|
||||
.apply()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* onNewToken may not be called on application upgrade, so ensure my shared pref is set
|
||||
*
|
||||
* @param activity the first launch Activity
|
||||
*/
|
||||
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager) {
|
||||
// if (TextUtils.isEmpty(getFcmToken(activity))) {
|
||||
//'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
||||
if (checkPlayServices(activity)) {
|
||||
try {
|
||||
FirebaseInstanceId.getInstance().instanceId
|
||||
.addOnSuccessListener(activity) { instanceIdResult ->
|
||||
storeFcmToken(activity, instanceIdResult.token)
|
||||
pushersManager.registerPusherWithFcmKey(instanceIdResult.token)
|
||||
}
|
||||
.addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.message) }
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.message)
|
||||
}
|
||||
|
||||
} else {
|
||||
Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show()
|
||||
Timber.e("No valid Google Play Services found. Cannot use FCM.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the device to make sure it has the Google Play Services APK. If
|
||||
* it doesn't, display a dialog that allows users to download the APK from
|
||||
* the Google Play Store or enable it in the device's system settings.
|
||||
*/
|
||||
private fun checkPlayServices(activity: Activity): Boolean {
|
||||
val apiAvailability = GoogleApiAvailability.getInstance()
|
||||
val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity)
|
||||
return resultCode == ConnectionResult.SUCCESS
|
||||
}
|
||||
}
|
@ -16,15 +16,11 @@
|
||||
package im.vector.riotredesign.push.fcm
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.fragments.troubleshoot.TestAccountSettings
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.features.settings.troubleshoot.*
|
||||
import im.vector.riotredesign.push.fcm.troubleshoot.TestFirebaseToken
|
||||
import im.vector.riotredesign.push.fcm.troubleshoot.TestPlayServices
|
||||
import im.vector.riotredesign.push.fcm.troubleshoot.TestTokenRegistration
|
||||
import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
|
||||
import im.vector.riotredesign.features.settings.troubleshoot.TestBingRulesSettings
|
||||
import im.vector.riotredesign.features.settings.troubleshoot.TestDeviceSettings
|
||||
import im.vector.riotredesign.features.settings.troubleshoot.TestSystemSettings
|
||||
|
||||
class NotificationTroubleshootTestManagerFactory {
|
||||
|
||||
|
@ -22,18 +22,27 @@ package im.vector.riotredesign.push.fcm
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.preference.BingRule
|
||||
import im.vector.riotredesign.core.pushers.PushersManager
|
||||
import im.vector.riotredesign.features.badge.BadgeProxy
|
||||
import im.vector.riotredesign.features.notifications.NotifiableEventResolver
|
||||
import im.vector.riotredesign.features.notifications.NotifiableMessageEvent
|
||||
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
|
||||
import im.vector.riotredesign.features.notifications.SimpleNotifiableEvent
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||
import org.koin.android.ext.android.inject
|
||||
>>>>>>> develop
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
@ -41,12 +50,18 @@ import timber.log.Timber
|
||||
*/
|
||||
class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
|
||||
<<<<<<< HEAD
|
||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||
|
||||
private val notifiableEventResolver by lazy {
|
||||
NotifiableEventResolver(this)
|
||||
}
|
||||
=======
|
||||
private val notificationDrawerManager by inject<NotificationDrawerManager>()
|
||||
private val pusherManager by inject<PushersManager>()
|
||||
>>>>>>> develop
|
||||
|
||||
private val notifiableEventResolver by inject<NotifiableEventResolver>()
|
||||
// UI handler
|
||||
private val mUIHandler by lazy {
|
||||
Handler(Looper.getMainLooper())
|
||||
@ -58,26 +73,27 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
* @param message the message
|
||||
*/
|
||||
override fun onMessageReceived(message: RemoteMessage?) {
|
||||
if (!PreferencesManager.areNotificationEnabledForDevice(applicationContext)) {
|
||||
Timber.i("Notification are disabled for this device")
|
||||
return
|
||||
}
|
||||
|
||||
if (message == null || message.data == null) {
|
||||
Timber.e("## onMessageReceived() : received a null message or message with no data")
|
||||
return
|
||||
}
|
||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||
Timber.i("## onMessageReceived()" + message.data.toString())
|
||||
Timber.i("## onMessageReceived() from FCM with priority " + message.priority)
|
||||
Timber.i("## onMessageReceived() %s", message.data.toString())
|
||||
Timber.i("## onMessageReceived() from FCM with priority %s", message.priority)
|
||||
}
|
||||
|
||||
//safe guard
|
||||
/* TODO
|
||||
val pushManager = Matrix.getInstance(applicationContext).pushManager
|
||||
if (!pushManager.areDeviceNotificationsAllowed()) {
|
||||
Timber.i("## onMessageReceived() : the notifications are disabled")
|
||||
return
|
||||
mUIHandler.post {
|
||||
if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||
//we are in foreground, let the sync do the things?
|
||||
Timber.v("PUSH received in a foreground state, ignore")
|
||||
} else {
|
||||
onMessageReceivedInternal(message.data)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
//TODO if the app is in foreground, we could just ignore this. The sync loop is already going?
|
||||
// TODO mUIHandler.post { onMessageReceivedInternal(message.data, pushManager) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -87,11 +103,27 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
* you retrieve the token.
|
||||
*/
|
||||
override fun onNewToken(refreshedToken: String?) {
|
||||
if (Matrix.getInstance().currentSession == null) return
|
||||
Timber.i("onNewToken: FCM Token has been updated")
|
||||
FcmHelper.storeFcmToken(this, refreshedToken)
|
||||
// TODO Matrix.getInstance(this)?.pushManager?.resetFCMRegistration(refreshedToken)
|
||||
if (refreshedToken == null) {
|
||||
Timber.w("onNewToken:received null token")
|
||||
} else {
|
||||
if (PreferencesManager.areNotificationEnabledForDevice(applicationContext)) {
|
||||
pusherManager.registerPusherWithFcmKey(refreshedToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the FCM server deletes pending messages. This may be due to:
|
||||
* - Too many messages stored on the FCM server.
|
||||
* This can occur when an app's servers send a bunch of non-collapsible messages to FCM servers while the device is offline.
|
||||
* - The device hasn't connected in a long time and the app server has recently (within the last 4 weeks)
|
||||
* sent a message to the app on that device.
|
||||
*
|
||||
* It is recommended that the app do a full sync with the app server after receiving this call.
|
||||
*/
|
||||
override fun onDeletedMessages() {
|
||||
Timber.v("## onDeletedMessages()")
|
||||
}
|
||||
@ -102,55 +134,58 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
* @param data Data map containing message data as key/value pairs.
|
||||
* For Set of keys use data.keySet().
|
||||
*/
|
||||
private fun onMessageReceivedInternal(data: Map<String, String> /*, pushManager: PushManager*/) {
|
||||
private fun onMessageReceivedInternal(data: Map<String, String>) {
|
||||
try {
|
||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||
Timber.i("## onMessageReceivedInternal() : $data")
|
||||
}
|
||||
val eventId = data["event_id"]
|
||||
val roomId = data["room_id"]
|
||||
if (eventId == null || roomId == null) {
|
||||
Timber.e("## onMessageReceivedInternal() missing eventId and/or roomId")
|
||||
return
|
||||
}
|
||||
// update the badge counter
|
||||
val unreadCount = data.get("unread")?.let { Integer.parseInt(it) } ?: 0
|
||||
BadgeProxy.updateBadgeCount(applicationContext, unreadCount)
|
||||
|
||||
/* TODO
|
||||
val session = Matrix.getInstance(applicationContext)?.defaultSession
|
||||
val session = safeGetCurrentSession()
|
||||
|
||||
if (VectorApp.isAppInBackground() && !pushManager.isBackgroundSyncAllowed) {
|
||||
//Notification contains metadata and maybe data information
|
||||
handleNotificationWithoutSyncingMode(data, session)
|
||||
if (session == null) {
|
||||
Timber.w("## Can't sync from push, no current session")
|
||||
} else {
|
||||
// Safe guard... (race?)
|
||||
if (isEventAlreadyKnown(data["event_id"], data["room_id"])) return
|
||||
//Catch up!!
|
||||
EventStreamServiceX.onPushReceived(this)
|
||||
if (isEventAlreadyKnown(eventId, roomId)) {
|
||||
Timber.i("Ignoring push, event already knwown")
|
||||
} else {
|
||||
Timber.v("Requesting background sync")
|
||||
session.requireBackgroundSync()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onMessageReceivedInternal() failed : " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun safeGetCurrentSession(): Session? {
|
||||
try {
|
||||
return Matrix.getInstance().currentSession
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## Failed to get current session")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// check if the event was not yet received
|
||||
// a previous catchup might have already retrieved the notified event
|
||||
private fun isEventAlreadyKnown(eventId: String?, roomId: String?): Boolean {
|
||||
if (null != eventId && null != roomId) {
|
||||
try {
|
||||
/* TODO
|
||||
val sessions = Matrix.getInstance(applicationContext).sessions
|
||||
|
||||
if (null != sessions && !sessions.isEmpty()) {
|
||||
for (session in sessions) {
|
||||
if (session.dataHandler?.store?.isReady == true) {
|
||||
session.dataHandler.store?.getEvent(eventId, roomId)?.let {
|
||||
Timber.e("## isEventAlreadyKnown() : ignore the event " + eventId
|
||||
+ " in room " + roomId + " because it is already known")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
val session = safeGetCurrentSession() ?: return false
|
||||
val room = session.getRoom(roomId) ?: return false
|
||||
return room.getTimeLineEvent(eventId) != null
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined " + e.message)
|
||||
Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined")
|
||||
}
|
||||
|
||||
}
|
||||
@ -186,48 +221,44 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
isPushGatewayEvent = true
|
||||
)
|
||||
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
|
||||
notificationDrawerManager.refreshNotificationDrawer(null)
|
||||
notificationDrawerManager.refreshNotificationDrawer()
|
||||
|
||||
return
|
||||
} else {
|
||||
|
||||
val event = parseEvent(data)
|
||||
if (event?.roomId == null) {
|
||||
//unsupported event
|
||||
Timber.e("Received an event with no room id")
|
||||
return
|
||||
val event = parseEvent(data) ?: return
|
||||
|
||||
val notifiableEvent = notifiableEventResolver.resolveEvent(event, session)
|
||||
|
||||
if (notifiableEvent == null) {
|
||||
Timber.e("Unsupported notifiable event ${eventId}")
|
||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||
Timber.e("--> ${event}")
|
||||
}
|
||||
} else {
|
||||
|
||||
var notifiableEvent = notifiableEventResolver.resolveEvent(event, null, null /* TODO session.fulfillRule(event) */, session)
|
||||
|
||||
if (notifiableEvent == null) {
|
||||
Timber.e("Unsupported notifiable event ${eventId}")
|
||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||
Timber.e("--> ${event}")
|
||||
if (notifiableEvent is NotifiableMessageEvent) {
|
||||
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
|
||||
notifiableEvent.senderName = data["sender_display_name"]
|
||||
?: data["sender"] ?: ""
|
||||
}
|
||||
} else {
|
||||
|
||||
|
||||
if (notifiableEvent is NotifiableMessageEvent) {
|
||||
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
|
||||
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
|
||||
}
|
||||
if (TextUtils.isEmpty(notifiableEvent.roomName)) {
|
||||
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
|
||||
}
|
||||
if (TextUtils.isEmpty(notifiableEvent.roomName)) {
|
||||
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
|
||||
}
|
||||
|
||||
notifiableEvent.isPushGatewayEvent = true
|
||||
notifiableEvent.matrixID = session.sessionParams.credentials.userId
|
||||
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
||||
notificationDrawerManager.refreshNotificationDrawer(null)
|
||||
}
|
||||
|
||||
notifiableEvent.isPushGatewayEvent = true
|
||||
notifiableEvent.matrixID = session.sessionParams.credentials.userId
|
||||
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
||||
notificationDrawerManager.refreshNotificationDrawer()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? {
|
||||
var roomName: String? = data["room_name"]
|
||||
val roomName: String? = data["room_name"]
|
||||
val roomId = data["room_id"]
|
||||
if (null == roomName && null != roomId) {
|
||||
// Try to get the room name from our store
|
||||
@ -256,13 +287,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
|
||||
try {
|
||||
return Event(eventId = data["event_id"],
|
||||
sender = data["sender"],
|
||||
senderId = data["sender"],
|
||||
roomId = data["room_id"],
|
||||
type = data.getValue("type"),
|
||||
// TODO content = data.getValue("content"),
|
||||
originServerTs = System.currentTimeMillis())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "buildEvent fails " + e.localizedMessage)
|
||||
Timber.e(e, "buildEvent fails ")
|
||||
}
|
||||
|
||||
return null
|
||||
|
@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="im.vector.riotredesign">
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
@ -19,13 +18,18 @@
|
||||
|
||||
<activity
|
||||
android:name=".features.MainActivity"
|
||||
android:theme="@style/AppTheme.Launcher">
|
||||
android:theme="@style/AppTheme.Launcher" />
|
||||
|
||||
<!-- Activity alias for the launcher Activity (must be declared after the Activity it targets) -->
|
||||
<activity-alias
|
||||
android:name=".features.Alias"
|
||||
android:targetActivity=".features.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</activity-alias>
|
||||
|
||||
<activity android:name=".features.home.HomeActivity" />
|
||||
<activity android:name=".features.login.LoginActivity" />
|
||||
@ -52,17 +56,37 @@
|
||||
android:label="@string/encryption_message_recovery" />
|
||||
|
||||
<activity
|
||||
android:name="im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity"
|
||||
android:name=".features.reactions.EmojiReactionPickerActivity"
|
||||
android:label="@string/title_activity_emoji_reaction_picker" />
|
||||
|
||||
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
|
||||
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
|
||||
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
|
||||
<activity android:name=".features.debug.DebugMenuActivity" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
<service
|
||||
android:name=".core.services.CallService"
|
||||
android:exported="false" />
|
||||
<!--<service-->
|
||||
<!--android:name="im.vector.matrix.android.internal.session.sync.job.SyncService"-->
|
||||
<!--android:exported="false" />-->
|
||||
|
||||
<service
|
||||
android:name=".core.services.VectorSyncService"
|
||||
android:exported="false">
|
||||
|
||||
</service>
|
||||
|
||||
<!-- Receivers -->
|
||||
|
||||
<!-- Exported false, should only be accessible from this app!! -->
|
||||
<receiver
|
||||
android:name=".features.notifications.NotificationBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Providers -->
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
@ -73,7 +97,6 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/riotx_provider_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -21,11 +21,12 @@
|
||||
div {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p>Riot Android</p>
|
||||
<p>RiotX Android</p>
|
||||
<p>Third Party Licenses</p>
|
||||
</div>
|
||||
|
||||
@ -173,6 +174,62 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
</pre>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>Tapadoo/Alerter</b>
|
||||
<br/>
|
||||
Tapadoo/Alerter is licensed under the MIT License
|
||||
Copyright 2017 Tapadoo, Dublin.
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
|
||||
Copyright 2017 Tapadoo, Dublin.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
</pre>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>com.github.piasy:BigImageViewer</b>
|
||||
<br/>
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Piasy
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</pre>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>textdrawable</b>
|
||||
<br/>
|
||||
textdrawable is licensed under the MIT License
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>
|
||||
Apache License
|
||||
@ -192,78 +249,25 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Copyright (c) 2019 The Matrix.org Foundation C.I.C
|
||||
</li>
|
||||
<li>
|
||||
<b>Jitsi Meet (<a
|
||||
href="https://github.com/jitsi/jitsi-meet">jitsi-meet</a>)</b>
|
||||
<b>rxkotlin</b>
|
||||
<br/>
|
||||
Copyright @ 2018-present 8x8, Inc.
|
||||
<br/>
|
||||
Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
Copyright io.reactivex.
|
||||
</li>
|
||||
<li>
|
||||
<b>Retrofit</b>
|
||||
<b>rxandroid</b>
|
||||
<br/>
|
||||
Copyright 2013 Square, Inc.
|
||||
Copyright io.reactivex.
|
||||
</li>
|
||||
<li>
|
||||
<b>okhttp</b>
|
||||
<b>rxrelay</b>
|
||||
<br/>
|
||||
Copyright 2016 Square, Inc.
|
||||
Copyright 2014 Netflix, Inc.
|
||||
Copyright 2015 Jake Wharton
|
||||
</li>
|
||||
<li>
|
||||
<b>ShortcutBadger</b>
|
||||
<b>rxbinding</b>
|
||||
<br/>
|
||||
Copyright 2014 Leo Lin
|
||||
</li>
|
||||
<li>
|
||||
<b>html-textview</b>
|
||||
<br/>
|
||||
Copyright (C) 2013-2015 Dominik Schürmann
|
||||
<br/>
|
||||
Copyright (C) 2013-2015 Juha Kuitunen
|
||||
<br/>
|
||||
Copyright (C) 2013 Mohammed Lakkadshaw
|
||||
<br/>
|
||||
Copyright (C) 2007 The Android Open Source Project
|
||||
</li>
|
||||
<li>
|
||||
<b>anddown</b>
|
||||
<br/>
|
||||
Copyright (c) 2016 CommonsWare, LLC
|
||||
</li>
|
||||
<li>
|
||||
<b>zip4j</b>
|
||||
<br/>
|
||||
Copyright 2010 Srikanth Reddy Lingala
|
||||
</li>
|
||||
<li>
|
||||
<b>SwipeBack</b>
|
||||
<br/>
|
||||
Copyright 2015 Eric Liu
|
||||
</li>
|
||||
<li>
|
||||
<b>Libphonenumber</b>
|
||||
<br/>
|
||||
Copyright 2017 Google
|
||||
</li>
|
||||
<li>
|
||||
<b>Butter Knife</b>
|
||||
<br/>
|
||||
Copyright 2013 Jake Wharton
|
||||
</li>
|
||||
<li>
|
||||
<b>FloatingActionButton</b>
|
||||
<br/>
|
||||
Copyright (C) 2014 Jerzy Chalupski
|
||||
</li>
|
||||
<li>
|
||||
<b>Spanny</b>
|
||||
<br/>
|
||||
Copyright 2015 Pavlovsky Ivan
|
||||
</li>
|
||||
<li>
|
||||
<b>PhotoView</b>
|
||||
<br/>
|
||||
Copyright 2018 Chris Banes
|
||||
Copyright (C) 2015 Jake Wharton
|
||||
</li>
|
||||
<li>
|
||||
<b>Epoxy</b>
|
||||
@ -271,9 +275,69 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Copyright 2016 Airbnb, Inc.
|
||||
</li>
|
||||
<li>
|
||||
<b>Anko</b>
|
||||
<b>mvrx</b>
|
||||
<br/>
|
||||
Copyright 2016 JetBrains s.r.o.
|
||||
Copyright 2018 Airbnb, Inc.
|
||||
</li>
|
||||
<li>
|
||||
<b>arrow-core</b>
|
||||
<br/>
|
||||
Copyright (C) 2017 The Λrrow Authors
|
||||
</li>
|
||||
<li>
|
||||
<b>material</b>
|
||||
<br/>
|
||||
Copyright (C) 2016 Google
|
||||
</li>
|
||||
<li>
|
||||
<b>span</b>
|
||||
<br/>
|
||||
Copyright 2018 Jun Gu
|
||||
</li>
|
||||
<li>
|
||||
<b>ru.noties.markwon</b>
|
||||
<br/>
|
||||
Copyright 2017 Dimitry Ivanov (mail@dimitryivanov.ru)
|
||||
</li>
|
||||
<li>
|
||||
<b>better-link-movement-method</b>
|
||||
<br/>
|
||||
Copyright 2018 Saket Narayan.
|
||||
</li>
|
||||
<li>
|
||||
<b>zxcvbn</b>
|
||||
<br/>
|
||||
Copyright (c) 2012-2016 Dan Wheeler and Dropbox, Inc.
|
||||
</li>
|
||||
<li>
|
||||
<b>com.otaliastudios:autocomplete</b>
|
||||
<br/>
|
||||
Copyright (c) 2017
|
||||
</li>
|
||||
<li>
|
||||
<b>Butterknife</b>
|
||||
<br/>
|
||||
Copyright 2013 Jake Wharton
|
||||
</li>
|
||||
<li>
|
||||
<b>seismic</b>
|
||||
<br/>
|
||||
Copyright 2012 Square, Inc.
|
||||
</li>
|
||||
<li>
|
||||
<b>videocache</b>
|
||||
<br/>
|
||||
Copyright 2014-2017 Alexey Danilov
|
||||
</li>
|
||||
<li>
|
||||
<b>ShortcutBadger</b>
|
||||
<br/>
|
||||
Copyright 2014 Leo Lin
|
||||
</li>
|
||||
<li>
|
||||
<b>FilePicker</b>
|
||||
<br/>
|
||||
Copyright (c) 2018, Jaisel Rahman
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
@ -453,25 +517,5 @@ Apache License
|
||||
of your accepting any such warranty or additional liability.
|
||||
</pre>
|
||||
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>Tapadoo/Alerter</b>
|
||||
<br/>
|
||||
Tapadoo/Alerter is licensed under the MIT License
|
||||
Copyright 2017 Tapadoo, Dublin.
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
|
||||
Copyright 2017 Tapadoo, Dublin.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -23,6 +23,10 @@ import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import androidx.core.provider.FontRequest
|
||||
import androidx.core.provider.FontsContractCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.multidex.MultiDex
|
||||
import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
@ -32,55 +36,106 @@ import com.github.piasy.biv.loader.glide.GlideImageLoader
|
||||
import com.jakewharton.threetenabp.AndroidThreeTen
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.MatrixConfiguration
|
||||
import im.vector.riotredesign.core.di.*
|
||||
import im.vector.matrix.android.api.auth.Authenticator
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
import im.vector.riotredesign.core.di.DaggerVectorComponent
|
||||
import im.vector.riotredesign.core.di.HasVectorInjector
|
||||
import im.vector.riotredesign.core.di.VectorComponent
|
||||
import im.vector.riotredesign.core.extensions.configureAndStart
|
||||
import im.vector.riotredesign.core.services.AlarmSyncBroadcastReceiver
|
||||
import im.vector.riotredesign.features.configuration.VectorConfiguration
|
||||
import im.vector.riotredesign.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
|
||||
import im.vector.riotredesign.features.notifications.NotificationUtils
|
||||
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
|
||||
import im.vector.riotredesign.features.rageshake.VectorFileLogger
|
||||
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
||||
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||
import im.vector.riotredesign.features.version.getVersion
|
||||
import im.vector.riotredesign.push.fcm.FcmHelper
|
||||
import timber.log.Timber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
|
||||
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {
|
||||
|
||||
|
||||
lateinit var appContext: Context
|
||||
//font thread handler
|
||||
@Inject lateinit var authenticator: Authenticator
|
||||
@Inject lateinit var vectorConfiguration: VectorConfiguration
|
||||
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
|
||||
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
|
||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
|
||||
lateinit var vectorComponent: VectorComponent
|
||||
private var fontThreadHandler: Handler? = null
|
||||
|
||||
|
||||
// var slowMode = false
|
||||
|
||||
|
||||
override fun onCreate() {
|
||||
val time = measureTimeMillis {
|
||||
super.onCreate()
|
||||
appContext = this
|
||||
vectorComponent = DaggerVectorComponent.factory().create(this)
|
||||
vectorComponent.inject(this)
|
||||
vectorUncaughtExceptionHandler.activate(this)
|
||||
// Log
|
||||
VectorFileLogger.init(this)
|
||||
Timber.plant(Timber.DebugTree(), VectorFileLogger)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Stetho.initializeWithDefaults(this)
|
||||
}
|
||||
AndroidThreeTen.init(this)
|
||||
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
|
||||
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks())
|
||||
val fontRequest = FontRequest(
|
||||
"com.google.android.gms.fonts",
|
||||
"com.google.android.gms",
|
||||
"Noto Color Emoji Compat",
|
||||
R.array.com_google_android_gms_fonts_certs
|
||||
)
|
||||
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
|
||||
vectorConfiguration.initConfiguration()
|
||||
super.onCreate()
|
||||
appContext = this
|
||||
vectorComponent = DaggerVectorComponent.factory().create(this)
|
||||
vectorComponent.inject(this)
|
||||
vectorUncaughtExceptionHandler.activate(this)
|
||||
// Log
|
||||
VectorFileLogger.init(this)
|
||||
Timber.plant(Timber.DebugTree(), VectorFileLogger)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Stetho.initializeWithDefaults(this)
|
||||
}
|
||||
Timber.v("On create took $time ms")
|
||||
logInfo()
|
||||
AndroidThreeTen.init(this)
|
||||
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
|
||||
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks())
|
||||
val fontRequest = FontRequest(
|
||||
"com.google.android.gms.fonts",
|
||||
"com.google.android.gms",
|
||||
"Noto Color Emoji Compat",
|
||||
R.array.com_google_android_gms_fonts_certs
|
||||
)
|
||||
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
|
||||
vectorConfiguration.initConfiguration()
|
||||
NotificationUtils.createNotificationChannels(applicationContext)
|
||||
if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
|
||||
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!
|
||||
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
|
||||
lastAuthenticatedSession.configureAndStart(pushRuleTriggerListener)
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun entersForeground() {
|
||||
AlarmSyncBroadcastReceiver.cancelAlarm(appContext)
|
||||
activeSessionHolder.getActiveSession().also {
|
||||
it.stopAnyBackgroundSync()
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun entersBackground() {
|
||||
Timber.i("App entered background") // call persistInfo
|
||||
notificationDrawerManager.persistInfo()
|
||||
if (FcmHelper.isPushSupported()) {
|
||||
//TODO FCM fallback
|
||||
} else {
|
||||
//TODO check if notifications are enabled for this device
|
||||
//We need to use alarm in this mode
|
||||
if (PreferencesManager.areNotificationEnabledForDevice(applicationContext)) {
|
||||
AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext, 4_000L)
|
||||
Timber.i("Alarm scheduled to restart service")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION)
|
||||
@ -91,6 +146,20 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||
return vectorComponent
|
||||
}
|
||||
|
||||
private fun logInfo() {
|
||||
val appVersion = getVersion(longFormat = true, useBuildNumber = true)
|
||||
val sdkVersion = Matrix.getSdkVersion()
|
||||
val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date())
|
||||
|
||||
Timber.v("----------------------------------------------------------------")
|
||||
Timber.v("----------------------------------------------------------------")
|
||||
Timber.v(" Application version: $appVersion")
|
||||
Timber.v(" SDK version: $sdkVersion")
|
||||
Timber.v(" Local time: $date")
|
||||
Timber.v("----------------------------------------------------------------")
|
||||
Timber.v("----------------------------------------------------------------\n\n\n\n")
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
|
@ -44,6 +44,10 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent
|
||||
return activeSession.get() != null
|
||||
}
|
||||
|
||||
fun getSafeActiveSession(): Session? {
|
||||
return activeSession.get()
|
||||
}
|
||||
|
||||
fun getActiveSession(): Session {
|
||||
return activeSession.get() ?: throw IllegalStateException("You should authenticate before using this")
|
||||
}
|
||||
|
@ -133,7 +133,6 @@ interface ScreenComponent {
|
||||
|
||||
fun inject(videoMediaViewerActivity: VideoMediaViewerActivity)
|
||||
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(vectorComponent: VectorComponent,
|
||||
|
@ -16,9 +16,6 @@
|
||||
|
||||
package im.vector.riotredesign.core.di
|
||||
|
||||
import com.squareup.inject.assisted.dagger2.AssistedModule
|
||||
import dagger.Module
|
||||
|
||||
/*
|
||||
@Module(includes = [AssistedInject_VectorAssistedModule::class])
|
||||
@AssistedModule
|
||||
|
@ -19,34 +19,30 @@ package im.vector.riotredesign.core.dialogs
|
||||
import android.app.Activity
|
||||
import android.text.Editable
|
||||
import android.text.TextUtils
|
||||
import android.text.TextWatcher
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.showPassword
|
||||
import im.vector.riotredesign.core.platform.SimpleTextWatcher
|
||||
|
||||
class ExportKeysDialog {
|
||||
|
||||
var passwordVisible = false
|
||||
|
||||
fun show(activity: Activity, exportKeyDialogListener: ExportKeyDialogListener) {
|
||||
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_export_e2e_keys, null)
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.encryption_export_room_keys)
|
||||
.setView(dialogLayout)
|
||||
|
||||
val passPhrase1EditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text)
|
||||
val passPhrase2EditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_confirm_passphrase_edit_text)
|
||||
val passPhrase2Til = dialogLayout.findViewById<TextInputLayout>(R.id.dialog_e2e_keys_confirm_passphrase_til)
|
||||
val exportButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_export_button)
|
||||
val textWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
|
||||
}
|
||||
|
||||
val passPhrase1EditText = dialogLayout.findViewById<TextInputEditText>(R.id.exportDialogEt)
|
||||
val passPhrase2EditText = dialogLayout.findViewById<TextInputEditText>(R.id.exportDialogEtConfirm)
|
||||
val passPhrase2Til = dialogLayout.findViewById<TextInputLayout>(R.id.exportDialogTilConfirm)
|
||||
val exportButton = dialogLayout.findViewById<Button>(R.id.exportDialogSubmit)
|
||||
val textWatcher = object : SimpleTextWatcher() {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
when {
|
||||
TextUtils.isEmpty(passPhrase1EditText.text) -> {
|
||||
@ -68,6 +64,14 @@ class ExportKeysDialog {
|
||||
passPhrase1EditText.addTextChangedListener(textWatcher)
|
||||
passPhrase2EditText.addTextChangedListener(textWatcher)
|
||||
|
||||
val showPassword = dialogLayout.findViewById<ImageView>(R.id.exportDialogShowPassword)
|
||||
showPassword.setOnClickListener {
|
||||
passwordVisible = !passwordVisible
|
||||
passPhrase1EditText.showPassword(passwordVisible)
|
||||
passPhrase2EditText.showPassword(passwordVisible)
|
||||
showPassword.setImageResource(if (passwordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||
}
|
||||
|
||||
val exportDialog = builder.show()
|
||||
|
||||
exportButton.setOnClickListener {
|
||||
|
@ -20,9 +20,13 @@ package im.vector.riotredesign.core.extensions
|
||||
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.sync.FilterService
|
||||
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
|
||||
|
||||
fun Session.openAndStartSync(){
|
||||
fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener) {
|
||||
open()
|
||||
setFilter(FilterService.FilterPreset.RiotFilter)
|
||||
startSync()
|
||||
refreshPushers()
|
||||
pushRuleTriggerListener.startWithSession(this)
|
||||
fetchPushRules()
|
||||
}
|
@ -18,16 +18,18 @@ package im.vector.riotredesign.core.files
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import arrow.core.Try
|
||||
import okio.Okio
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Save a string to a file with Okio
|
||||
* @return true in case of success
|
||||
*/
|
||||
fun saveStringToFile(str: String, file: File): Boolean {
|
||||
return try {
|
||||
@WorkerThread
|
||||
fun writeToFile(str: String, file: File): Try<Unit> {
|
||||
return Try {
|
||||
val sink = Okio.sink(file)
|
||||
|
||||
val bufferedSink = Okio.buffer(sink)
|
||||
@ -36,14 +38,25 @@ fun saveStringToFile(str: String, file: File): Boolean {
|
||||
|
||||
bufferedSink.close()
|
||||
sink.close()
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error saving file")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a byte array to a file with Okio
|
||||
*/
|
||||
@WorkerThread
|
||||
fun writeToFile(data: ByteArray, file: File): Try<Unit> {
|
||||
return Try {
|
||||
val sink = Okio.sink(file)
|
||||
|
||||
val bufferedSink = Okio.buffer(sink)
|
||||
|
||||
bufferedSink.write(data)
|
||||
|
||||
bufferedSink.close()
|
||||
sink.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun addEntryToDownloadManager(context: Context,
|
||||
file: File,
|
||||
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.intent
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import androidx.core.util.PatternsCompat.WEB_URL
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Inspired from Riot code: RoomMediaMessage.java
|
||||
*/
|
||||
sealed class ExternalIntentData {
|
||||
/**
|
||||
* Constructor for a text message.
|
||||
*
|
||||
* @param text the text
|
||||
* @param htmlText the HTML text
|
||||
* @param format the formatted text format
|
||||
*/
|
||||
data class IntentDataText(
|
||||
val text: CharSequence? = null,
|
||||
val htmlText: String? = null,
|
||||
val format: String? = null,
|
||||
val clipDataItem: ClipData.Item = ClipData.Item(text, htmlText),
|
||||
val mimeType: String? = if (null == htmlText) ClipDescription.MIMETYPE_TEXT_PLAIN else format
|
||||
) : ExternalIntentData()
|
||||
|
||||
/**
|
||||
* Clip data
|
||||
*/
|
||||
data class IntentDataClipData(
|
||||
val clipDataItem: ClipData.Item,
|
||||
val mimeType: String?
|
||||
) : ExternalIntentData()
|
||||
|
||||
/**
|
||||
* Constructor from a media Uri/
|
||||
*
|
||||
* @param uri the media uri
|
||||
* @param filename the media file name
|
||||
*/
|
||||
data class IntentDataUri(
|
||||
val uri: Uri,
|
||||
val filename: String? = null
|
||||
) : ExternalIntentData()
|
||||
}
|
||||
|
||||
|
||||
fun analyseIntent(intent: Intent): List<ExternalIntentData> {
|
||||
val externalIntentDataList = ArrayList<ExternalIntentData>()
|
||||
|
||||
|
||||
// chrome adds many items when sharing an web page link
|
||||
// so, test first the type
|
||||
if (TextUtils.equals(intent.type, ClipDescription.MIMETYPE_TEXT_PLAIN)) {
|
||||
var message: String? = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
|
||||
if (null == message) {
|
||||
val sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
|
||||
if (null != sequence) {
|
||||
message = sequence.toString()
|
||||
}
|
||||
}
|
||||
|
||||
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||
|
||||
if (!TextUtils.isEmpty(subject)) {
|
||||
if (TextUtils.isEmpty(message)) {
|
||||
message = subject
|
||||
} else if (WEB_URL.matcher(message!!).matches()) {
|
||||
message = subject + "\n" + message
|
||||
}
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(message)) {
|
||||
externalIntentDataList.add(ExternalIntentData.IntentDataText(message!!, null, intent.type))
|
||||
return externalIntentDataList
|
||||
}
|
||||
}
|
||||
|
||||
var clipData: ClipData? = null
|
||||
var mimetypes: MutableList<String>? = null
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
clipData = intent.clipData
|
||||
}
|
||||
|
||||
// multiple data
|
||||
if (null != clipData) {
|
||||
if (null != clipData.description) {
|
||||
if (0 != clipData.description.mimeTypeCount) {
|
||||
mimetypes = ArrayList()
|
||||
|
||||
for (i in 0 until clipData.description.mimeTypeCount) {
|
||||
mimetypes.add(clipData.description.getMimeType(i))
|
||||
}
|
||||
|
||||
// if the filter is "accept anything" the mimetype does not make sense
|
||||
if (1 == mimetypes.size) {
|
||||
if (mimetypes[0].endsWith("/*")) {
|
||||
mimetypes = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val count = clipData.itemCount
|
||||
|
||||
for (i in 0 until count) {
|
||||
val item = clipData.getItemAt(i)
|
||||
var mimetype: String? = null
|
||||
|
||||
if (null != mimetypes) {
|
||||
if (i < mimetypes.size) {
|
||||
mimetype = mimetypes[i]
|
||||
} else {
|
||||
mimetype = mimetypes[0]
|
||||
}
|
||||
|
||||
// uris list is not a valid mimetype
|
||||
if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) {
|
||||
mimetype = null
|
||||
}
|
||||
}
|
||||
|
||||
externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimetype))
|
||||
}
|
||||
} else if (null != intent.data) {
|
||||
externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!))
|
||||
}
|
||||
|
||||
return externalIntentDataList
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.intent
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
|
||||
fun getFilenameFromUri(context: Context, uri: Uri): String? {
|
||||
var result: String? = null
|
||||
if (uri.scheme == "content") {
|
||||
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
||||
}
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
result = uri.path
|
||||
val cut = result.lastIndexOf('/')
|
||||
if (cut != -1) {
|
||||
result = result.substring(cut + 1)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.intent
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import im.vector.riotredesign.core.utils.getFileExtension
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Returns the mimetype from a uri.
|
||||
*
|
||||
* @param context the context
|
||||
* @return the mimetype
|
||||
*/
|
||||
fun getMimeTypeFromUri(context: Context, uri: Uri): String? {
|
||||
var mimeType: String? = null
|
||||
|
||||
try {
|
||||
mimeType = context.contentResolver.getType(uri)
|
||||
|
||||
// try to find the mimetype from the filename
|
||||
if (null == mimeType) {
|
||||
val extension = getFileExtension(uri.toString())
|
||||
if (extension != null) {
|
||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
}
|
||||
}
|
||||
|
||||
if (null != mimeType) {
|
||||
// the mimetype is sometimes in uppercase.
|
||||
mimeType = mimeType.toLowerCase()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to open resource input stream")
|
||||
}
|
||||
|
||||
return mimeType
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.Checkable
|
||||
|
||||
class CheckableView : View, Checkable {
|
||||
|
||||
private var mChecked = false
|
||||
|
||||
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 isChecked(): Boolean {
|
||||
return mChecked
|
||||
}
|
||||
|
||||
override fun setChecked(b: Boolean) {
|
||||
if (b != mChecked) {
|
||||
mChecked = b
|
||||
refreshDrawableState()
|
||||
}
|
||||
}
|
||||
|
||||
override fun toggle() {
|
||||
isChecked = !mChecked
|
||||
}
|
||||
|
||||
public override fun onCreateDrawableState(extraSpace: Int): IntArray {
|
||||
val drawableState = super.onCreateDrawableState(extraSpace + 1)
|
||||
if (isChecked) {
|
||||
mergeDrawableStates(drawableState, CHECKED_STATE_SET)
|
||||
}
|
||||
return drawableState
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
|
||||
}
|
||||
}
|
@ -356,7 +356,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
|
||||
open fun getMenuRes() = -1
|
||||
|
||||
@AttrRes
|
||||
open fun getMenuTint() = R.attr.vctr_icon_tint_on_dark_action_bar_color
|
||||
open fun getMenuTint() = R.attr.vctr_icon_tint_on_light_action_bar_color
|
||||
|
||||
/**
|
||||
* Return a object containing other themes for this activity
|
||||
|
@ -28,6 +28,8 @@ abstract class VectorPreferenceFragment : PreferenceFragmentCompat() {
|
||||
activity as VectorBaseActivity
|
||||
}
|
||||
|
||||
abstract var titleRes: Int
|
||||
|
||||
/* ==========================================================================================
|
||||
* Life cycle
|
||||
* ========================================================================================== */
|
||||
@ -36,6 +38,7 @@ abstract class VectorPreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(titleRes)
|
||||
Timber.v("onResume Fragment ${this.javaClass.simpleName}")
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ import android.widget.TextView
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
// TODO Replace by real Bingrule class
|
||||
// TODO Replace by real Bingrule class, then delete
|
||||
class BingRule(rule: BingRule) {
|
||||
fun shouldNotNotify() = false
|
||||
fun shouldNotify() = false
|
||||
|
@ -39,7 +39,8 @@ open class UserAvatarPreference : Preference {
|
||||
|
||||
init {
|
||||
widgetLayoutResource = R.layout.vector_settings_round_avatar
|
||||
isIconSpaceReserved = false
|
||||
// Set to false to remove the space when there is no icon
|
||||
isIconSpaceReserved = true
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
|
@ -37,7 +37,8 @@ class VectorEditTextPreference : EditTextPreference {
|
||||
|
||||
init {
|
||||
dialogLayoutResource = R.layout.dialog_preference_edit_text
|
||||
isIconSpaceReserved = false
|
||||
// Set to false to remove the space when there is no icon
|
||||
isIconSpaceReserved = true
|
||||
}
|
||||
|
||||
// No single line for title
|
||||
|
@ -54,7 +54,8 @@ class VectorListPreference : ListPreference {
|
||||
|
||||
init {
|
||||
widgetLayoutResource = R.layout.vector_settings_list_preference_with_warning
|
||||
isIconSpaceReserved = false
|
||||
// Set to false to remove the space when there is no icon
|
||||
isIconSpaceReserved = true
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
|
@ -75,7 +75,8 @@ open class VectorPreference : Preference {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
|
||||
|
||||
init {
|
||||
isIconSpaceReserved = false
|
||||
// Set to false to remove the space when there is no icon
|
||||
isIconSpaceReserved = true
|
||||
}
|
||||
|
||||
var isHighlighted = false
|
||||
@ -156,8 +157,4 @@ open class VectorPreference : Preference {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = VectorPreference::class.java.simpleName
|
||||
}
|
||||
}
|
@ -36,7 +36,8 @@ class VectorPreferenceCategory : PreferenceCategory {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
init {
|
||||
isIconSpaceReserved = false
|
||||
// Set to false to remove the space when there is no icon
|
||||
isIconSpaceReserved = true
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
@ -47,6 +48,8 @@ class VectorPreferenceCategory : PreferenceCategory {
|
||||
titleTextView?.setTypeface(null, Typeface.BOLD)
|
||||
|
||||
// "isIconSpaceReserved = false" does not work for preference category, so remove the padding
|
||||
(titleTextView?.parent as? ViewGroup)?.setPadding(0, 0, 0, 0)
|
||||
if (!isIconSpaceReserved) {
|
||||
(titleTextView?.parent as? ViewGroup)?.setPadding(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
@ -37,7 +37,8 @@ class VectorSwitchPreference : SwitchPreference {
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
init {
|
||||
isIconSpaceReserved = false
|
||||
// Set to false to remove the space when there is no icon
|
||||
isIconSpaceReserved = true
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
|
@ -0,0 +1,38 @@
|
||||
package im.vector.riotredesign.core.pushers
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.resources.AppNameProvider
|
||||
import im.vector.riotredesign.core.resources.LocaleProvider
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
|
||||
private const val DEFAULT_PUSHER_FILE_TAG = "mobile"
|
||||
|
||||
class PushersManager(
|
||||
private val currentSession: Session,
|
||||
private val localeProvider: LocaleProvider,
|
||||
private val stringProvider: StringProvider,
|
||||
private val appNameProvider: AppNameProvider
|
||||
) {
|
||||
|
||||
fun registerPusherWithFcmKey(pushKey: String) {
|
||||
var profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + Math.abs(currentSession.sessionParams.credentials.userId.hashCode())
|
||||
|
||||
currentSession.addHttpPusher(
|
||||
pushKey,
|
||||
stringProvider.getString(R.string.pusher_app_id),
|
||||
profileTag,
|
||||
localeProvider.current().language,
|
||||
appNameProvider.getAppName(),
|
||||
currentSession.sessionParams.credentials.deviceId ?: "MOBILE",
|
||||
stringProvider.getString(R.string.pusher_http_url),
|
||||
false,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
fun unregisterPusher(pushKey: String, callback: MatrixCallback<Unit>) {
|
||||
currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id),callback)
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package im.vector.riotredesign.core.resources
|
||||
|
||||
import android.content.Context
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
class AppNameProvider(private val context: Context) {
|
||||
|
||||
fun getAppName(): String {
|
||||
try {
|
||||
val appPackageName = context.applicationContext.packageName
|
||||
val pm = context.packageManager
|
||||
val appInfo = pm.getApplicationInfo(appPackageName, 0)
|
||||
var appName = pm.getApplicationLabel(appInfo).toString()
|
||||
|
||||
// Use appPackageName instead of appName if appName contains any non-ASCII character
|
||||
if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
|
||||
appName = appPackageName
|
||||
}
|
||||
return appName
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## AppNameProvider() : failed " + e.message)
|
||||
return "RiotXAndroid"
|
||||
}
|
||||
}
|
||||
}
|
@ -26,6 +26,4 @@ class LocaleProvider @Inject constructor(private val resources: Resources) {
|
||||
fun current(): Locale {
|
||||
return ConfigurationCompat.getLocales(resources.configuration)[0]
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package im.vector.riotredesign.core.services
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import timber.log.Timber
|
||||
|
||||
class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
|
||||
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
||||
//Aquire a lock to give enough time for the sync :/
|
||||
(context.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "riotx:fdroidSynclock").apply {
|
||||
acquire((10_000).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This method is called when the BroadcastReceiver is receiving an Intent broadcast.
|
||||
Timber.d("RestartBroadcastReceiver received intent")
|
||||
Intent(context, VectorSyncService::class.java).also {
|
||||
it.action = "SLOW"
|
||||
context.startService(it)
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
//TODO
|
||||
Timber.e(ex)
|
||||
}
|
||||
}
|
||||
|
||||
scheduleAlarm(context, 30_000L)
|
||||
|
||||
Timber.i("Alarm scheduled to restart service")
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_CODE = 0
|
||||
|
||||
fun scheduleAlarm(context: Context, delay: Long) {
|
||||
//Reschedule
|
||||
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
|
||||
val pIntent = PendingIntent.getBroadcast(context, AlarmSyncBroadcastReceiver.REQUEST_CODE,
|
||||
intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val firstMillis = System.currentTimeMillis() + delay
|
||||
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pIntent)
|
||||
} else {
|
||||
alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pIntent)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelAlarm(context: Context) {
|
||||
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
|
||||
val pIntent = PendingIntent.getBroadcast(context, AlarmSyncBroadcastReceiver.REQUEST_CODE,
|
||||
intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
alarmMgr.cancel(pIntent)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.services
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
|
||||
/**
|
||||
* This class simulate push event when FCM is not working/disabled
|
||||
*/
|
||||
class PushSimulatorWorker(val context: Context,
|
||||
workerParams: WorkerParameters) : Worker(context, workerParams) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
// Simulate a Push
|
||||
EventStreamServiceX.onSimulatedPushReceived(context)
|
||||
|
||||
// Indicate whether the task finished successfully with the Result
|
||||
return Result.success()
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.core.services
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import im.vector.matrix.android.internal.session.sync.job.SyncService
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.features.notifications.NotificationUtils
|
||||
import timber.log.Timber
|
||||
|
||||
class VectorSyncService : SyncService() {
|
||||
|
||||
override fun onCreate() {
|
||||
Timber.v("VectorSyncService - onCreate ")
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Timber.v("VectorSyncService - onDestroy ")
|
||||
removeForegroundNotif()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun removeForegroundNotif() {
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Service is started only in fdroid mode when no FCM is available
|
||||
* Otherwise it is bounded
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Timber.v("VectorSyncService - onStartCommand ")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notification = NotificationUtils.buildForegroundServiceNotification(applicationContext, R.string.notification_listening_for_events, false)
|
||||
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
/**
|
||||
* If the service is bounded and the service was previously started we can remove foreground notif
|
||||
*/
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
Timber.v("VectorSyncService - onBind ")
|
||||
stopForeground(true)
|
||||
return super.onBind(intent)
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
Timber.v("VectorSyncService - onUnbind ")
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.core.ui.list
|
||||
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotredesign.core.extensions.setTextOrHide
|
||||
|
||||
/**
|
||||
* A generic list item.
|
||||
* Displays an item with a title, and optional description.
|
||||
* Can display an accessory on the right, that can be an image or an indeterminate progress.
|
||||
* If provided with an action, will display a button at the bottom of the list item.
|
||||
*/
|
||||
@EpoxyModelClass(layout = R.layout.item_generic_footer)
|
||||
abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>() {
|
||||
|
||||
|
||||
@EpoxyAttribute
|
||||
var text: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var style: GenericItem.STYLE = GenericItem.STYLE.NORMAL_TEXT
|
||||
|
||||
@EpoxyAttribute
|
||||
var itemClickAction: GenericItem.Action? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
|
||||
holder.text.setTextOrHide(text)
|
||||
when (style) {
|
||||
GenericItem.STYLE.BIG_TEXT -> holder.text.textSize = 18f
|
||||
GenericItem.STYLE.NORMAL_TEXT -> holder.text.textSize = 14f
|
||||
}
|
||||
|
||||
|
||||
holder.view.setOnClickListener {
|
||||
itemClickAction?.perform?.run()
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val text by bind<TextView>(R.id.itemGenericFooterText)
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ package im.vector.riotredesign.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
|
||||
/**
|
||||
@ -29,9 +30,13 @@ import androidx.appcompat.app.AlertDialog
|
||||
*/
|
||||
fun Context.displayInWebView(url: String) {
|
||||
val wv = WebView(this)
|
||||
|
||||
// Set a WebViewClient to ensure redirection is handled directly in the WebView
|
||||
wv.webViewClient = WebViewClient()
|
||||
|
||||
wv.loadUrl(url)
|
||||
AlertDialog.Builder(this)
|
||||
.setView(wv)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.Authenticator
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
import im.vector.riotredesign.core.di.ScreenComponent
|
||||
import im.vector.riotredesign.core.extensions.openAndStartSync
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import im.vector.riotredesign.features.home.HomeActivity
|
||||
import im.vector.riotredesign.features.login.LoginActivity
|
||||
@ -76,16 +75,12 @@ class MainActivity : VectorBaseActivity() {
|
||||
}
|
||||
})
|
||||
else -> start()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
val intent = if (authenticator.hasAuthenticatedSessions()) {
|
||||
if (!sessionHolder.hasActiveSession()) {
|
||||
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!
|
||||
sessionHolder.setActiveSession(lastAuthenticatedSession)
|
||||
lastAuthenticatedSession.openAndStartSync()
|
||||
}
|
||||
val intent = if (sessionHolder.hasActiveSession()) {
|
||||
HomeActivity.newIntent(this)
|
||||
} else {
|
||||
LoginActivity.newIntent(this)
|
||||
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.crypto.keys
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
import im.vector.riotredesign.core.files.addEntryToDownloadManager
|
||||
import im.vector.riotredesign.core.files.writeToFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class KeysExporter(private val session: Session) {
|
||||
|
||||
/**
|
||||
* Export keys and return the file path with the callback
|
||||
*/
|
||||
fun export(context: Context, password: String, callback: MatrixCallback<String>) {
|
||||
session.exportRoomKeys(password, object : MatrixCallback<ByteArray> {
|
||||
|
||||
override fun onSuccess(data: ByteArray) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Try {
|
||||
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
|
||||
|
||||
writeToFile(data, file)
|
||||
|
||||
addEntryToDownloadManager(context, file, "text/plain")
|
||||
|
||||
file.absolutePath
|
||||
}
|
||||
}
|
||||
.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.crypto.keys
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.riotredesign.core.intent.getMimeTypeFromUri
|
||||
import im.vector.riotredesign.core.resources.openResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class KeysImporter(private val session: Session) {
|
||||
|
||||
/**
|
||||
* Import keys from provided Uri
|
||||
*/
|
||||
fun import(context: Context,
|
||||
uri: Uri,
|
||||
mimetype: String?,
|
||||
password: String,
|
||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Try {
|
||||
val resource = openResource(context, uri, mimetype ?: getMimeTypeFromUri(context, uri))
|
||||
|
||||
if (resource?.mContentStream == null) {
|
||||
throw Exception("Error")
|
||||
}
|
||||
|
||||
val data: ByteArray
|
||||
try {
|
||||
data = ByteArray(resource.mContentStream!!.available())
|
||||
resource.mContentStream!!.read(data)
|
||||
resource.mContentStream!!.close()
|
||||
|
||||
data
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
resource.mContentStream!!.close()
|
||||
} catch (e2: Exception) {
|
||||
Timber.e(e2, "## importKeys()")
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
{
|
||||
callback.onFailure(it)
|
||||
},
|
||||
{ byteArray ->
|
||||
session.importRoomKeys(byteArray,
|
||||
password,
|
||||
null,
|
||||
callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -22,10 +22,13 @@ import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.dialogs.ExportKeysDialog
|
||||
import im.vector.riotredesign.core.extensions.observeEvent
|
||||
import im.vector.riotredesign.core.platform.SimpleFragmentActivity
|
||||
import im.vector.riotredesign.core.utils.toast
|
||||
import im.vector.riotredesign.features.crypto.keys.KeysExporter
|
||||
|
||||
class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
|
||||
@ -63,19 +66,19 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
|
||||
viewModel.navigateEvent.observeEvent(this) { uxStateEvent ->
|
||||
when (uxStateEvent) {
|
||||
KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_2 -> {
|
||||
KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_2 -> {
|
||||
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, KeysBackupSetupStep2Fragment.newInstance())
|
||||
.commit()
|
||||
}
|
||||
KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_3 -> {
|
||||
KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_3 -> {
|
||||
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, KeysBackupSetupStep3Fragment.newInstance())
|
||||
.commit()
|
||||
}
|
||||
KeysBackupSetupSharedViewModel.NAVIGATE_FINISH -> {
|
||||
KeysBackupSetupSharedViewModel.NAVIGATE_FINISH -> {
|
||||
val resultIntent = Intent()
|
||||
viewModel.keysVersion.value?.version?.let {
|
||||
resultIntent.putExtra(KEYS_VERSION, it)
|
||||
@ -83,7 +86,18 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT -> {
|
||||
KeysBackupSetupSharedViewModel.NAVIGATE_PROMPT_REPLACE -> {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.keys_backup_setup_override_backup_prompt_tile)
|
||||
.setMessage(R.string.keys_backup_setup_override_backup_prompt_description)
|
||||
.setPositiveButton(R.string.keys_backup_setup_override_replace) { _, _ ->
|
||||
viewModel.forceCreateKeyBackup(this)
|
||||
}.setNegativeButton(R.string.keys_backup_setup_override_stop) { _, _ ->
|
||||
viewModel.stopAndKeepAfterDetectingExistingOnServer()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT -> {
|
||||
exportKeysManually()
|
||||
}
|
||||
}
|
||||
@ -117,49 +131,38 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
})
|
||||
}
|
||||
|
||||
fun exportKeysManually() {
|
||||
private fun exportKeysManually() {
|
||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
notImplemented()
|
||||
/*
|
||||
showWaitingView()
|
||||
|
||||
CommonActivityUtils.exportKeys(session, passphrase, object : SimpleApiCallback<String>(this@KeysBackupSetupActivity) {
|
||||
override fun onSuccess(filename: String) {
|
||||
hideWaitingView()
|
||||
KeysExporter(session)
|
||||
.export(this@KeysBackupSetupActivity,
|
||||
passphrase,
|
||||
object : MatrixCallback<String> {
|
||||
|
||||
AlertDialog.Builder(this@KeysBackupSetupActivity)
|
||||
.setMessage(getString(R.string.encryption_export_saved_as, filename))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { dialog, which ->
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra(MANUAL_EXPORT, true)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
override fun onSuccess(data: String) {
|
||||
hideWaitingView()
|
||||
|
||||
override fun onNetworkError(e: Exception) {
|
||||
super.onNetworkError(e)
|
||||
hideWaitingView()
|
||||
}
|
||||
AlertDialog.Builder(this@KeysBackupSetupActivity)
|
||||
.setMessage(getString(R.string.encryption_export_saved_as, data))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { dialog, which ->
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra(MANUAL_EXPORT, true)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onMatrixError(e: MatrixError) {
|
||||
super.onMatrixError(e)
|
||||
hideWaitingView()
|
||||
}
|
||||
|
||||
override fun onUnexpectedError(e: Exception) {
|
||||
super.onUnexpectedError(e)
|
||||
hideWaitingView()
|
||||
}
|
||||
})
|
||||
*/
|
||||
override fun onFailure(failure: Throwable) {
|
||||
toast(failure.localizedMessage)
|
||||
hideWaitingView()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -171,8 +174,8 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.keys_backup_setup_skip_title)
|
||||
.setMessage(R.string.keys_backup_setup_skip_msg)
|
||||
.setNegativeButton(R.string.stay, null)
|
||||
.setPositiveButton(R.string.abort) { _, _ ->
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.leave) { _, _ ->
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
|
@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.WaitingViewData
|
||||
import im.vector.riotredesign.core.utils.LiveEvent
|
||||
@ -40,6 +41,7 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
|
||||
companion object {
|
||||
const val NAVIGATE_TO_STEP_2 = "NAVIGATE_TO_STEP_2"
|
||||
const val NAVIGATE_TO_STEP_3 = "NAVIGATE_TO_STEP_3"
|
||||
const val NAVIGATE_PROMPT_REPLACE = "NAVIGATE_PROMPT_REPLACE"
|
||||
const val NAVIGATE_FINISH = "NAVIGATE_FINISH"
|
||||
const val NAVIGATE_MANUAL_EXPORT = "NAVIGATE_MANUAL_EXPORT"
|
||||
}
|
||||
@ -124,15 +126,8 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
|
||||
megolmBackupCreationInfo = data
|
||||
copyHasBeenMade = false
|
||||
|
||||
val keyBackup = session?.getKeysBackupService()
|
||||
if (keyBackup != null) {
|
||||
createKeysBackup(context, keyBackup)
|
||||
} else {
|
||||
loadingStatus.value = null
|
||||
|
||||
isCreatingBackupVersion.value = false
|
||||
prepareRecoverFailError.value = Exception()
|
||||
}
|
||||
val keyBackup = session.getKeysBackupService()
|
||||
createKeysBackup(context, keyBackup)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
@ -150,18 +145,32 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createKeysBackup(context: Context, keysBackup: KeysBackupService) {
|
||||
fun forceCreateKeyBackup(context: Context) {
|
||||
val keyBackup = session.getKeysBackupService()
|
||||
createKeysBackup(context, keyBackup, true)
|
||||
}
|
||||
|
||||
fun stopAndKeepAfterDetectingExistingOnServer() {
|
||||
loadingStatus.value = null
|
||||
navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
|
||||
session.getKeysBackupService().checkAndStartKeysBackup()
|
||||
}
|
||||
|
||||
private fun createKeysBackup(context: Context, keysBackup: KeysBackupService, forceOverride: Boolean = false) {
|
||||
loadingStatus.value = WaitingViewData(context.getString(R.string.keys_backup_setup_creating_backup), isIndeterminate = true)
|
||||
|
||||
creatingBackupError.value = null
|
||||
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : MatrixCallback<KeysVersion> {
|
||||
|
||||
override fun onSuccess(data: KeysVersion) {
|
||||
loadingStatus.value = null
|
||||
|
||||
isCreatingBackupVersion.value = false
|
||||
keysVersion.value = data
|
||||
navigateEvent.value = LiveEvent(NAVIGATE_TO_STEP_3)
|
||||
keysBackup.getCurrentVersion(object : MatrixCallback<KeysVersionResult?> {
|
||||
override fun onSuccess(data: KeysVersionResult?) {
|
||||
if (data?.version.isNullOrBlank() || forceOverride) {
|
||||
processOnCreate()
|
||||
} else {
|
||||
loadingStatus.value = null
|
||||
// we should prompt
|
||||
isCreatingBackupVersion.value = false
|
||||
navigateEvent.value = LiveEvent(NAVIGATE_PROMPT_REPLACE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
@ -171,7 +180,26 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
|
||||
isCreatingBackupVersion.value = false
|
||||
creatingBackupError.value = failure
|
||||
}
|
||||
|
||||
fun processOnCreate() {
|
||||
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : MatrixCallback<KeysVersion> {
|
||||
override fun onSuccess(data: KeysVersion) {
|
||||
loadingStatus.value = null
|
||||
|
||||
isCreatingBackupVersion.value = false
|
||||
keysVersion.value = data
|
||||
navigateEvent.value = LiveEvent(NAVIGATE_TO_STEP_3)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e(failure, "## createKeyBackupVersion")
|
||||
loadingStatus.value = null
|
||||
|
||||
isCreatingBackupVersion.value = false
|
||||
creatingBackupError.value = failure
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -25,15 +25,20 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import arrow.core.Try
|
||||
import butterknife.BindView
|
||||
import butterknife.OnClick
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.di.ScreenComponent
|
||||
import im.vector.riotredesign.core.files.addEntryToDownloadManager
|
||||
import im.vector.riotredesign.core.files.saveStringToFile
|
||||
import im.vector.riotredesign.core.files.writeToFile
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import im.vector.riotredesign.core.utils.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
|
||||
@ -161,30 +166,39 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
|
||||
}
|
||||
|
||||
private fun exportRecoveryKeyToFile(data: String) {
|
||||
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Try {
|
||||
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")
|
||||
|
||||
if (saveStringToFile(data, file)) {
|
||||
addEntryToDownloadManager(requireContext(), file, "text/plain")
|
||||
writeToFile(data, file)
|
||||
|
||||
context?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, file.absolutePath))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
addEntryToDownloadManager(requireContext(), file, "text/plain")
|
||||
|
||||
file.absolutePath
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
{ throwable ->
|
||||
context?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(throwable.localizedMessage)
|
||||
}
|
||||
},
|
||||
{ path ->
|
||||
viewModel.copyHasBeenMade = true
|
||||
|
||||
viewModel.copyHasBeenMade = true
|
||||
} else {
|
||||
context?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(getString(R.string.unknown_error))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
context?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, path))
|
||||
}
|
||||
}
|
||||
)
|
||||
?.setCancelable(false)
|
||||
?.setPositiveButton(R.string.ok, null)
|
||||
?.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,11 +39,15 @@ import im.vector.riotredesign.core.extensions.replaceFragment
|
||||
import im.vector.riotredesign.core.platform.OnBackPressed
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import im.vector.riotredesign.core.pushers.PushersManager
|
||||
import im.vector.riotredesign.features.crypto.keysrequest.KeyRequestHandler
|
||||
import im.vector.riotredesign.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
|
||||
import im.vector.riotredesign.features.rageshake.BugReporter
|
||||
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
||||
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
|
||||
import im.vector.riotredesign.features.workers.signout.SignOutViewModel
|
||||
import im.vector.riotredesign.push.fcm.FcmHelper
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -66,6 +70,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
@Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler
|
||||
// TODO Move this elsewhere
|
||||
@Inject lateinit var keyRequestHandler: KeyRequestHandler
|
||||
@Inject lateinit var pushManager: PushersManager
|
||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||
|
||||
private var progress: ProgressDialog? = null
|
||||
|
||||
@ -84,8 +90,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
homeNavigator.activity = this
|
||||
navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(HomeNavigationViewModel::class.java)
|
||||
|
||||
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager)
|
||||
navigationViewModel = ViewModelProviders.of(this).get(HomeNavigationViewModel::class.java)
|
||||
drawerLayout.addDrawerListener(drawerListener)
|
||||
if (isFirstCreation()) {
|
||||
val homeDrawerFragment = HomeDrawerFragment.newInstance()
|
||||
@ -111,6 +117,20 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
is Navigation.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
|
||||
}
|
||||
}
|
||||
|
||||
if (intent.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)) {
|
||||
notificationDrawerManager.clearAllEvents()
|
||||
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {
|
||||
notificationDrawerManager.clearAllEvents()
|
||||
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@ -132,6 +152,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
.setNegativeButton(R.string.no) { _, _ -> bugReporter.deleteCrashFile(this) }
|
||||
.show()
|
||||
}
|
||||
|
||||
//Force remote backup state update to update the banner if needed
|
||||
ViewModelProviders.of(this).get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded()
|
||||
}
|
||||
|
||||
override fun configure(toolbar: Toolbar) {
|
||||
@ -143,7 +166,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.sliding_menu_sign_out -> {
|
||||
SignOutUiWorker(this).perform(activeSessionHolder.getActiveSession())
|
||||
SignOutUiWorker(this, notificationDrawerManager).perform(activeSessionHolder.getActiveSession())
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -182,10 +205,14 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
|
||||
|
||||
companion object {
|
||||
fun newIntent(context: Context): Intent {
|
||||
return Intent(context, HomeActivity::class.java)
|
||||
}
|
||||
private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION"
|
||||
|
||||
fun newIntent(context: Context, clearNotification: Boolean = false): Intent {
|
||||
return Intent(context, HomeActivity::class.java)
|
||||
.apply {
|
||||
putExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, clearNotification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -17,12 +17,9 @@
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.di.ScreenScope
|
||||
import im.vector.riotredesign.core.extensions.replaceFragment
|
||||
import im.vector.riotredesign.features.navigation.Navigator
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@ -37,43 +34,11 @@ class HomeNavigator @Inject constructor() {
|
||||
fun openSelectedGroup(groupSummary: GroupSummary) {
|
||||
Timber.v("Open selected group ${groupSummary.groupId}")
|
||||
activity?.let {
|
||||
it.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
|
||||
val args = HomeDetailParams(groupSummary.groupId, groupSummary.displayName, groupSummary.avatarUrl)
|
||||
val homeDetailFragment = HomeDetailFragment.newInstance(args)
|
||||
it.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer)
|
||||
}
|
||||
}
|
||||
|
||||
fun openRoomDetail(roomId: String,
|
||||
eventId: String?,
|
||||
navigator: Navigator) {
|
||||
Timber.v("Open room detail $roomId - $eventId")
|
||||
activity?.let {
|
||||
//TODO enable eventId permalink. It doesn't work enough at the moment.
|
||||
it.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
navigator.openRoom(roomId, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun openGroupDetail(groupId: String) {
|
||||
Timber.v("Open group detail $groupId")
|
||||
}
|
||||
|
||||
fun openUserDetail(userId: String) {
|
||||
Timber.v("Open user detail $userId")
|
||||
}
|
||||
|
||||
// Private Methods *****************************************************************************
|
||||
|
||||
private fun clearBackStack(fragmentManager: FragmentManager) {
|
||||
if (fragmentManager.backStackEntryCount > 0) {
|
||||
val first = fragmentManager.getBackStackEntryAt(0)
|
||||
fragmentManager.popBackStack(first.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRoot(roomId: String): Boolean {
|
||||
return rootRoomId == roomId
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkData
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkParser
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.features.navigation.Navigator
|
||||
|
||||
class PermalinkHandler(private val session: Session,
|
||||
private val navigator: Navigator) {
|
||||
|
||||
fun launch(context: Context, deepLink: String?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean {
|
||||
val uri = deepLink?.let { Uri.parse(it) }
|
||||
return launch(context, uri, navigateToRoomInterceptor)
|
||||
}
|
||||
|
||||
fun launch(context: Context, deepLink: Uri?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean {
|
||||
if (deepLink == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return when (val permalinkData = PermalinkParser.parse(deepLink)) {
|
||||
is PermalinkData.EventLink -> {
|
||||
if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias, permalinkData.eventId) != true) {
|
||||
openRoom(context, permalinkData.roomIdOrAlias, permalinkData.eventId)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
is PermalinkData.RoomLink -> {
|
||||
if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias) != true) {
|
||||
openRoom(context, permalinkData.roomIdOrAlias)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
is PermalinkData.GroupLink -> {
|
||||
navigator.openGroupDetail(permalinkData.groupId, context)
|
||||
true
|
||||
}
|
||||
is PermalinkData.UserLink -> {
|
||||
navigator.openUserDetail(permalinkData.userId, context)
|
||||
true
|
||||
}
|
||||
is PermalinkData.FallbackLink -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open room either joined, or not unknown
|
||||
*/
|
||||
private fun openRoom(context: Context, roomIdOrAlias: String, eventId: String? = null) {
|
||||
if (session.getRoom(roomIdOrAlias) != null) {
|
||||
navigator.openRoom(context, roomIdOrAlias, eventId)
|
||||
} else {
|
||||
navigator.openNotJoinedRoom(context, roomIdOrAlias, eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface NavigateToRoomInterceptor {
|
||||
|
||||
/**
|
||||
* Return true if the navigation has been intercepted
|
||||
*/
|
||||
fun navToRoom(roomId: String, eventId: String? = null): Boolean
|
||||
|
||||
}
|
@ -54,8 +54,8 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
groupController.callback = this
|
||||
stateView.contentView = epoxyRecyclerView
|
||||
epoxyRecyclerView.setController(groupController)
|
||||
stateView.contentView = groupListEpoxyRecyclerView
|
||||
groupListEpoxyRecyclerView.setController(groupController)
|
||||
viewModel.subscribe { renderState(it) }
|
||||
viewModel.openGroupLiveData.observeEvent(this) {
|
||||
homeNavigator.openSelectedGroup(it)
|
||||
|
@ -90,7 +90,8 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
|
||||
|
||||
private fun observeGroupSummaries() {
|
||||
session
|
||||
.rx().liveGroupSummaries()
|
||||
.rx()
|
||||
.liveGroupSummaries()
|
||||
.map {
|
||||
val myUser = session.getUser(session.sessionParams.credentials.userId)
|
||||
val allCommunityGroup = GroupSummary(
|
||||
@ -100,6 +101,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
|
||||
listOf(allCommunityGroup) + it
|
||||
}
|
||||
.execute { async ->
|
||||
// TODO Phase2 Handle the case where the selected group is deleted on another client
|
||||
val newSelectedGroup = selectedGroup ?: async()?.firstOrNull()
|
||||
copy(asyncGroups = async, selectedGroup = newSelectedGroup)
|
||||
}
|
||||
|
@ -30,8 +30,9 @@ sealed class RoomDetailActions {
|
||||
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
|
||||
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
|
||||
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
|
||||
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val opposite: String) : RoomDetailActions()
|
||||
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
|
||||
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
|
||||
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
|
||||
object AcceptInvite : RoomDetailActions()
|
||||
object RejectInvite : RoomDetailActions()
|
||||
|
||||
|
@ -57,13 +57,7 @@ import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.riotredesign.R
|
||||
@ -72,6 +66,7 @@ import im.vector.riotredesign.core.dialogs.DialogListItem
|
||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||
import im.vector.riotredesign.core.extensions.hideKeyboard
|
||||
import im.vector.riotredesign.core.extensions.observeEvent
|
||||
import im.vector.riotredesign.core.extensions.setTextOrHide
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import im.vector.riotredesign.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
||||
@ -86,9 +81,7 @@ import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandP
|
||||
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
|
||||
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
|
||||
import im.vector.riotredesign.features.command.Command
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
||||
import im.vector.riotredesign.features.home.getColorFromUserId
|
||||
import im.vector.riotredesign.features.home.*
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
|
||||
@ -106,6 +99,7 @@ import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import im.vector.riotredesign.features.media.ImageMediaViewerActivity
|
||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||
import im.vector.riotredesign.features.media.VideoMediaViewerActivity
|
||||
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
|
||||
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
@ -179,10 +173,12 @@ class RoomDetailFragment :
|
||||
@Inject lateinit var commandAutocompletePolicy: CommandAutocompletePolicy
|
||||
@Inject lateinit var autocompleteCommandPresenter: AutocompleteCommandPresenter
|
||||
@Inject lateinit var autocompleteUserPresenter: AutocompleteUserPresenter
|
||||
@Inject lateinit var homePermalinkHandler: HomePermalinkHandler
|
||||
@Inject lateinit var permalinkHandler: PermalinkHandler
|
||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||
@Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory
|
||||
@Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory
|
||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
|
||||
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_room_detail
|
||||
@ -216,6 +212,11 @@ class RoomDetailFragment :
|
||||
handleActions(it)
|
||||
}
|
||||
|
||||
roomDetailViewModel.navigateToEvent.observeEvent(this) {
|
||||
//
|
||||
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
|
||||
}
|
||||
|
||||
roomDetailViewModel.selectSubscribe(
|
||||
RoomDetailViewState::sendMode,
|
||||
RoomDetailViewState::selectedEvent,
|
||||
@ -239,25 +240,25 @@ class RoomDetailFragment :
|
||||
}
|
||||
//switch to expanded bar
|
||||
composerLayout.composerRelatedMessageTitle.apply {
|
||||
text = event.senderName
|
||||
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.sender)))
|
||||
text = event.getDisambiguatedDisplayName()
|
||||
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId)))
|
||||
}
|
||||
|
||||
//TODO this is used at several places, find way to refactor?
|
||||
val messageContent: MessageContent? =
|
||||
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.content.toModel()
|
||||
?: event.root.content.toModel()
|
||||
val nonFormattedBody = messageContent?.body ?: ""
|
||||
var formattedBody: CharSequence? = null
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(messageContent.formattedBody
|
||||
?: messageContent.body)
|
||||
?: messageContent.body)
|
||||
formattedBody = Markwon.builder(requireContext())
|
||||
.usePlugin(HtmlPlugin.create()).build().render(document)
|
||||
}
|
||||
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||
?: nonFormattedBody
|
||||
?: nonFormattedBody
|
||||
|
||||
|
||||
if (mode == SendMode.EDIT) {
|
||||
@ -272,8 +273,8 @@ class RoomDetailFragment :
|
||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_reply))
|
||||
}
|
||||
|
||||
avatarRenderer.render(event.senderAvatar, event.root.sender
|
||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
composerLayout.expand {
|
||||
@ -289,6 +290,18 @@ class RoomDetailFragment :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
notificationDrawerManager.setCurrentRoom(null)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
@ -296,9 +309,9 @@ class RoomDetailFragment :
|
||||
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
||||
REACTION_SELECT_REQUEST_CODE -> {
|
||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||
?: return
|
||||
?: return
|
||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||
?: return
|
||||
?: return
|
||||
//TODO check if already reacted with that?
|
||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
||||
}
|
||||
@ -314,12 +327,14 @@ class RoomDetailFragment :
|
||||
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
|
||||
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
||||
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
|
||||
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.itemAnimator = null
|
||||
recyclerView.setHasFixedSize(true)
|
||||
timelineEventController.addModelBuildListener {
|
||||
it.dispatchTo(stateRestorer)
|
||||
it.dispatchTo(scrollOnNewMessageCallback)
|
||||
it.dispatchTo(scrollOnHighlightedEventCallback)
|
||||
}
|
||||
|
||||
recyclerView.addOnScrollListener(
|
||||
@ -484,7 +499,7 @@ class RoomDetailFragment :
|
||||
val summary = state.asyncRoomSummary()
|
||||
val inviter = state.asyncInviter()
|
||||
if (summary?.membership == Membership.JOIN) {
|
||||
timelineEventController.setTimeline(state.timeline)
|
||||
timelineEventController.setTimeline(state.timeline, state.eventId)
|
||||
inviteView.visibility = View.GONE
|
||||
|
||||
val uid = session.sessionParams.credentials.userId
|
||||
@ -503,12 +518,7 @@ class RoomDetailFragment :
|
||||
state.asyncRoomSummary()?.let {
|
||||
roomToolbarTitleView.text = it.displayName
|
||||
avatarRenderer.render(it, roomToolbarAvatarImageView)
|
||||
if (it.topic.isNotEmpty()) {
|
||||
roomToolbarSubtitleView.visibility = View.VISIBLE
|
||||
roomToolbarSubtitleView.text = it.topic
|
||||
} else {
|
||||
roomToolbarSubtitleView.visibility = View.GONE
|
||||
}
|
||||
roomToolbarSubtitleView.setTextOrHide(it.topic)
|
||||
}
|
||||
}
|
||||
|
||||
@ -551,20 +561,49 @@ class RoomDetailFragment :
|
||||
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
|
||||
override fun onUrlClicked(url: String) {
|
||||
homePermalinkHandler.launch(url)
|
||||
override fun onUrlClicked(url: String): Boolean {
|
||||
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
|
||||
override fun navToRoom(roomId: String, eventId: String?): Boolean {
|
||||
// Same room?
|
||||
if (roomId == roomDetailArgs.roomId) {
|
||||
// Navigation to same room
|
||||
if (eventId == null) {
|
||||
showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room))
|
||||
} else {
|
||||
// Highlight and scroll to this event
|
||||
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, timelineEventController.searchPositionOfEvent(eventId)))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Not handled
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onUrlLongClicked(url: String): Boolean {
|
||||
// Copy the url to the clipboard
|
||||
copyToClipboard(requireContext(), url)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onEventVisible(event: TimelineEvent) {
|
||||
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
|
||||
}
|
||||
|
||||
override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) {
|
||||
vectorBaseActivity.notImplemented("encrypted message click")
|
||||
}
|
||||
|
||||
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
|
||||
// TODO Use navigator
|
||||
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
|
||||
// TODO Use navigator
|
||||
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
|
||||
startActivity(intent)
|
||||
}
|
||||
@ -593,7 +632,7 @@ class RoomDetailFragment :
|
||||
}
|
||||
|
||||
override fun onAvatarClicked(informationData: MessageInformationData) {
|
||||
vectorBaseActivity.notImplemented()
|
||||
vectorBaseActivity.notImplemented("Click on user avatar")
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@ -636,7 +675,7 @@ class RoomDetailFragment :
|
||||
}
|
||||
MessageMenuViewModel.ACTION_VIEW_REACTIONS -> {
|
||||
val messageInformationData = actionData.data as? MessageInformationData
|
||||
?: return
|
||||
?: return
|
||||
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData)
|
||||
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
||||
}
|
||||
@ -694,9 +733,9 @@ class RoomDetailFragment :
|
||||
.show()
|
||||
}
|
||||
MessageMenuViewModel.ACTION_QUICK_REACT -> {
|
||||
//eventId,ClickedOn,Opposite
|
||||
(actionData.data as? Triple<String, String, String>)?.let { (eventId, clickedOn, opposite) ->
|
||||
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite))
|
||||
//eventId,ClickedOn,Add
|
||||
(actionData.data as? Triple<String, String, Boolean>)?.let { (eventId, clickedOn, add) ->
|
||||
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, add))
|
||||
}
|
||||
}
|
||||
MessageMenuViewModel.ACTION_EDIT -> {
|
||||
@ -775,7 +814,7 @@ class RoomDetailFragment :
|
||||
imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
|
||||
private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
|
||||
val snack = Snackbar.make(view!!, message, duration)
|
||||
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
|
||||
snack.show()
|
||||
|
@ -44,6 +44,7 @@ import im.vector.riotredesign.features.home.room.detail.timeline.helper.Timeline
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import timber.log.Timber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -62,7 +63,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
} else {
|
||||
TimelineDisplayableEvents.DISPLAYABLE_TYPES
|
||||
}
|
||||
private val timeline = room.createTimeline(eventId, allowedTypes)
|
||||
private var timeline = room.createTimeline(eventId, allowedTypes)
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
@ -110,6 +111,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
else -> Timber.e("Unhandled Action: $action")
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,6 +143,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
|
||||
get() = _sendMessageResultLiveData
|
||||
|
||||
private val _navigateToEvent = MutableLiveData<LiveEvent<String>>()
|
||||
val navigateToEvent: LiveData<LiveEvent<String>>
|
||||
get() = _navigateToEvent
|
||||
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
||||
@ -213,7 +221,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
}
|
||||
SendMode.EDIT -> {
|
||||
room.editTextMessage(state.selectedEvent?.root?.eventId
|
||||
?: "", action.text, action.autoMarkdown)
|
||||
?: "", action.text, action.autoMarkdown)
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
@ -225,7 +233,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
SendMode.QUOTE -> {
|
||||
val messageContent: MessageContent? =
|
||||
state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: state.selectedEvent?.root?.content.toModel()
|
||||
?: state.selectedEvent?.root?.content.toModel()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
val finalText = legacyRiotQuoteText(textMsg, action.text)
|
||||
@ -294,7 +302,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
|
||||
_nonBlockingPopAlert.postValue(LiveEvent(
|
||||
Pair(R.string.last_edited_info_message, listOf(
|
||||
lastReplace.senderName ?: "?",
|
||||
lastReplace.getDisambiguatedDisplayName(),
|
||||
dateFormat.format(Date(lastReplace.root.originServerTs ?: 0)))
|
||||
))
|
||||
)
|
||||
@ -345,7 +353,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
|
||||
|
||||
private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
|
||||
room.updateQuickReaction(action.selectedReaction, action.opposite, action.targetEventId, session.sessionParams.credentials.userId)
|
||||
if (action.add) {
|
||||
room.sendReaction(action.selectedReaction, action.targetEventId)
|
||||
} else {
|
||||
room.undoReaction(action.selectedReaction, action.targetEventId, session.sessionParams.credentials.userId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
||||
@ -415,6 +427,56 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) {
|
||||
val targetEventId = action.eventId
|
||||
|
||||
if (action.position != null) {
|
||||
// Event is already in RAM
|
||||
withState {
|
||||
if (it.eventId == targetEventId) {
|
||||
// ensure another click on the same permalink will also do a scroll
|
||||
setState {
|
||||
copy(
|
||||
eventId = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
eventId = targetEventId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_navigateToEvent.postValue(LiveEvent(targetEventId))
|
||||
} else {
|
||||
// change timeline
|
||||
timeline.dispose()
|
||||
timeline = room.createTimeline(targetEventId, allowedTypes)
|
||||
timeline.start()
|
||||
|
||||
withState {
|
||||
if (it.eventId == targetEventId) {
|
||||
// ensure another click on the same permalink will also do a scroll
|
||||
setState {
|
||||
copy(
|
||||
eventId = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
eventId = targetEventId,
|
||||
timeline = this@RoomDetailViewModel.timeline
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_navigateToEvent.postValue(LiveEvent(targetEventId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeEventDisplayedActions() {
|
||||
// We are buffering scroll events for one second
|
||||
@ -441,7 +503,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
private fun observeInvitationState() {
|
||||
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
||||
if (summary.membership == Membership.INVITE) {
|
||||
summary.lastMessage?.sender?.let { senderId ->
|
||||
summary.lastMessage?.senderId?.let { senderId ->
|
||||
session.getUser(senderId)
|
||||
}?.also {
|
||||
setState { copy(asyncInviter = Success(it)) }
|
||||
|
@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
|
||||
@ -46,7 +45,6 @@ data class RoomDetailViewState(
|
||||
val timeline: Timeline? = null,
|
||||
val asyncInviter: Async<User> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val asyncTimelineData: Async<TimelineData> = Uninitialized,
|
||||
val sendMode: SendMode = SendMode.REGULAR,
|
||||
val selectedEvent: TimelineEvent? = null
|
||||
) : MvRxState {
|
||||
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager,
|
||||
private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback {
|
||||
|
||||
private val scheduledEventId = AtomicReference<String?>()
|
||||
|
||||
override fun onChanged(position: Int, count: Int, tag: Any?) {
|
||||
val eventId = scheduledEventId.get() ?: return
|
||||
|
||||
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
|
||||
|
||||
if (positionToScroll != null) {
|
||||
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||
|
||||
// Do not scroll it item is already visible
|
||||
if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
|
||||
// Note: Offset will be from the bottom, since the layoutManager is reversed
|
||||
layoutManager.scrollToPositionWithOffset(positionToScroll, 120)
|
||||
}
|
||||
scheduledEventId.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun scheduleScrollTo(eventId: String?) {
|
||||
scheduledEventId.set(eventId)
|
||||
}
|
||||
}
|
@ -50,9 +50,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||
private val backgroundHandler: Handler
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||
|
||||
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback {
|
||||
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
|
||||
fun onEventVisible(event: TimelineEvent)
|
||||
fun onUrlClicked(url: String)
|
||||
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
|
||||
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
||||
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
|
||||
fun onFileMessageClicked(messageFileContent: MessageFileContent)
|
||||
@ -75,6 +75,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||
fun onMemberNameClicked(informationData: MessageInformationData)
|
||||
}
|
||||
|
||||
interface UrlClickCallback {
|
||||
fun onUrlClicked(url: String): Boolean
|
||||
fun onUrlLongClicked(url: String): Boolean
|
||||
}
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<String>()
|
||||
private val mergeItemCollapseStates = HashMap<String, Boolean>()
|
||||
private val modelCache = arrayListOf<CacheItemData?>()
|
||||
@ -87,39 +92,43 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||
|
||||
private val listUpdateCallback = object : ListUpdateCallback {
|
||||
|
||||
@Synchronized
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(position until (position + count)).forEach {
|
||||
modelCache[it] = null
|
||||
synchronized(modelCache) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(position until (position + count)).forEach {
|
||||
modelCache[it] = null
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
val model = modelCache.removeAt(fromPosition)
|
||||
modelCache.add(toPosition, model)
|
||||
requestModelBuild()
|
||||
synchronized(modelCache) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
val model = modelCache.removeAt(fromPosition)
|
||||
modelCache.add(toPosition, model)
|
||||
requestModelBuild()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(0 until count).forEach {
|
||||
modelCache.add(position, null)
|
||||
synchronized(modelCache) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(0 until count).forEach {
|
||||
modelCache.add(position, null)
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(0 until count).forEach {
|
||||
modelCache.removeAt(position)
|
||||
synchronized(modelCache) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(0 until count).forEach {
|
||||
modelCache.removeAt(position)
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,13 +136,37 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun setTimeline(timeline: Timeline?) {
|
||||
fun setTimeline(timeline: Timeline?, eventIdToHighlight: String?) {
|
||||
if (this.timeline != timeline) {
|
||||
this.timeline = timeline
|
||||
this.timeline?.listener = this
|
||||
|
||||
// Clear cache
|
||||
synchronized(modelCache) {
|
||||
for (i in 0 until modelCache.size) {
|
||||
modelCache[i] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.eventIdToHighlight != eventIdToHighlight) {
|
||||
// Clear cache to force a refresh
|
||||
synchronized(modelCache) {
|
||||
for (i in 0 until modelCache.size) {
|
||||
if (modelCache[i]?.eventId == eventIdToHighlight
|
||||
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
||||
modelCache[i] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
this.eventIdToHighlight = eventIdToHighlight
|
||||
|
||||
requestModelBuild()
|
||||
}
|
||||
}
|
||||
|
||||
private var eventIdToHighlight: String? = null
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
timelineMediaSizeProvider.recyclerView = recyclerView
|
||||
@ -173,28 +206,29 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||
require(inSubmitList || Looper.myLooper() == backgroundHandler.looper)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getModels(): List<EpoxyModel<*>> {
|
||||
(0 until modelCache.size).forEach { position ->
|
||||
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
||||
// We then are sure we always have items up to date.
|
||||
if (modelCache[position] == null
|
||||
|| modelCache[position]?.mergedHeaderModel != null
|
||||
|| modelCache[position]?.formattedDayModel != null) {
|
||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||
}
|
||||
}
|
||||
return modelCache
|
||||
.map {
|
||||
val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) {
|
||||
null
|
||||
} else {
|
||||
it.eventModel
|
||||
}
|
||||
listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
|
||||
synchronized(modelCache) {
|
||||
(0 until modelCache.size).forEach { position ->
|
||||
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
||||
// We then are sure we always have items up to date.
|
||||
if (modelCache[position] == null
|
||||
|| modelCache[position]?.mergedHeaderModel != null
|
||||
|| modelCache[position]?.formattedDayModel != null) {
|
||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||
}
|
||||
.flatten()
|
||||
.filterNotNull()
|
||||
}
|
||||
return modelCache
|
||||
.map {
|
||||
val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) {
|
||||
null
|
||||
} else {
|
||||
it.eventModel
|
||||
}
|
||||
listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
|
||||
}
|
||||
.flatten()
|
||||
.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -205,14 +239,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
|
||||
val eventModel = timelineItemFactory.create(event, nextEvent, callback).also {
|
||||
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
}
|
||||
val mergedHeaderModel = buildMergedHeaderItem(event, nextEvent, items, addDaySeparator, currentPosition)
|
||||
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
|
||||
|
||||
return CacheItemData(event.localId, eventModel, mergedHeaderModel, daySeparatorItem)
|
||||
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
|
||||
}
|
||||
|
||||
private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? {
|
||||
@ -224,6 +258,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Phase 3 Handle the case where the eventId we have to highlight is merged
|
||||
private fun buildMergedHeaderItem(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
items: List<TimelineEvent>,
|
||||
@ -241,7 +276,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||
val senderAvatar = mergedEvent.senderAvatar()
|
||||
val senderName = mergedEvent.senderName()
|
||||
MergedHeaderItem.Data(
|
||||
userId = mergedEvent.root.sender ?: "",
|
||||
userId = mergedEvent.root.senderId ?: "",
|
||||
avatarUrl = senderAvatar,
|
||||
memberName = senderName ?: "",
|
||||
eventId = mergedEvent.localId
|
||||
@ -273,10 +308,23 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||
addIf(shouldAdd, this@TimelineEventController)
|
||||
}
|
||||
|
||||
fun searchPositionOfEvent(eventId: String): Int? {
|
||||
synchronized(modelCache) {
|
||||
// Search in the cache
|
||||
modelCache.forEachIndexed { idx, cacheItemData ->
|
||||
if (cacheItemData?.eventId == eventId) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class CacheItemData(
|
||||
val localId: String,
|
||||
val eventId: String?,
|
||||
val eventModel: EpoxyModel<*>? = null,
|
||||
val mergedHeaderModel: MergedHeaderItem? = null,
|
||||
val formattedDayModel: DaySeparatorItem? = null
|
||||
|
@ -101,8 +101,9 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
.commit()
|
||||
}
|
||||
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
|
||||
override fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List<String>, eventId: String) {
|
||||
actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Triple(eventId, clikedOn, opposite))
|
||||
|
||||
override fun didQuickReactWith(clikedOn: String, add: Boolean, eventId: String) {
|
||||
actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Triple(eventId, clikedOn, add))
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,8 @@ data class MessageActionState(
|
||||
/**
|
||||
* Information related to an event and used to display preview in contextual bottomsheet.
|
||||
*/
|
||||
class MessageActionsViewModel @AssistedInject constructor(@Assisted initialState: MessageActionState,
|
||||
class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: MessageActionState,
|
||||
private val eventHtmlRenderer: EventHtmlRenderer,
|
||||
private val session: Session,
|
||||
private val noticeEventFormatter: NoticeEventFormatter
|
||||
@ -91,11 +92,11 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted initialState
|
||||
when (event.root.type) {
|
||||
EventType.MESSAGE -> {
|
||||
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.content.toModel()
|
||||
?: event.root.content.toModel()
|
||||
body = messageContent?.body
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
body = eventHtmlRenderer.render(messageContent.formattedBody
|
||||
?: messageContent.body)
|
||||
?: messageContent.body)
|
||||
}
|
||||
}
|
||||
EventType.STATE_ROOM_NAME,
|
||||
@ -109,7 +110,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted initialState
|
||||
}
|
||||
}
|
||||
return state.copy(
|
||||
userId = event.root.sender ?: "",
|
||||
userId = event.root.senderId ?: "",
|
||||
senderName = informationData.memberName?.toString() ?: "",
|
||||
messageBody = body,
|
||||
ts = dateFormat.format(Date(originTs ?: 0)),
|
||||
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import org.json.JSONObject
|
||||
|
||||
@ -52,7 +53,8 @@ data class MessageMenuState(
|
||||
* Manages list actions for a given message (copy / paste / forward...)
|
||||
*/
|
||||
class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: MessageMenuState,
|
||||
private val session: Session) : VectorViewModel<MessageMenuState>(initialState) {
|
||||
private val session: Session,
|
||||
private val stringProvider: StringProvider) : VectorViewModel<MessageMenuState>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
@ -159,10 +161,14 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||
this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, JSONObject(event.root.toContent()).toString(4)))
|
||||
if (event.isEncrypted()) {
|
||||
this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, state.eventId))
|
||||
val decryptedContent = event.root.mClearEvent?.toContent()?.let {
|
||||
JSONObject(it).toString(4)
|
||||
} ?: stringProvider.getString(R.string.encryption_information_decryption_error)
|
||||
this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, decryptedContent))
|
||||
}
|
||||
this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, state.eventId))
|
||||
|
||||
if (session.sessionParams.credentials.userId != event.root.sender && event.root.getClearType() == EventType.MESSAGE) {
|
||||
if (session.sessionParams.credentials.userId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
|
||||
//not sent by me
|
||||
this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, state.eventId))
|
||||
}
|
||||
@ -207,12 +213,12 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||
}
|
||||
}
|
||||
|
||||
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
|
||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
//TODO if user is admin or moderator
|
||||
return event.root.sender == myUserId
|
||||
}
|
||||
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
|
||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
//TODO if user is admin or moderator
|
||||
return event.root.senderId == myUserId
|
||||
}
|
||||
|
||||
private fun canViewReactions(event: TimelineEvent): Boolean {
|
||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
@ -221,16 +227,17 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
|
||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
//TODO if user is admin or moderator
|
||||
val messageContent = event.root.content.toModel<MessageContent>()
|
||||
return event.root.sender == myUserId && (
|
||||
messageContent?.type == MessageType.MSGTYPE_TEXT
|
||||
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
|
||||
)
|
||||
}
|
||||
|
||||
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
|
||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
//TODO if user is admin or moderator
|
||||
val messageContent = event.root.content.toModel<MessageContent>()
|
||||
return event.root.senderId == myUserId && (
|
||||
messageContent?.type == MessageType.MSGTYPE_TEXT
|
||||
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun canCopy(type: String?): Boolean {
|
||||
|
@ -19,9 +19,6 @@ import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.transition.TransitionManager
|
||||
import butterknife.BindView
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
@ -29,6 +26,7 @@ import im.vector.riotredesign.EmojiCompatFontProvider
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.di.ScreenComponent
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.adapter_item_action_quick_reaction.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@ -38,21 +36,6 @@ class QuickReactionFragment : VectorBaseFragment() {
|
||||
|
||||
private val viewModel: QuickReactionViewModel by fragmentViewModel(QuickReactionViewModel::class)
|
||||
|
||||
@BindView(R.id.root_layout)
|
||||
lateinit var rootLayout: ConstraintLayout
|
||||
|
||||
@BindView(R.id.quick_react_1_text)
|
||||
lateinit var quickReact1Text: TextView
|
||||
|
||||
@BindView(R.id.quick_react_2_text)
|
||||
lateinit var quickReact2Text: TextView
|
||||
|
||||
@BindView(R.id.quick_react_3_text)
|
||||
lateinit var quickReact3Text: TextView
|
||||
|
||||
@BindView(R.id.quick_react_4_text)
|
||||
lateinit var quickReact4Text: TextView
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
@Inject lateinit var fontProvider: EmojiCompatFontProvider
|
||||
@ -64,77 +47,35 @@ class QuickReactionFragment : VectorBaseFragment() {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
lateinit var textViews: List<TextView>
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
quickReact1Text.text = QuickReactionViewModel.AGREE_POSITIVE
|
||||
quickReact2Text.text = QuickReactionViewModel.AGREE_NEGATIVE
|
||||
quickReact3Text.text = QuickReactionViewModel.LIKE_POSITIVE
|
||||
quickReact4Text.text = QuickReactionViewModel.LIKE_NEGATIVE
|
||||
|
||||
listOf(quickReact1Text, quickReact2Text, quickReact3Text, quickReact4Text).forEach {
|
||||
it.typeface = fontProvider.typeface ?: Typeface.DEFAULT
|
||||
}
|
||||
|
||||
//configure click listeners
|
||||
quickReact1Text.setOnClickListener {
|
||||
viewModel.toggleAgree(true)
|
||||
}
|
||||
quickReact2Text.setOnClickListener {
|
||||
viewModel.toggleAgree(false)
|
||||
}
|
||||
quickReact3Text.setOnClickListener {
|
||||
viewModel.toggleLike(true)
|
||||
}
|
||||
quickReact4Text.setOnClickListener {
|
||||
viewModel.toggleLike(false)
|
||||
textViews = listOf(quickReaction0, quickReaction1, quickReaction2, quickReaction3,
|
||||
quickReaction4, quickReaction5, quickReaction6, quickReaction7)
|
||||
textViews.forEachIndexed { index, textView ->
|
||||
textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT
|
||||
textView.setOnClickListener {
|
||||
viewModel.didSelect(index)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
|
||||
TransitionManager.beginDelayedTransition(rootLayout)
|
||||
when (it.agreeTriggleState) {
|
||||
TriggleState.NONE -> {
|
||||
quickReact1Text.alpha = 1f
|
||||
quickReact2Text.alpha = 1f
|
||||
}
|
||||
TriggleState.FIRST -> {
|
||||
quickReact1Text.alpha = 1f
|
||||
quickReact2Text.alpha = 0.2f
|
||||
|
||||
}
|
||||
TriggleState.SECOND -> {
|
||||
quickReact1Text.alpha = 0.2f
|
||||
quickReact2Text.alpha = 1f
|
||||
}
|
||||
}
|
||||
when (it.likeTriggleState) {
|
||||
TriggleState.NONE -> {
|
||||
quickReact3Text.alpha = 1f
|
||||
quickReact4Text.alpha = 1f
|
||||
}
|
||||
TriggleState.FIRST -> {
|
||||
quickReact3Text.alpha = 1f
|
||||
quickReact4Text.alpha = 0.2f
|
||||
|
||||
}
|
||||
TriggleState.SECOND -> {
|
||||
quickReact3Text.alpha = 0.2f
|
||||
quickReact4Text.alpha = 1f
|
||||
}
|
||||
it.quickStates.forEachIndexed { index, qs ->
|
||||
textViews[index].text = qs.reaction
|
||||
textViews[index].alpha = if (qs.isSelected) 0.2f else 1f
|
||||
}
|
||||
|
||||
if (it.selectionResult != null) {
|
||||
val clikedOn = it.selectionResult.first
|
||||
interactionListener?.didQuickReactWith(clikedOn, QuickReactionViewModel.getOpposite(clikedOn)
|
||||
?: "", it.selectionResult.second, it.eventId)
|
||||
if (it.result != null) {
|
||||
interactionListener?.didQuickReactWith(it.result.reaction, it.result.isSelected, it.eventId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface InteractionListener {
|
||||
fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List<String>, eventId: String)
|
||||
fun didQuickReactWith(clikedOn: String, add: Boolean, eventId: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -28,27 +28,23 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInf
|
||||
/**
|
||||
* Quick reactions state, it's a toggle with 3rd state
|
||||
*/
|
||||
enum class TriggleState {
|
||||
NONE,
|
||||
FIRST,
|
||||
SECOND
|
||||
}
|
||||
data class ToggleState(
|
||||
val reaction: String,
|
||||
val isSelected: Boolean
|
||||
)
|
||||
|
||||
data class QuickReactionState(
|
||||
val roomId: String,
|
||||
val eventId: String,
|
||||
val informationData: MessageInformationData,
|
||||
val agreeTriggleState: TriggleState = TriggleState.NONE,
|
||||
val likeTriggleState: TriggleState = TriggleState.NONE,
|
||||
val quickStates: List<ToggleState> = emptyList(),
|
||||
val result: ToggleState? = null
|
||||
/** Pair of 'clickedOn' and current toggles state*/
|
||||
val selectionResult: Pair<String, List<String>>? = null
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Quick reaction view model
|
||||
*/
|
||||
@ -62,20 +58,7 @@ class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState:
|
||||
|
||||
companion object : MvRxViewModelFactory<QuickReactionViewModel, QuickReactionState> {
|
||||
|
||||
const val AGREE_POSITIVE = "👍"
|
||||
const val AGREE_NEGATIVE = "👎"
|
||||
const val LIKE_POSITIVE = "🙂"
|
||||
const val LIKE_NEGATIVE = "😔"
|
||||
|
||||
fun getOpposite(reaction: String): String? {
|
||||
return when (reaction) {
|
||||
AGREE_POSITIVE -> AGREE_NEGATIVE
|
||||
AGREE_NEGATIVE -> AGREE_POSITIVE
|
||||
LIKE_POSITIVE -> LIKE_NEGATIVE
|
||||
LIKE_NEGATIVE -> LIKE_POSITIVE
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: QuickReactionState): QuickReactionViewModel? {
|
||||
val fragment: QuickReactionFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
@ -89,84 +72,18 @@ class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState:
|
||||
|
||||
private fun reduceState(state: QuickReactionState): QuickReactionState {
|
||||
val event = session.getRoom(state.roomId)?.getTimeLineEvent(state.eventId) ?: return state
|
||||
var agreeTriggle: TriggleState = TriggleState.NONE
|
||||
var likeTriggle: TriggleState = TriggleState.NONE
|
||||
event.annotations?.reactionsSummary?.forEach {
|
||||
//it.addedByMe
|
||||
if (it.addedByMe) {
|
||||
if (AGREE_POSITIVE == it.key) {
|
||||
agreeTriggle = TriggleState.FIRST
|
||||
} else if (AGREE_NEGATIVE == it.key) {
|
||||
agreeTriggle = TriggleState.SECOND
|
||||
}
|
||||
|
||||
if (LIKE_POSITIVE == it.key) {
|
||||
likeTriggle = TriggleState.FIRST
|
||||
} else if (LIKE_NEGATIVE == it.key) {
|
||||
likeTriggle = TriggleState.SECOND
|
||||
}
|
||||
}
|
||||
val summary = event.annotations?.reactionsSummary
|
||||
val quickReactions = quickEmojis.map { emoji ->
|
||||
ToggleState(emoji, summary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
|
||||
}
|
||||
return state.copy(
|
||||
agreeTriggleState = agreeTriggle,
|
||||
likeTriggleState = likeTriggle
|
||||
)
|
||||
return state.copy(quickStates = quickReactions)
|
||||
}
|
||||
|
||||
fun toggleAgree(isFirst: Boolean) = withState {
|
||||
if (isFirst) {
|
||||
setState {
|
||||
val newTriggle = if (it.agreeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST
|
||||
copy(
|
||||
agreeTriggleState = newTriggle,
|
||||
selectionResult = Pair(AGREE_POSITIVE, getReactions(this, newTriggle, null))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
val newTriggle = if (it.agreeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND
|
||||
copy(
|
||||
agreeTriggleState = agreeTriggleState,
|
||||
selectionResult = Pair(AGREE_NEGATIVE, getReactions(this, newTriggle, null))
|
||||
)
|
||||
}
|
||||
fun didSelect(index: Int) = withState {
|
||||
val isSelected = it.quickStates[index].isSelected
|
||||
setState {
|
||||
copy(result = ToggleState(it.quickStates[index].reaction, !isSelected))
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleLike(isFirst: Boolean) = withState {
|
||||
if (isFirst) {
|
||||
setState {
|
||||
val newTriggle = if (it.likeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST
|
||||
copy(
|
||||
likeTriggleState = newTriggle,
|
||||
selectionResult = Pair(LIKE_POSITIVE, getReactions(this, null, newTriggle))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
val newTriggle = if (it.likeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND
|
||||
copy(
|
||||
likeTriggleState = newTriggle,
|
||||
selectionResult = Pair(LIKE_NEGATIVE, getReactions(this, null, newTriggle))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getReactions(state: QuickReactionState, newState1: TriggleState?, newState2: TriggleState?): List<String> {
|
||||
return ArrayList<String>(4).apply {
|
||||
when (newState2 ?: state.likeTriggleState) {
|
||||
TriggleState.FIRST -> add(LIKE_POSITIVE)
|
||||
TriggleState.SECOND -> add(LIKE_NEGATIVE)
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
when (newState1 ?: state.agreeTriggleState) {
|
||||
TriggleState.FIRST -> add(AGREE_POSITIVE)
|
||||
TriggleState.SECOND -> add(AGREE_NEGATIVE)
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -46,13 +46,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: DisplayReactionsViewState): ViewReactionViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<ViewReactionViewModel, DisplayReactionsViewState> {
|
||||
companion object : MvRxViewModelFactory<ViewReactionViewModel, DisplayReactionsViewState> {
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? {
|
||||
val fragment: ViewReactionBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.viewReactionViewModelFactory.create(state)
|
||||
}
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? {
|
||||
val fragment: ViewReactionBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.viewReactionViewModelFactory.create(state)
|
||||
}
|
||||
|
||||
}
|
||||
@ -76,7 +77,7 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
|
||||
ReactionInfo(
|
||||
event.root.eventId!!,
|
||||
it.key,
|
||||
event.root.sender ?: "",
|
||||
event.root.senderId ?: "",
|
||||
event.senderName,
|
||||
timelineDateFormatter.formatMessageHour(localDate)
|
||||
)
|
||||
|
@ -23,13 +23,15 @@ import javax.inject.Inject
|
||||
|
||||
class DefaultItemFactory @Inject constructor(){
|
||||
|
||||
fun create(event: TimelineEvent, exception: Exception? = null): DefaultItem? {
|
||||
fun create(event: TimelineEvent, highlight: Boolean, exception: Exception? = null): DefaultItem? {
|
||||
val text = if (exception == null) {
|
||||
"${event.root.getClearType()} events are not yet handled"
|
||||
} else {
|
||||
"an exception occurred when rendering the event ${event.root.eventId}"
|
||||
}
|
||||
return DefaultItem_().text(text)
|
||||
return DefaultItem_()
|
||||
.text(text)
|
||||
.highlighted(highlight)
|
||||
}
|
||||
|
||||
}
|
@ -16,31 +16,37 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.View
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotredesign.core.resources.ColorProvider
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.core.utils.DebouncedClickListener
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
|
||||
import javax.inject.Inject
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.util.MessageInformationDataFactory
|
||||
import me.gujun.android.span.span
|
||||
|
||||
// This class handles timeline event who haven't been successfully decrypted
|
||||
class EncryptedItemFactory @Inject constructor(private val stringProvider: StringProvider,
|
||||
private val avatarRenderer: AvatarRenderer) {
|
||||
// This class handles timeline events who haven't been successfully decrypted
|
||||
class EncryptedItemFactory(private val messageInformationDataFactory: MessageInformationDataFactory,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val stringProvider: StringProvider,
|
||||
private val avatarRenderer: AvatarRenderer) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||
event.root.eventId ?: return null
|
||||
|
||||
fun create(timelineEvent: TimelineEvent): VectorEpoxyModel<*>? {
|
||||
return when {
|
||||
EventType.ENCRYPTED == timelineEvent.root.getClearType() -> {
|
||||
val cryptoError = timelineEvent.root.mCryptoError
|
||||
EventType.ENCRYPTED == event.root.getClearType() -> {
|
||||
val cryptoError = event.root.mCryptoError
|
||||
val errorDescription =
|
||||
if (cryptoError?.code == MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE) {
|
||||
stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
|
||||
@ -49,23 +55,34 @@ class EncryptedItemFactory @Inject constructor(private val stringProvider: Strin
|
||||
}
|
||||
|
||||
val message = stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
|
||||
val spannableStr = SpannableString(message)
|
||||
spannableStr.setSpan(StyleSpan(Typeface.ITALIC), 0, message.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
val spannableStr = span(message) {
|
||||
textStyle = "italic"
|
||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||
}
|
||||
|
||||
// TODO This is not correct format for error, change it
|
||||
val informationData = MessageInformationData(
|
||||
eventId = timelineEvent.root.eventId ?: "?",
|
||||
senderId = timelineEvent.root.sender ?: "",
|
||||
sendState = timelineEvent.sendState,
|
||||
avatarUrl = timelineEvent.senderAvatar(),
|
||||
memberName = timelineEvent.senderName(),
|
||||
showInformation = false
|
||||
)
|
||||
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
return NoticeItem_()
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.noticeText(spannableStr)
|
||||
|
||||
return MessageTextItem_()
|
||||
.message(spannableStr)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.urlClickCallback(callback)
|
||||
.cellClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onEncryptedMessageClicked(informationData, view)
|
||||
}))
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, null, view)
|
||||
?: false
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventConte
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||
@ -34,11 +35,14 @@ import javax.inject.Inject
|
||||
class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider,
|
||||
private val avatarRenderer: AvatarRenderer) {
|
||||
|
||||
fun create(event: TimelineEvent): NoticeItem? {
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.BaseCallback?): NoticeItem? {
|
||||
|
||||
val text = buildNoticeText(event.root, event.senderName) ?: return null
|
||||
val informationData = MessageInformationData(
|
||||
eventId = event.root.eventId ?: "?",
|
||||
senderId = event.root.sender ?: "",
|
||||
senderId = event.root.senderId ?: "",
|
||||
sendState = event.sendState,
|
||||
avatarUrl = event.senderAvatar(),
|
||||
memberName = event.senderName(),
|
||||
@ -48,6 +52,8 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.noticeText(text)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.baseCallback(callback)
|
||||
}
|
||||
|
||||
private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
|
||||
|
@ -25,7 +25,6 @@ import android.text.style.RelativeSizeSpan
|
||||
import android.view.View
|
||||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
@ -35,18 +34,16 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.EmojiCompatFontProvider
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
import im.vector.riotredesign.core.linkify.VectorLinkify
|
||||
import im.vector.riotredesign.core.resources.ColorProvider
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.core.utils.DebouncedClickListener
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.getColorFromUserId
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.*
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.util.MessageInformationDataFactory
|
||||
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||
@ -57,56 +54,26 @@ class MessageItemFactory @Inject constructor(
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||
private val timelineDateFormatter: TimelineDateFormatter,
|
||||
private val htmlRenderer: EventHtmlRenderer,
|
||||
private val stringProvider: StringProvider,
|
||||
private val emojiCompatFontProvider: EmojiCompatFontProvider,
|
||||
private val imageContentRenderer: ImageContentRenderer,
|
||||
private val messageInformationDataFactory: MessageInformationDataFactory,
|
||||
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder) {
|
||||
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?
|
||||
): VectorEpoxyModel<*>? {
|
||||
event.root.eventId ?: return null
|
||||
|
||||
val eventId = event.root.eventId ?: return null
|
||||
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||
?: false
|
||||
|
||||
val showInformation = addDaySeparator
|
||||
|| event.senderAvatar != nextEvent?.senderAvatar
|
||||
|| event.senderName != nextEvent?.senderName
|
||||
|| nextEvent?.root?.getClearType() != EventType.MESSAGE
|
||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||
|
||||
val time = timelineDateFormatter.formatMessageHour(date)
|
||||
val avatarUrl = event.senderAvatar
|
||||
val memberName = event.senderName ?: event.root.sender ?: ""
|
||||
val formattedMemberName = span(memberName) {
|
||||
textColor = colorProvider.getColor(getColorFromUserId(event.root.sender
|
||||
?: ""))
|
||||
}
|
||||
val hasBeenEdited = event.annotations?.editSummary != null
|
||||
val informationData = MessageInformationData(eventId = eventId,
|
||||
senderId = event.root.sender ?: "",
|
||||
sendState = event.sendState,
|
||||
time = time,
|
||||
avatarUrl = avatarUrl,
|
||||
memberName = formattedMemberName,
|
||||
showInformation = showInformation,
|
||||
orderedReactionList = event.annotations?.reactionsSummary?.map {
|
||||
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
|
||||
},
|
||||
hasBeenEdited = hasBeenEdited
|
||||
)
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
|
||||
if (event.root.unsignedData?.redactedEvent != null) {
|
||||
//message is redacted
|
||||
return buildRedactedItem(informationData, callback)
|
||||
return buildRedactedItem(informationData, highlight, callback)
|
||||
}
|
||||
|
||||
val messageContent: MessageContent =
|
||||
@ -124,31 +91,33 @@ class MessageItemFactory @Inject constructor(
|
||||
return when (messageContent) {
|
||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
|
||||
informationData,
|
||||
hasBeenEdited,
|
||||
event.annotations?.editSummary,
|
||||
highlight,
|
||||
callback)
|
||||
is MessageTextContent -> buildTextMessageItem(event.sendState,
|
||||
messageContent,
|
||||
informationData,
|
||||
hasBeenEdited,
|
||||
event.annotations?.editSummary,
|
||||
highlight,
|
||||
callback
|
||||
)
|
||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
|
||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
|
||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
|
||||
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback)
|
||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent)
|
||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent, highlight)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAudioMessageItem(messageContent: MessageAudioContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageFileItem? {
|
||||
return MessageFileItem_()
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.filename(messageContent.body)
|
||||
.iconRes(R.drawable.filetype_audio)
|
||||
@ -170,10 +139,12 @@ class MessageItemFactory @Inject constructor(
|
||||
|
||||
private fun buildFileMessageItem(messageContent: MessageFileContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageFileItem? {
|
||||
return MessageFileItem_()
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.filename(messageContent.body)
|
||||
.reactionPillCallback(callback)
|
||||
@ -193,13 +164,16 @@ class MessageItemFactory @Inject constructor(
|
||||
}))
|
||||
}
|
||||
|
||||
private fun buildNotHandledMessageItem(messageContent: MessageContent): DefaultItem? {
|
||||
private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? {
|
||||
val text = "${messageContent.type} message events are not yet handled"
|
||||
return DefaultItem_().text(text)
|
||||
return DefaultItem_()
|
||||
.text(text)
|
||||
.highlighted(highlight)
|
||||
}
|
||||
|
||||
private fun buildImageMessageItem(messageContent: MessageImageContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
|
||||
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
@ -219,6 +193,7 @@ class MessageItemFactory @Inject constructor(
|
||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||
.playable(messageContent.info?.mimeType == "image/gif")
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.mediaData(data)
|
||||
.reactionPillCallback(callback)
|
||||
@ -239,6 +214,7 @@ class MessageItemFactory @Inject constructor(
|
||||
|
||||
private fun buildVideoMessageItem(messageContent: MessageVideoContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
|
||||
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
@ -263,6 +239,7 @@ class MessageItemFactory @Inject constructor(
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.playable(true)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.mediaData(thumbnailData)
|
||||
.reactionPillCallback(callback)
|
||||
@ -281,8 +258,8 @@ class MessageItemFactory @Inject constructor(
|
||||
private fun buildTextMessageItem(sendState: SendState,
|
||||
messageContent: MessageTextContent,
|
||||
informationData: MessageInformationData,
|
||||
hasBeenEdited: Boolean,
|
||||
editSummary: EditAggregatedSummary?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val bodyToUse = messageContent.formattedBody?.let {
|
||||
@ -293,7 +270,7 @@ class MessageItemFactory @Inject constructor(
|
||||
|
||||
return MessageTextItem_()
|
||||
.apply {
|
||||
if (hasBeenEdited) {
|
||||
if (informationData.hasBeenEdited) {
|
||||
val spannable = annotateWithEdited(linkifiedBody, callback, informationData, editSummary)
|
||||
message(spannable)
|
||||
} else {
|
||||
@ -302,7 +279,9 @@ class MessageItemFactory @Inject constructor(
|
||||
}
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.urlClickCallback(callback)
|
||||
.reactionPillCallback(callback)
|
||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||
//click on the text
|
||||
@ -352,6 +331,7 @@ class MessageItemFactory @Inject constructor(
|
||||
|
||||
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val message = messageContent.body.let {
|
||||
@ -366,8 +346,10 @@ class MessageItemFactory @Inject constructor(
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.message(message)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.reactionPillCallback(callback)
|
||||
.urlClickCallback(callback)
|
||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||
.memberClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
@ -385,8 +367,8 @@ class MessageItemFactory @Inject constructor(
|
||||
|
||||
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
|
||||
informationData: MessageInformationData,
|
||||
hasBeenEdited: Boolean,
|
||||
editSummary: EditAggregatedSummary?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val message = messageContent.body.let {
|
||||
@ -395,7 +377,7 @@ class MessageItemFactory @Inject constructor(
|
||||
}
|
||||
return MessageTextItem_()
|
||||
.apply {
|
||||
if (hasBeenEdited) {
|
||||
if (informationData.hasBeenEdited) {
|
||||
val spannable = annotateWithEdited(message, callback, informationData, editSummary)
|
||||
message(spannable)
|
||||
} else {
|
||||
@ -404,8 +386,10 @@ class MessageItemFactory @Inject constructor(
|
||||
}
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.reactionPillCallback(callback)
|
||||
.urlClickCallback(callback)
|
||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||
.cellClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
@ -418,10 +402,12 @@ class MessageItemFactory @Inject constructor(
|
||||
}
|
||||
|
||||
private fun buildRedactedItem(informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): RedactedMessageItem? {
|
||||
return RedactedMessageItem_()
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
}
|
||||
|
||||
|
@ -31,11 +31,12 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
||||
private val avatarRenderer: AvatarRenderer) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||
val formattedText = eventFormatter.format(event) ?: return null
|
||||
val informationData = MessageInformationData(
|
||||
eventId = event.root.eventId ?: "?",
|
||||
senderId = event.root.sender ?: "",
|
||||
senderId = event.root.senderId ?: "",
|
||||
sendState = event.sendState,
|
||||
avatarUrl = event.senderAvatar(),
|
||||
memberName = event.senderName(),
|
||||
@ -45,6 +46,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
||||
return NoticeItem_()
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.noticeText(formattedText)
|
||||
.highlighted(highlight)
|
||||
.informationData(informationData)
|
||||
.baseCallback(callback)
|
||||
}
|
||||
|
@ -38,11 +38,13 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
eventIdToHighlight: String?,
|
||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
||||
val highlight = event.root.eventId == eventIdToHighlight
|
||||
|
||||
val computedModel = try {
|
||||
when (event.root.getClearType()) {
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||
// State and call
|
||||
EventType.STATE_ROOM_NAME,
|
||||
EventType.STATE_ROOM_TOPIC,
|
||||
@ -50,38 +52,38 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
EventType.STATE_HISTORY_VISIBILITY,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> noticeItemFactory.create(event, callback)
|
||||
EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback)
|
||||
|
||||
// Crypto
|
||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event)
|
||||
EventType.ENCRYPTED -> encryptedItemFactory.create(event)
|
||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
||||
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
||||
|
||||
// Unhandled event types (yet)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
EventType.STICKER,
|
||||
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)
|
||||
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event, highlight)
|
||||
|
||||
else -> {
|
||||
//These are just for debug to display hidden event, they should be filtered out in normal mode
|
||||
if (TimelineDisplayableEvents.DEBUG_HIDDEN_EVENT) {
|
||||
val informationData = MessageInformationData(eventId = event.root.eventId
|
||||
?: "?",
|
||||
senderId = event.root.sender
|
||||
?: "",
|
||||
sendState = event.sendState,
|
||||
time = "",
|
||||
avatarUrl = null,
|
||||
memberName = "",
|
||||
showInformation = false
|
||||
?: "?",
|
||||
senderId = event.root.senderId ?: "",
|
||||
sendState = event.sendState,
|
||||
time = "",
|
||||
avatarUrl = null,
|
||||
memberName = "",
|
||||
showInformation = false
|
||||
)
|
||||
val messageContent = event.root.content.toModel<MessageContent>()
|
||||
?: MessageDefaultContent("", "", null, null)
|
||||
?: MessageDefaultContent("", "", null, null)
|
||||
MessageTextItem_()
|
||||
.informationData(informationData)
|
||||
.message("{ \"type\": ${event.root.type} }")
|
||||
.highlighted(highlight)
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||
?: false
|
||||
?: false
|
||||
}
|
||||
} else {
|
||||
Timber.w("Ignored event (type: ${event.root.type}")
|
||||
@ -91,7 +93,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "failed to create message item")
|
||||
defaultItemFactory.create(event, e)
|
||||
defaultItemFactory.create(event, highlight, e)
|
||||
}
|
||||
return (computedModel ?: EmptyItem_())
|
||||
}
|
||||
|
@ -20,12 +20,7 @@ import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||
import im.vector.matrix.android.api.session.room.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
@ -38,13 +33,29 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||
|
||||
fun format(timelineEvent: TimelineEvent): CharSequence? {
|
||||
return when (val type = timelineEvent.root.getClearType()) {
|
||||
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderName)
|
||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderName)
|
||||
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
|
||||
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderName)
|
||||
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.senderName)
|
||||
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun format(event: Event, senderName: String?): CharSequence? {
|
||||
return when (val type = event.getClearType()) {
|
||||
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
|
||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName)
|
||||
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> formatCallEvent(event, senderName)
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
null
|
||||
@ -72,7 +83,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||
|
||||
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
|
||||
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
|
||||
?: return null
|
||||
?: return null
|
||||
|
||||
val formattedVisibility = when (historyVisibility) {
|
||||
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
|
||||
@ -117,12 +128,12 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
|
||||
val displayNameText = when {
|
||||
prevEventContent?.displayName.isNullOrEmpty() ->
|
||||
stringProvider.getString(R.string.notice_display_name_set, event.sender, eventContent?.displayName)
|
||||
stringProvider.getString(R.string.notice_display_name_set, event.senderId, eventContent?.displayName)
|
||||
eventContent?.displayName.isNullOrEmpty() ->
|
||||
stringProvider.getString(R.string.notice_display_name_removed, event.sender, prevEventContent?.displayName)
|
||||
stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
|
||||
else ->
|
||||
stringProvider.getString(R.string.notice_display_name_changed_from,
|
||||
event.sender, prevEventContent?.displayName, eventContent?.displayName)
|
||||
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
|
||||
}
|
||||
displayText.append(displayNameText)
|
||||
}
|
||||
@ -140,8 +151,8 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||
}
|
||||
|
||||
private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
||||
val senderDisplayName = senderName ?: event.sender
|
||||
val targetDisplayName = eventContent?.displayName ?: event.sender
|
||||
val senderDisplayName = senderName ?: event.senderId
|
||||
val targetDisplayName = eventContent?.displayName ?: event.senderId
|
||||
return when {
|
||||
Membership.INVITE == eventContent?.membership -> {
|
||||
// TODO get userId
|
||||
@ -149,7 +160,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||
when {
|
||||
eventContent.thirdPartyInvite != null ->
|
||||
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
||||
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
||||
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
||||
TextUtils.equals(event.stateKey, selfUserId) ->
|
||||
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
||||
event.stateKey.isNullOrEmpty() ->
|
||||
@ -162,7 +173,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||
stringProvider.getString(R.string.notice_room_join, senderDisplayName)
|
||||
Membership.LEAVE == eventContent?.membership ->
|
||||
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
|
||||
return if (TextUtils.equals(event.sender, event.stateKey)) {
|
||||
return if (TextUtils.equals(event.senderId, event.stateKey)) {
|
||||
if (prevEventContent?.membership == Membership.INVITE) {
|
||||
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
|
||||
} else {
|
||||
|
@ -22,7 +22,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
object RoomMemberEventHelper {
|
||||
|
||||
|
||||
}
|
@ -85,12 +85,11 @@ fun TimelineEvent.senderAvatar(): String? {
|
||||
|
||||
fun TimelineEvent.senderName(): String? {
|
||||
// We might have no senderName when user leave, so we try to get it from prevContent
|
||||
return senderName
|
||||
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
|
||||
root.prevContent.toModel<RoomMember>()?.displayName
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return when {
|
||||
senderName != null -> getDisambiguatedDisplayName()
|
||||
root.type == EventType.STATE_ROOM_MEMBER -> root.prevContent.toModel<RoomMember>()?.displayName
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun TimelineEvent.canBeMerged(): Boolean {
|
||||
|
@ -85,7 +85,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
if (informationData.showInformation) {
|
||||
|
||||
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
|
||||
val size = dpToPx(avatarStyle.avatarSizeDP, holder.view.context)
|
||||
height = size
|
||||
|
@ -19,20 +19,28 @@ import android.view.View
|
||||
import android.view.ViewStub
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.constraintlayout.widget.Guideline
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotredesign.core.platform.CheckableView
|
||||
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
|
||||
|
||||
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
|
||||
|
||||
var avatarStyle: AvatarStyle = Companion.AvatarStyle.SMALL
|
||||
var avatarStyle: AvatarStyle = AvatarStyle.SMALL
|
||||
|
||||
// To use for instance when opening a permalink with an eventId
|
||||
@EpoxyAttribute
|
||||
var highlighted: Boolean = false
|
||||
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
//optimize?
|
||||
val px = dpToPx(avatarStyle.avatarSizeDP, holder.view.context)
|
||||
val px = dpToPx(avatarStyle.avatarSizeDP + 8, holder.view.context)
|
||||
holder.leftGuideline.setGuidelineBegin(px)
|
||||
|
||||
holder.checkableBackground.isChecked = highlighted
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +54,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
||||
abstract class BaseHolder : VectorEpoxyHolder() {
|
||||
|
||||
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
|
||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||
|
||||
@IdRes
|
||||
abstract fun getStubId(): Int
|
||||
|
@ -25,6 +25,7 @@ import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.utils.containsOnlyEmojis
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.html.PillImageSpan
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@ -41,15 +42,18 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
override lateinit var avatarRenderer: AvatarRenderer
|
||||
@EpoxyAttribute
|
||||
override lateinit var informationData: MessageInformationData
|
||||
@EpoxyAttribute
|
||||
var urlClickCallback: TimelineEventController.UrlClickCallback? = null
|
||||
|
||||
val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
|
||||
it.setOnLinkClickListener { textView, url ->
|
||||
//Return false to let android manage the click on the link
|
||||
false
|
||||
// TODO Move this instantiation somewhere else?
|
||||
private val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
|
||||
it.setOnLinkClickListener { _, url ->
|
||||
//Return false to let android manage the click on the link, or true if the link is handled by the application
|
||||
urlClickCallback?.onUrlClicked(url) == true
|
||||
}
|
||||
it.setOnLinkLongClickListener { textView, url ->
|
||||
it.setOnLinkLongClickListener { _, url ->
|
||||
//Long clicks are handled by parent, return true to block android to do something with url
|
||||
true
|
||||
urlClickCallback?.onUrlLongClicked(url) == true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,11 +41,9 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||
var baseCallback: TimelineEventController.BaseCallback? = null
|
||||
|
||||
private var longClickListener = View.OnLongClickListener {
|
||||
baseCallback?.onEventLongClicked(informationData, null, it)
|
||||
baseCallback != null
|
||||
return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true
|
||||
}
|
||||
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.noticeTextView.text = noticeText
|
||||
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.util
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
import im.vector.riotredesign.core.resources.ColorProvider
|
||||
import im.vector.riotredesign.features.home.getColorFromUserId
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.ReactionInfoData
|
||||
import me.gujun.android.span.span
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
|
||||
*/
|
||||
class MessageInformationDataFactory @Inject constructor(private val timelineDateFormatter: TimelineDateFormatter,
|
||||
private val colorProvider: ColorProvider) {
|
||||
|
||||
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
|
||||
// Non nullability has been tested before
|
||||
val eventId = event.root.eventId!!
|
||||
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||
?: false
|
||||
|
||||
val showInformation =
|
||||
addDaySeparator
|
||||
|| event.senderAvatar != nextEvent?.senderAvatar
|
||||
|| event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName()
|
||||
|| (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED)
|
||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||
|
||||
val time = timelineDateFormatter.formatMessageHour(date)
|
||||
val avatarUrl = event.senderAvatar
|
||||
val memberName = event.getDisambiguatedDisplayName()
|
||||
val formattedMemberName = span(memberName) {
|
||||
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId
|
||||
?: ""))
|
||||
}
|
||||
|
||||
val hasBeenEdited = event.annotations?.editSummary != null
|
||||
|
||||
return MessageInformationData(
|
||||
eventId = eventId,
|
||||
senderId = event.root.senderId ?: "",
|
||||
sendState = event.sendState,
|
||||
time = time,
|
||||
avatarUrl = avatarUrl,
|
||||
memberName = formattedMemberName,
|
||||
showInformation = showInformation,
|
||||
orderedReactionList = event.annotations?.reactionsSummary?.map {
|
||||
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
|
||||
},
|
||||
hasBeenEdited = hasBeenEdited
|
||||
)
|
||||
}
|
||||
}
|
@ -23,11 +23,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Incomplete
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.*
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
@ -82,7 +78,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
|
||||
setupRecyclerView()
|
||||
roomListViewModel.subscribe { renderState(it) }
|
||||
roomListViewModel.openRoomLiveData.observeEvent(this) {
|
||||
navigator.openRoom(it, requireActivity())
|
||||
navigator.openRoom(requireActivity(), it)
|
||||
}
|
||||
|
||||
createChatFabMenu.listener = this
|
||||
@ -103,25 +99,26 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
|
||||
}
|
||||
|
||||
// Hide FAB when list is scrolling
|
||||
epoxyRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
createChatFabMenu.removeCallbacks(showFabRunnable)
|
||||
epoxyRecyclerView.addOnScrollListener(
|
||||
object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
createChatFabMenu.removeCallbacks(showFabRunnable)
|
||||
|
||||
when (newState) {
|
||||
RecyclerView.SCROLL_STATE_IDLE -> {
|
||||
createChatFabMenu.postDelayed(showFabRunnable, 1000)
|
||||
}
|
||||
RecyclerView.SCROLL_STATE_DRAGGING,
|
||||
RecyclerView.SCROLL_STATE_SETTLING -> {
|
||||
when (roomListParams.displayMode) {
|
||||
DisplayMode.HOME -> createChatFabMenu.hide()
|
||||
DisplayMode.PEOPLE -> createChatRoomButton.hide()
|
||||
else -> createGroupRoomButton.hide()
|
||||
when (newState) {
|
||||
RecyclerView.SCROLL_STATE_IDLE -> {
|
||||
createChatFabMenu.postDelayed(showFabRunnable, 1000)
|
||||
}
|
||||
RecyclerView.SCROLL_STATE_DRAGGING,
|
||||
RecyclerView.SCROLL_STATE_SETTLING -> {
|
||||
when (roomListParams.displayMode) {
|
||||
DisplayMode.HOME -> createChatFabMenu.hide()
|
||||
DisplayMode.PEOPLE -> createChatRoomButton.hide()
|
||||
else -> createGroupRoomButton.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -230,7 +227,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
|
||||
return super.onBackPressed()
|
||||
}
|
||||
|
||||
// RoomSummaryController.Callback **************************************************************
|
||||
// RoomSummaryController.Callback **************************************************************
|
||||
|
||||
override fun onRoomSelected(room: RoomSummary) {
|
||||
roomListViewModel.accept(RoomListActions.SelectRoom(room))
|
||||
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
|
||||
class RoomSummaryComparator
|
||||
: Comparator<RoomSummary> {
|
||||
|
||||
override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
|
||||
val retValue: Int
|
||||
var leftHighlightCount = 0
|
||||
var rightHighlightCount = 0
|
||||
var leftNotificationCount = 0
|
||||
var rightNotificationCount = 0
|
||||
var rightTimestamp = 0L
|
||||
var leftTimestamp = 0L
|
||||
|
||||
if (null != leftRoomSummary) {
|
||||
leftHighlightCount = leftRoomSummary.highlightCount
|
||||
leftNotificationCount = leftRoomSummary.notificationCount
|
||||
leftTimestamp = leftRoomSummary.lastMessage?.originServerTs ?: 0
|
||||
}
|
||||
if (null != rightRoomSummary) {
|
||||
rightHighlightCount = rightRoomSummary.highlightCount
|
||||
rightNotificationCount = rightRoomSummary.notificationCount
|
||||
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
|
||||
}
|
||||
|
||||
if (rightRoomSummary?.lastMessage == null) {
|
||||
retValue = -1
|
||||
} else if (leftRoomSummary?.lastMessage == null) {
|
||||
retValue = 1
|
||||
} else if (rightHighlightCount > 0 && leftHighlightCount == 0) {
|
||||
retValue = 1
|
||||
} else if (rightHighlightCount == 0 && leftHighlightCount > 0) {
|
||||
retValue = -1
|
||||
} else if (rightNotificationCount > 0 && leftNotificationCount == 0) {
|
||||
retValue = 1
|
||||
} else if (rightNotificationCount == 0 && leftNotificationCount > 0) {
|
||||
retValue = -1
|
||||
} else {
|
||||
val deltaTimestamp = rightTimestamp - leftTimestamp
|
||||
if (deltaTimestamp > 0) {
|
||||
retValue = 1
|
||||
} else if (deltaTimestamp < 0) {
|
||||
retValue = -1
|
||||
} else {
|
||||
retValue = 0
|
||||
}
|
||||
}
|
||||
return retValue
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -18,56 +18,31 @@ package im.vector.riotredesign.features.home.room.list.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.transition.ChangeTransform
|
||||
import androidx.transition.Transition
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.animations.ANIMATION_DURATION_SHORT
|
||||
import im.vector.riotredesign.core.animations.SimpleTransitionListener
|
||||
import im.vector.riotredesign.core.animations.VectorFullTransitionSet
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import kotlinx.android.synthetic.main.merge_fab_menu_view.view.*
|
||||
import kotlinx.android.synthetic.main.motion_fab_menu_merge.view.*
|
||||
|
||||
class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
private var isFabMenuOpened = false
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.merge_fab_menu_view, this)
|
||||
inflate(context, R.layout.motion_fab_menu_merge, this)
|
||||
}
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
|
||||
// Collapse
|
||||
ConstraintSet().also {
|
||||
it.clone(context, R.layout.constraint_set_fab_menu_close)
|
||||
it.applyTo(this)
|
||||
}
|
||||
|
||||
createRoomItemChat.isVisible = false
|
||||
createRoomItemChatLabel.isVisible = false
|
||||
createRoomItemGroup.isVisible = false
|
||||
createRoomItemGroupLabel.isVisible = false
|
||||
// Collapse end
|
||||
|
||||
// Tint label background
|
||||
listOf(createRoomItemChatLabel, createRoomItemGroupLabel)
|
||||
.forEach {
|
||||
it.setBackgroundResource(ThemeUtils.getResourceId(context, R.drawable.vector_label_background_light))
|
||||
}
|
||||
|
||||
createRoomButton.setOnClickListener {
|
||||
toggleFabMenu()
|
||||
}
|
||||
|
||||
listOf(createRoomItemChat, createRoomItemChatLabel)
|
||||
.forEach {
|
||||
it.setOnClickListener {
|
||||
@ -89,68 +64,25 @@ class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSe
|
||||
}
|
||||
|
||||
fun show() {
|
||||
isVisible = true
|
||||
createRoomButton.show()
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
createRoomButton.hide()
|
||||
}
|
||||
|
||||
private fun openFabMenu() {
|
||||
if (isFabMenuOpened) {
|
||||
return
|
||||
}
|
||||
|
||||
toggleFabMenu()
|
||||
createRoomButton.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton?) {
|
||||
super.onHidden(fab)
|
||||
isVisible = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun closeFabMenu() {
|
||||
if (!isFabMenuOpened) {
|
||||
return
|
||||
}
|
||||
|
||||
toggleFabMenu()
|
||||
}
|
||||
|
||||
private fun toggleFabMenu() {
|
||||
isFabMenuOpened = !isFabMenuOpened
|
||||
|
||||
TransitionManager.beginDelayedTransition(parent as? ViewGroup ?: this,
|
||||
VectorFullTransitionSet().apply {
|
||||
duration = ANIMATION_DURATION_SHORT
|
||||
ChangeTransform()
|
||||
addListener(object : SimpleTransitionListener() {
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
// Hide the view after the transition for a better visual effect
|
||||
createRoomItemChat.isVisible = isFabMenuOpened
|
||||
createRoomItemChatLabel.isVisible = isFabMenuOpened
|
||||
createRoomItemGroup.isVisible = isFabMenuOpened
|
||||
createRoomItemGroupLabel.isVisible = isFabMenuOpened
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (isFabMenuOpened) {
|
||||
// Animate manually the rotation for a better effect
|
||||
createRoomButton.animate().setDuration(ANIMATION_DURATION_SHORT).rotation(135f)
|
||||
|
||||
|
||||
ConstraintSet().also {
|
||||
it.clone(context, R.layout.constraint_set_fab_menu_open)
|
||||
it.applyTo(this)
|
||||
}
|
||||
} else {
|
||||
createRoomButton.animate().setDuration(ANIMATION_DURATION_SHORT).rotation(0f)
|
||||
|
||||
ConstraintSet().also {
|
||||
it.clone(context, R.layout.constraint_set_fab_menu_close)
|
||||
it.applyTo(this)
|
||||
}
|
||||
}
|
||||
transitionToStart()
|
||||
}
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
if (isFabMenuOpened) {
|
||||
if (currentState == R.id.constraint_set_fab_menu_open) {
|
||||
closeFabMenu()
|
||||
return true
|
||||
}
|
||||
|
@ -23,19 +23,18 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import arrow.core.Try
|
||||
import com.jakewharton.rxbinding2.widget.RxTextView
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.Authenticator
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.sync.FilterService
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
import im.vector.riotredesign.core.di.ScreenComponent
|
||||
import im.vector.riotredesign.core.extensions.openAndStartSync
|
||||
import im.vector.riotredesign.core.extensions.configureAndStart
|
||||
import im.vector.riotredesign.core.extensions.showPassword
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import im.vector.riotredesign.features.home.HomeActivity
|
||||
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.Function3
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
@ -50,6 +49,7 @@ class LoginActivity : VectorBaseActivity() {
|
||||
|
||||
@Inject lateinit var authenticator: Authenticator
|
||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
|
||||
|
||||
private var passwordShown = false
|
||||
|
||||
@ -82,7 +82,7 @@ class LoginActivity : VectorBaseActivity() {
|
||||
authenticator.authenticate(homeServerConnectionConfig, login, password, object : MatrixCallback<Session> {
|
||||
override fun onSuccess(data: Session) {
|
||||
activeSessionHolder.setActiveSession(data)
|
||||
data.openAndStartSync()
|
||||
data.configureAndStart(pushRuleTriggerListener)
|
||||
goToHome()
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,9 @@ package im.vector.riotredesign.features.navigation
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import im.vector.riotredesign.core.utils.toast
|
||||
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||
import im.vector.riotredesign.features.debug.DebugMenuActivity
|
||||
@ -27,18 +30,27 @@ import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
|
||||
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity
|
||||
import im.vector.riotredesign.features.roomdirectory.roompreview.RoomPreviewActivity
|
||||
import im.vector.riotredesign.features.settings.VectorSettingsActivity
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DefaultNavigator @Inject constructor() : Navigator {
|
||||
|
||||
override fun openRoom(roomId: String, context: Context) {
|
||||
val args = RoomDetailArgs(roomId)
|
||||
override fun openRoom(context: Context, roomId: String, eventId: String?) {
|
||||
val args = RoomDetailArgs(roomId, eventId)
|
||||
val intent = RoomDetailActivity.newIntent(context, args)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String?) {
|
||||
if (context is VectorBaseActivity) {
|
||||
context.notImplemented("Open not joined room")
|
||||
} else {
|
||||
context.toast(R.string.not_implemented)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openRoomPreview(publicRoom: PublicRoom, context: Context) {
|
||||
val intent = RoomPreviewActivity.getIntent(context, publicRoom)
|
||||
context.startActivity(intent)
|
||||
@ -65,4 +77,12 @@ class DefaultNavigator @Inject constructor() : Navigator {
|
||||
override fun openKeysBackupManager(context: Context) {
|
||||
context.startActivity(KeysBackupManageActivity.intent(context))
|
||||
}
|
||||
|
||||
override fun openGroupDetail(groupId: String, context: Context) {
|
||||
Timber.v("Open group detail $groupId")
|
||||
}
|
||||
|
||||
override fun openUserDetail(userId: String, context: Context) {
|
||||
Timber.v("Open user detail $userId")
|
||||
}
|
||||
}
|
@ -21,7 +21,9 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
||||
|
||||
interface Navigator {
|
||||
|
||||
fun openRoom(roomId: String, context: Context)
|
||||
fun openRoom(context: Context, roomId: String, eventId: String? = null)
|
||||
|
||||
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String? = null)
|
||||
|
||||
fun openRoomPreview(publicRoom: PublicRoom, context: Context)
|
||||
|
||||
@ -34,4 +36,8 @@ interface Navigator {
|
||||
fun openKeysBackupSetup(context: Context, showManualExport: Boolean)
|
||||
|
||||
fun openKeysBackupManager(context: Context)
|
||||
|
||||
fun openGroupDetail(groupId: String, context: Context)
|
||||
|
||||
fun openUserDetail(userId: String, context: Context)
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* FIXME It works, but it does not refresh the notification, when it's already displayed
|
||||
*/
|
||||
class BitmapLoader(val context: Context,
|
||||
val listener: BitmapLoaderListener) {
|
||||
|
||||
/**
|
||||
* Avatar Url -> Icon
|
||||
*/
|
||||
private val cache = HashMap<String, Bitmap>()
|
||||
|
||||
// URLs to load
|
||||
private val toLoad = HashSet<String>()
|
||||
|
||||
// Black list of URLs (broken URL, etc.)
|
||||
private val blacklist = HashSet<String>()
|
||||
|
||||
private var uiHandler = Handler()
|
||||
|
||||
private val handlerThread: HandlerThread = HandlerThread("BitmapLoader", Thread.MIN_PRIORITY)
|
||||
private var backgroundHandler: Handler
|
||||
|
||||
init {
|
||||
handlerThread.start()
|
||||
backgroundHandler = Handler(handlerThread.looper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon of a room.
|
||||
* If already in cache, use it, else load it and call BitmapLoaderListener.onBitmapsLoaded() when ready
|
||||
*/
|
||||
fun getRoomBitmap(path: String?): Bitmap? {
|
||||
if (path == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
synchronized(cache) {
|
||||
if (cache[path] != null) {
|
||||
return cache[path]
|
||||
}
|
||||
|
||||
// Add to the queue, if not blacklisted
|
||||
if (!blacklist.contains(path)) {
|
||||
if (toLoad.contains(path)) {
|
||||
// Wait
|
||||
} else {
|
||||
toLoad.add(path)
|
||||
|
||||
backgroundHandler.post {
|
||||
loadRoomBitmap(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun loadRoomBitmap(path: String) {
|
||||
val bitmap = path.let {
|
||||
try {
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(path)
|
||||
.format(DecodeFormat.PREFER_ARGB_8888)
|
||||
.submit()
|
||||
.get()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "decodeFile failed")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
synchronized(cache) {
|
||||
if (bitmap == null) {
|
||||
// Add to the blacklist
|
||||
blacklist.add(path)
|
||||
} else {
|
||||
cache[path] = bitmap
|
||||
}
|
||||
|
||||
toLoad.remove(path)
|
||||
|
||||
if (toLoad.isEmpty()) {
|
||||
uiHandler.post {
|
||||
listener.onBitmapsLoaded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface BitmapLoaderListener {
|
||||
fun onBitmapsLoaded()
|
||||
}
|
||||
}
|
@ -15,16 +15,21 @@
|
||||
*/
|
||||
package im.vector.riotredesign.features.notifications
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.riotredesign.core.preference.BingRule
|
||||
|
||||
// TODO Remove
|
||||
class RoomState {
|
||||
|
||||
}
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event.
|
||||
@ -32,146 +37,137 @@ class RoomState {
|
||||
* The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that,
|
||||
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
|
||||
*/
|
||||
class NotifiableEventResolver(val context: Context) {
|
||||
class NotifiableEventResolver(private val stringProvider: StringProvider,
|
||||
private val noticeEventFormatter: NoticeEventFormatter) {
|
||||
|
||||
//private val eventDisplay = RiotEventDisplay(context)
|
||||
|
||||
fun resolveEvent(event: Event, roomState: RoomState?, bingRule: BingRule?, session: Session): NotifiableEvent? {
|
||||
// TODO
|
||||
return null
|
||||
/*
|
||||
val store = session.dataHandler.store
|
||||
if (store == null) {
|
||||
Log.e("## NotifiableEventResolver, unable to get store")
|
||||
//TODO notify somehow that something did fail?
|
||||
return null
|
||||
}
|
||||
fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session): NotifiableEvent? {
|
||||
val roomID = event.roomId ?: return null
|
||||
val eventId = event.eventId ?: return null
|
||||
val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
|
||||
|
||||
when (event.getClearType()) {
|
||||
EventType.MESSAGE -> {
|
||||
return resolveMessageEvent(event, bingRule, session, store)
|
||||
EventType.MESSAGE -> {
|
||||
return resolveMessageEvent(timelineEvent, session)
|
||||
}
|
||||
EventType.ENCRYPTED -> {
|
||||
val messageEvent = resolveMessageEvent(event, bingRule, session, store)
|
||||
EventType.ENCRYPTED -> {
|
||||
val messageEvent = resolveMessageEvent(timelineEvent, session)
|
||||
messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE
|
||||
return messageEvent
|
||||
}
|
||||
EventType.STATE_ROOM_MEMBER -> {
|
||||
return resolveStateRoomEvent(event, bingRule, session, store)
|
||||
return resolveStateRoomEvent(event, session)
|
||||
}
|
||||
else -> {
|
||||
else -> {
|
||||
|
||||
//If the event can be displayed, display it as is
|
||||
eventDisplay.getTextualDisplay(event, roomState)?.toString()?.let { body ->
|
||||
return SimpleNotifiableEvent(
|
||||
session.myUserId,
|
||||
eventId = event.eventId,
|
||||
noisy = bingRule?.notificationSound != null,
|
||||
timestamp = event.originServerTs,
|
||||
description = body,
|
||||
soundName = bingRule?.notificationSound,
|
||||
title = context.getString(R.string.notification_unknown_new_event),
|
||||
type = event.getClearType())
|
||||
}
|
||||
//TODO Better event text display
|
||||
val bodyPreview = event.type
|
||||
|
||||
return SimpleNotifiableEvent(
|
||||
session.sessionParams.credentials.userId,
|
||||
eventId = event.eventId!!,
|
||||
noisy = false,//will be updated
|
||||
timestamp = event.originServerTs ?: System.currentTimeMillis(),
|
||||
description = bodyPreview,
|
||||
title = stringProvider.getString(R.string.notification_unknown_new_event),
|
||||
soundName = null,
|
||||
type = event.type)
|
||||
|
||||
|
||||
//Unsupported event
|
||||
Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule")
|
||||
return null
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
private fun resolveMessageEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? {
|
||||
//If we are here, that means that the event should be notified to the user, we check now how it should be presented (sound)
|
||||
val soundName = bingRule?.notificationSound
|
||||
val noisy = bingRule?.notificationSound != null
|
||||
|
||||
private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? {
|
||||
|
||||
//The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
|
||||
val room = store.getRoom(event.roomId /*roomID cannot be null (see Matrix SDK code)*/)
|
||||
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
|
||||
|
||||
|
||||
if (room == null) {
|
||||
Timber.e("## Unable to resolve room for eventId [${event.eventId}] and roomID [${event.roomId}]")
|
||||
Timber.e("## Unable to resolve room for eventId [${event}]")
|
||||
// Ok room is not known in store, but we can still display something
|
||||
val body = eventDisplay.getTextualDisplay(event, null)?.toString()
|
||||
?: context.getString(R.string.notification_unknown_new_event)
|
||||
val roomName = context.getString(R.string.notification_unknown_room_name)
|
||||
val senderDisplayName = event.sender ?: ""
|
||||
val body =
|
||||
event.annotations?.editSummary?.aggregatedContent?.toModel<MessageContent>()?.body
|
||||
?: event.root.getClearContent().toModel<MessageContent>()?.body
|
||||
?: stringProvider.getString(R.string.notification_unknown_new_event)
|
||||
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
|
||||
val senderDisplayName = event.senderName
|
||||
|
||||
val notifiableEvent = NotifiableMessageEvent(
|
||||
eventId = event.eventId,
|
||||
timestamp = event.originServerTs,
|
||||
noisy = noisy,
|
||||
eventId = event.root.eventId!!,
|
||||
timestamp = event.root.originServerTs ?: 0,
|
||||
noisy = false,//will be updated
|
||||
senderName = senderDisplayName,
|
||||
senderId = event.sender,
|
||||
senderId = event.root.senderId,
|
||||
body = body,
|
||||
roomId = event.roomId,
|
||||
roomId = event.root.roomId!!,
|
||||
roomName = roomName)
|
||||
|
||||
notifiableEvent.matrixID = session.myUserId
|
||||
notifiableEvent.soundName = soundName
|
||||
|
||||
notifiableEvent.matrixID = session.sessionParams.credentials.userId
|
||||
return notifiableEvent
|
||||
} else {
|
||||
|
||||
val body = eventDisplay.getTextualDisplay(event, room.state)?.toString()
|
||||
?: context.getString(R.string.notification_unknown_new_event)
|
||||
val roomName = room.getRoomDisplayName(context)
|
||||
val senderDisplayName = room.state.getMemberName(event.sender) ?: event.sender ?: ""
|
||||
val body = event.annotations?.editSummary?.aggregatedContent?.toModel<MessageContent>()?.body
|
||||
?: event.root.getClearContent().toModel<MessageContent>()?.body
|
||||
?: stringProvider.getString(R.string.notification_unknown_new_event)
|
||||
val roomName = room.roomSummary?.displayName ?: ""
|
||||
val senderDisplayName = event.senderName ?: ""
|
||||
|
||||
val notifiableEvent = NotifiableMessageEvent(
|
||||
eventId = event.eventId,
|
||||
timestamp = event.originServerTs,
|
||||
noisy = noisy,
|
||||
eventId = event.root.eventId!!,
|
||||
timestamp = event.root.originServerTs ?: 0,
|
||||
noisy = false,//will be updated
|
||||
senderName = senderDisplayName,
|
||||
senderId = event.sender,
|
||||
senderId = event.root.senderId,
|
||||
body = body,
|
||||
roomId = event.roomId,
|
||||
roomId = event.root.roomId!!,
|
||||
roomName = roomName,
|
||||
roomIsDirect = room.isDirect)
|
||||
roomIsDirect = room.roomSummary?.isDirect ?: false)
|
||||
|
||||
notifiableEvent.matrixID = session.myUserId
|
||||
notifiableEvent.soundName = soundName
|
||||
notifiableEvent.matrixID = session.sessionParams.credentials.userId
|
||||
notifiableEvent.soundName = null
|
||||
|
||||
// Get the avatars URL
|
||||
// TODO They will be not displayed the first time (known limitation)
|
||||
notifiableEvent.roomAvatarPath = session.contentUrlResolver()
|
||||
.resolveThumbnail(room.roomSummary?.avatarUrl,
|
||||
250,
|
||||
250,
|
||||
ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
|
||||
val roomAvatarPath = session.mediaCache?.thumbnailCacheFile(room.avatarUrl, 50)
|
||||
if (roomAvatarPath != null) {
|
||||
notifiableEvent.roomAvatarPath = roomAvatarPath.path
|
||||
} else {
|
||||
// prepare for the next time
|
||||
session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), room.avatarUrl, 50)
|
||||
}
|
||||
|
||||
room.state.getMember(event.sender)?.avatarUrl?.let {
|
||||
val size = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
|
||||
val userAvatarUrlPath = session.mediaCache?.thumbnailCacheFile(it, size)
|
||||
if (userAvatarUrlPath != null) {
|
||||
notifiableEvent.senderAvatarPath = userAvatarUrlPath.path
|
||||
} else {
|
||||
// prepare for the next time
|
||||
session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), it, size)
|
||||
}
|
||||
}
|
||||
notifiableEvent.senderAvatarPath = session.contentUrlResolver()
|
||||
.resolveThumbnail(event.senderAvatar,
|
||||
250,
|
||||
250,
|
||||
ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
|
||||
return notifiableEvent
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveStateRoomEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? {
|
||||
if (RoomMember.MEMBERSHIP_INVITE == event.contentAsJsonObject?.getAsJsonPrimitive("membership")?.asString) {
|
||||
val room = store.getRoom(event.roomId /*roomID cannot be null (see Matrix SDK code)*/)
|
||||
val body = eventDisplay.getTextualDisplay(event, room.state)?.toString()
|
||||
?: context.getString(R.string.notification_new_invitation)
|
||||
|
||||
private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
|
||||
val content = event.content?.toModel<RoomMember>() ?: return null
|
||||
val roomId = event.roomId ?: return null
|
||||
val dName = event.senderId?.let { session.getUser(it)?.displayName }
|
||||
if (Membership.INVITE == content.membership) {
|
||||
val body = noticeEventFormatter.format(event, dName)
|
||||
?: stringProvider.getString(R.string.notification_new_invitation)
|
||||
return InviteNotifiableEvent(
|
||||
session.myUserId,
|
||||
eventId = event.eventId,
|
||||
roomId = event.roomId,
|
||||
timestamp = event.originServerTs,
|
||||
noisy = bingRule?.notificationSound != null,
|
||||
title = context.getString(R.string.notification_new_invitation),
|
||||
description = body,
|
||||
soundName = bingRule?.notificationSound,
|
||||
session.sessionParams.credentials.userId,
|
||||
eventId = event.eventId!!,
|
||||
roomId = roomId,
|
||||
timestamp = event.originServerTs ?: 0,
|
||||
noisy = false,//will be set later
|
||||
title = stringProvider.getString(R.string.notification_new_invitation),
|
||||
description = body.toString(),
|
||||
soundName = null, //will be set later
|
||||
type = event.getClearType(),
|
||||
isPushGatewayEvent = false)
|
||||
} else {
|
||||
@ -182,6 +178,6 @@ class NotifiableEventResolver(val context: Context) {
|
||||
//TODO generic handling?
|
||||
}
|
||||
return null
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.features.notifications
|
||||
|
||||
import im.vector.matrix.android.api.pushrules.Action
|
||||
|
||||
data class NotificationAction(
|
||||
val shouldNotify: Boolean,
|
||||
val highlight: Boolean = false,
|
||||
val soundName: String? = null
|
||||
) {
|
||||
companion object {
|
||||
fun extractFrom(ruleActions: List<Action>): NotificationAction {
|
||||
var shouldNotify = false
|
||||
var highlight = false
|
||||
var sound: String? = null
|
||||
ruleActions.forEach {
|
||||
if (it.type == Action.Type.NOTIFY) shouldNotify = true
|
||||
if (it.type == Action.Type.DONT_NOTIFY) shouldNotify = false
|
||||
if (it.type == Action.Type.SET_TWEAK) {
|
||||
if (it.tweak_action == "highlight") highlight = it.boolValue ?: false
|
||||
if (it.tweak_action == "sound") sound = it.stringValue
|
||||
}
|
||||
}
|
||||
return NotificationAction(shouldNotify, highlight, sound)
|
||||
}
|
||||
}
|
||||
}
|
@ -20,9 +20,14 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.RemoteInput
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@ -31,22 +36,22 @@ import javax.inject.Inject
|
||||
class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent == null || context == null) return
|
||||
|
||||
Timber.v("ReplyNotificationBroadcastReceiver received : $intent")
|
||||
Timber.v("NotificationBroadcastReceiver received : $intent")
|
||||
|
||||
when (intent.action) {
|
||||
NotificationUtils.SMART_REPLY_ACTION ->
|
||||
NotificationUtils.SMART_REPLY_ACTION ->
|
||||
handleSmartReply(intent, context)
|
||||
NotificationUtils.DISMISS_ROOM_NOTIF_ACTION ->
|
||||
intent.getStringExtra(KEY_ROOM_ID)?.let {
|
||||
notificationDrawerManager.clearMessageEventOfRoom(it)
|
||||
}
|
||||
NotificationUtils.DISMISS_SUMMARY_ACTION ->
|
||||
NotificationUtils.DISMISS_SUMMARY_ACTION ->
|
||||
notificationDrawerManager.clearAllEvents()
|
||||
NotificationUtils.MARK_ROOM_READ_ACTION ->
|
||||
NotificationUtils.MARK_ROOM_READ_ACTION ->
|
||||
intent.getStringExtra(KEY_ROOM_ID)?.let {
|
||||
notificationDrawerManager.clearMessageEventOfRoom(it)
|
||||
handleMarkAsRead(context, it)
|
||||
@ -55,67 +60,61 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
private fun handleMarkAsRead(context: Context, roomId: String) {
|
||||
/*
|
||||
TODO
|
||||
Matrix.getInstance(context)?.defaultSession?.let { session ->
|
||||
session.dataHandler
|
||||
?.getRoom(roomId)
|
||||
?.markAllAsRead(object : SimpleApiCallback<Unit>() {
|
||||
override fun onSuccess(void: Void?) {
|
||||
// Ignore
|
||||
}
|
||||
})
|
||||
activeSessionHolder.getActiveSession().let { session ->
|
||||
session.getRoom(roomId)
|
||||
?.markAllAsRead(object : MatrixCallback<Unit> {})
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private fun handleSmartReply(intent: Intent, context: Context) {
|
||||
/*
|
||||
TODO
|
||||
val message = getReplyMessage(intent)
|
||||
val roomId = intent.getStringExtra(KEY_ROOM_ID)
|
||||
|
||||
if (TextUtils.isEmpty(message) || TextUtils.isEmpty(roomId)) {
|
||||
if (message.isNullOrBlank() || roomId.isBlank()) {
|
||||
//ignore this event
|
||||
//Can this happen? should we update notification?
|
||||
return
|
||||
}
|
||||
val matrixId = intent.getStringExtra(EXTRA_MATRIX_ID)
|
||||
Matrix.getInstance(context)?.getSession(matrixId)?.let { session ->
|
||||
session.dataHandler?.getRoom(roomId)?.let { room ->
|
||||
sendMatrixEvent(message!!, session, roomId!!, room, context)
|
||||
activeSessionHolder.getActiveSession().let { session ->
|
||||
session.getRoom(roomId)?.let { room ->
|
||||
sendMatrixEvent(message, session, room, context)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private fun sendMatrixEvent(message: String, session: Session, roomId: String, room: Room, context: Context?) {
|
||||
/*
|
||||
TODO
|
||||
private fun sendMatrixEvent(message: String, session: Session, room: Room, context: Context?) {
|
||||
|
||||
val mxMessage = Message()
|
||||
mxMessage.msgtype = Message.MSGTYPE_TEXT
|
||||
mxMessage.body = message
|
||||
room.sendTextMessage(message)
|
||||
|
||||
// Create a new event to be displayed in the notification drawer, right now
|
||||
|
||||
val notifiableMessageEvent = NotifiableMessageEvent(
|
||||
// Generate a Fake event id
|
||||
UUID.randomUUID().toString(),
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
session.getUser(session.sessionParams.credentials.userId)?.displayName
|
||||
?: context?.getString(R.string.notification_sender_me),
|
||||
session.sessionParams.credentials.userId,
|
||||
message,
|
||||
room.roomId,
|
||||
room.roomSummary?.displayName ?: room.roomId,
|
||||
room.roomSummary?.isDirect == true
|
||||
)
|
||||
notifiableMessageEvent.outGoingMessage = true
|
||||
|
||||
notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
|
||||
notificationDrawerManager.refreshNotificationDrawer()
|
||||
|
||||
/*
|
||||
// TODO Error cannot be managed the same way than in Riot
|
||||
|
||||
val event = Event(mxMessage, session.credentials.userId, roomId)
|
||||
room.storeOutgoingEvent(event)
|
||||
room.sendEvent(event, object : MatrixCallback<Void?> {
|
||||
override fun onSuccess(info: Void?) {
|
||||
Timber.v("Send message : onSuccess ")
|
||||
val notifiableMessageEvent = NotifiableMessageEvent(
|
||||
event.eventId,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
session.myUser?.displayname
|
||||
?: context?.getString(R.string.notification_sender_me),
|
||||
session.myUserId,
|
||||
message,
|
||||
roomId,
|
||||
room.getRoomDisplayName(context),
|
||||
room.isDirect)
|
||||
notifiableMessageEvent.outGoingMessage = true
|
||||
VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
|
||||
VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null)
|
||||
}
|
||||
|
||||
override fun onNetworkError(e: Exception) {
|
||||
|
@ -18,14 +18,15 @@ package im.vector.riotredesign.features.notifications
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.text.TextUtils
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
import im.vector.riotredesign.core.utils.SecretStoringUtils
|
||||
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
@ -38,16 +39,15 @@ import javax.inject.Singleton
|
||||
* organise them in order to display them in the notification drawer.
|
||||
* Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning.
|
||||
*/
|
||||
|
||||
@Singleton
|
||||
class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
class NotificationDrawerManager @Inject constructor(private val context: Context,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val outdatedDetector: OutdatedEventDetector?) {
|
||||
|
||||
//The first time the notification drawer is refreshed, we force re-render of all notifications
|
||||
private var firstTime = true
|
||||
|
||||
private var eventList = loadEventInfo()
|
||||
private var myUserDisplayName: String = ""
|
||||
private var myUserAvatarUrl: String = ""
|
||||
|
||||
private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
|
||||
|
||||
@ -57,31 +57,17 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
object : IconLoader.IconLoaderListener {
|
||||
override fun onIconsLoaded() {
|
||||
// Force refresh
|
||||
refreshNotificationDrawer(null)
|
||||
refreshNotificationDrawer()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* No multi session support for now
|
||||
*/
|
||||
private fun initWithSession(session: Session?) {
|
||||
session?.let {
|
||||
/*
|
||||
myUserDisplayName = it.myUser?.displayname ?: it.myUserId
|
||||
|
||||
// User Avatar
|
||||
it.myUser?.avatarUrl?.let { avatarUrl ->
|
||||
val userAvatarUrlPath = it.mediaCache?.thumbnailCacheFile(avatarUrl, avatarSize)
|
||||
if (userAvatarUrlPath != null) {
|
||||
myUserAvatarUrl = userAvatarUrlPath.path
|
||||
} else {
|
||||
// prepare for the next time
|
||||
session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), avatarUrl, avatarSize)
|
||||
private var bitmapLoader = BitmapLoader(context,
|
||||
object : BitmapLoader.BitmapLoaderListener {
|
||||
override fun onBitmapsLoaded() {
|
||||
// Force refresh
|
||||
refreshNotificationDrawer()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
Should be called as soon as a new event is ready to be displayed.
|
||||
@ -90,6 +76,10 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
Events might be grouped and there might not be one notification per event!
|
||||
*/
|
||||
fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||
if (!PreferencesManager.areNotificationEnabledForDevice(context)) {
|
||||
Timber.i("Notification are disabled for this device")
|
||||
return
|
||||
}
|
||||
//If we support multi session, event list should be per userId
|
||||
//Currently only manage single session
|
||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||
@ -116,7 +106,6 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
} else {
|
||||
eventList.add(notifiableEvent)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,7 +116,7 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
synchronized(eventList) {
|
||||
eventList.clear()
|
||||
}
|
||||
refreshNotificationDrawer(null)
|
||||
refreshNotificationDrawer()
|
||||
}
|
||||
|
||||
/** Clear all known message events for this room and refresh the notification drawer */
|
||||
@ -143,12 +132,12 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
}
|
||||
NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID)
|
||||
}
|
||||
refreshNotificationDrawer(null)
|
||||
refreshNotificationDrawer()
|
||||
}
|
||||
|
||||
/**
|
||||
Should be called when the application is currently opened and showing timeline for the given roomId.
|
||||
Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
||||
* Should be called when the application is currently opened and showing timeline for the given roomId.
|
||||
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
||||
*/
|
||||
fun setCurrentRoom(roomId: String?) {
|
||||
var hasChanged: Boolean
|
||||
@ -181,17 +170,11 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
}
|
||||
|
||||
|
||||
fun refreshNotificationDrawer(outdatedDetector: OutdatedEventDetector?) {
|
||||
if (myUserDisplayName.isBlank()) {
|
||||
// TODO
|
||||
// initWithSession(Matrix.getInstance(context).defaultSession)
|
||||
}
|
||||
|
||||
if (myUserDisplayName.isBlank()) {
|
||||
// Should not happen, but myUserDisplayName cannot be blank if used to create a Person
|
||||
return
|
||||
}
|
||||
|
||||
fun refreshNotificationDrawer() {
|
||||
val session = activeSessionHolder.getActiveSession()
|
||||
val user = session.getUser(session.sessionParams.credentials.userId)
|
||||
val myUserDisplayName = user?.displayName ?: ""
|
||||
val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
synchronized(eventList) {
|
||||
|
||||
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ")
|
||||
@ -242,22 +225,23 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
val roomGroup = RoomEventGroupInfo(roomId)
|
||||
roomGroup.hasNewEvent = false
|
||||
roomGroup.shouldBing = false
|
||||
roomGroup.isDirect = events[0].roomIsDirect
|
||||
val roomName = events[0].roomName ?: events[0].senderName ?: ""
|
||||
|
||||
val roomEventGroupInfo = RoomEventGroupInfo(
|
||||
roomId = roomId,
|
||||
isDirect = events[0].roomIsDirect,
|
||||
roomDisplayName = roomName)
|
||||
|
||||
val style = NotificationCompat.MessagingStyle(Person.Builder()
|
||||
.setName(myUserDisplayName)
|
||||
.setIcon(iconLoader.getUserIcon(myUserAvatarUrl))
|
||||
.setKey(events[0].matrixID)
|
||||
.build())
|
||||
roomGroup.roomDisplayName = roomName
|
||||
|
||||
style.isGroupConversation = !roomGroup.isDirect
|
||||
style.isGroupConversation = !roomEventGroupInfo.isDirect
|
||||
|
||||
if (!roomGroup.isDirect) {
|
||||
style.conversationTitle = roomName
|
||||
if (!roomEventGroupInfo.isDirect) {
|
||||
style.conversationTitle = roomEventGroupInfo.roomDisplayName
|
||||
}
|
||||
|
||||
val largeBitmap = getRoomBitmap(events)
|
||||
@ -266,10 +250,10 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
for (event in events) {
|
||||
//if all events in this room have already been displayed there is no need to update it
|
||||
if (!event.hasBeenDisplayed) {
|
||||
roomGroup.shouldBing = roomGroup.shouldBing || event.noisy
|
||||
roomGroup.customSound = event.soundName
|
||||
roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy
|
||||
roomEventGroupInfo.customSound = event.soundName
|
||||
}
|
||||
roomGroup.hasNewEvent = roomGroup.hasNewEvent || !event.hasBeenDisplayed
|
||||
roomEventGroupInfo.hasNewEvent = roomEventGroupInfo.hasNewEvent || !event.hasBeenDisplayed
|
||||
|
||||
val senderPerson = Person.Builder()
|
||||
.setName(event.senderName)
|
||||
@ -279,7 +263,7 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
|
||||
if (event.outGoingMessage && event.outGoingMessageFailed) {
|
||||
style.addMessage(context.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
|
||||
roomGroup.hasSmartReplyError = true
|
||||
roomEventGroupInfo.hasSmartReplyError = true
|
||||
} else {
|
||||
style.addMessage(event.body, event.timestamp, senderPerson)
|
||||
}
|
||||
@ -300,7 +284,7 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
summaryInboxStyle.addLine(roomName)
|
||||
}
|
||||
|
||||
if (firstTime || roomGroup.hasNewEvent) {
|
||||
if (firstTime || roomEventGroupInfo.hasNewEvent) {
|
||||
//Should update displayed notification
|
||||
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId need refresh")
|
||||
val lastMessageTimestamp = events.last().timestamp
|
||||
@ -309,14 +293,14 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
globalLastMessageTimestamp = lastMessageTimestamp
|
||||
}
|
||||
|
||||
NotificationUtils.buildMessagesListNotification(context, style, roomGroup, largeBitmap, lastMessageTimestamp, myUserDisplayName)
|
||||
NotificationUtils.buildMessagesListNotification(context, style, roomEventGroupInfo, largeBitmap, lastMessageTimestamp, myUserDisplayName)
|
||||
?.let {
|
||||
//is there an id for this room?
|
||||
notifications.add(it)
|
||||
NotificationUtils.showNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID, it)
|
||||
}
|
||||
hasNewEvent = true
|
||||
summaryIsNoisy = summaryIsNoisy || roomGroup.shouldBing
|
||||
summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing
|
||||
} else {
|
||||
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId is up to date")
|
||||
}
|
||||
@ -397,19 +381,10 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
|
||||
if (events.isEmpty()) return null
|
||||
|
||||
//Use the last event (most recent?)
|
||||
val roomAvatarPath = events[events.size - 1].roomAvatarPath
|
||||
?: events[events.size - 1].senderAvatarPath
|
||||
if (!TextUtils.isEmpty(roomAvatarPath)) {
|
||||
val options = BitmapFactory.Options()
|
||||
options.inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||
try {
|
||||
return BitmapFactory.decodeFile(roomAvatarPath, options)
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "decodeFile failed with an oom")
|
||||
}
|
||||
val roomAvatarPath = events.last().roomAvatarPath
|
||||
?: events.last().senderAvatarPath
|
||||
|
||||
}
|
||||
return null
|
||||
return bitmapLoader.getRoomBitmap(roomAvatarPath)
|
||||
}
|
||||
|
||||
private fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean {
|
||||
|
@ -39,6 +39,8 @@ import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.utils.startNotificationChannelSettingsIntent
|
||||
import im.vector.riotredesign.features.home.HomeActivity
|
||||
import im.vector.riotredesign.features.home.room.detail.RoomDetailActivity
|
||||
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
|
||||
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
@ -180,7 +182,7 @@ object NotificationUtils {
|
||||
* @return the polling thread listener notification
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int): Notification {
|
||||
fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int, withProgress: Boolean = true): Notification {
|
||||
// build the pending intent go to the home screen if this is clicked.
|
||||
val i = Intent(context, HomeActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
@ -190,16 +192,21 @@ object NotificationUtils {
|
||||
|
||||
val builder = NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(context.getString(subTitleResId))
|
||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||
.setSmallIcon(R.drawable.logo_transparent)
|
||||
.setProgress(0, 0, true)
|
||||
.setSmallIcon(R.drawable.sync)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setColor(accentColor)
|
||||
.setContentIntent(pi)
|
||||
.apply {
|
||||
if (withProgress) {
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
}
|
||||
|
||||
// hide the notification from the status bar
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
builder.priority = NotificationCompat.PRIORITY_MIN
|
||||
}
|
||||
// PRIORITY_MIN should not be used with Service#startForeground(int, Notification)
|
||||
builder.priority = NotificationCompat.PRIORITY_LOW
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
// builder.priority = NotificationCompat.PRIORITY_MIN
|
||||
// }
|
||||
|
||||
val notification = builder.build()
|
||||
|
||||
@ -220,7 +227,7 @@ object NotificationUtils {
|
||||
PendingIntent::class.java)
|
||||
deprecatedMethod.invoke(notification, context, context.getString(R.string.app_name), context.getString(subTitleResId), pi)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=" + ex.message)
|
||||
Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=")
|
||||
}
|
||||
|
||||
}
|
||||
@ -540,27 +547,21 @@ object NotificationUtils {
|
||||
}
|
||||
|
||||
private fun buildOpenRoomIntent(context: Context, roomId: String): PendingIntent? {
|
||||
// TODO
|
||||
return null
|
||||
/*
|
||||
val roomIntentTap = Intent(context, VectorRoomActivity::class.java)
|
||||
roomIntentTap.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId)
|
||||
val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId))
|
||||
roomIntentTap.action = TAP_TO_VIEW_ACTION
|
||||
//pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
|
||||
roomIntentTap.data = Uri.parse("foobar://openRoom?$roomId")
|
||||
|
||||
// Recreate the back stack
|
||||
return TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(Intent(context, VectorHomeActivity::class.java))
|
||||
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
|
||||
.addNextIntent(roomIntentTap)
|
||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
*/
|
||||
}
|
||||
|
||||
private fun buildOpenHomePendingIntentForSummary(context: Context): PendingIntent {
|
||||
val intent = Intent(context, HomeActivity::class.java)
|
||||
val intent = HomeActivity.newIntent(context, clearNotification = true)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
// TODO intent.putExtra(VectorHomeActivity.EXTRA_CLEAR_EXISTING_NOTIFICATION, true)
|
||||
intent.data = Uri.parse("foobar://tapSummary")
|
||||
return PendingIntent.getActivity(context, Random().nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
@ -15,9 +15,10 @@
|
||||
*/
|
||||
package im.vector.riotredesign.features.notifications
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import javax.inject.Inject
|
||||
|
||||
class OutdatedEventDetector(val context: Context) {
|
||||
class OutdatedEventDetector @Inject constructor(private val session: Session) {
|
||||
|
||||
/**
|
||||
* Returns true if the given event is outdated.
|
||||
@ -28,20 +29,8 @@ class OutdatedEventDetector(val context: Context) {
|
||||
if (notifiableEvent is NotifiableMessageEvent) {
|
||||
val eventID = notifiableEvent.eventId
|
||||
val roomID = notifiableEvent.roomId
|
||||
/*
|
||||
TODO
|
||||
Matrix.getMXSession(context.applicationContext, notifiableEvent.matrixID)?.let { session ->
|
||||
//find the room
|
||||
if (session.isAlive) {
|
||||
session.dataHandler.getRoom(roomID)?.let { room ->
|
||||
if (room.isEventRead(eventID)) {
|
||||
Timber.v("Notifiable Event $eventID is read, and should be removed")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
val room = session.getRoom(roomID) ?: return false
|
||||
return room.isEventRead(eventID)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.notifications
|
||||
|
||||
import im.vector.matrix.android.api.pushrules.Action
|
||||
import im.vector.matrix.android.api.pushrules.PushRuleService
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Singleton
|
||||
class PushRuleTriggerListener @Inject constructor(
|
||||
private val resolver: NotifiableEventResolver,
|
||||
private val drawerManager: NotificationDrawerManager
|
||||
) : PushRuleService.PushRuleListener {
|
||||
|
||||
|
||||
var session: Session? = null
|
||||
|
||||
override fun onMatchRule(event: Event, actions: List<Action>) {
|
||||
Timber.v("Push rule match for event ${event.eventId}")
|
||||
if (session == null) {
|
||||
Timber.e("Called without active session")
|
||||
return
|
||||
}
|
||||
val notificationAction = NotificationAction.extractFrom(actions)
|
||||
if (notificationAction.shouldNotify) {
|
||||
val resolveEvent = resolver.resolveEvent(event, session!!)
|
||||
if (resolveEvent == null) {
|
||||
Timber.v("## Failed to resolve event")
|
||||
//TODO
|
||||
} else {
|
||||
resolveEvent.noisy = !notificationAction.soundName.isNullOrBlank()
|
||||
Timber.v("New event to notify $resolveEvent tweaks:$notificationAction")
|
||||
drawerManager.onNotifiableEventReceived(resolveEvent)
|
||||
}
|
||||
} else {
|
||||
Timber.v("Matched push rule is set to not notify")
|
||||
}
|
||||
}
|
||||
|
||||
override fun batchFinish() {
|
||||
drawerManager.refreshNotificationDrawer()
|
||||
}
|
||||
|
||||
fun startWithSession(session: Session) {
|
||||
if (this.session != null) {
|
||||
stop()
|
||||
}
|
||||
this.session = session
|
||||
session.addPushRuleListener(this)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
session?.removePushRuleListener(this)
|
||||
session = null
|
||||
drawerManager.clearAllEvents()
|
||||
}
|
||||
|
||||
}
|
@ -20,15 +20,14 @@ package im.vector.riotredesign.features.notifications
|
||||
* Data class to hold information about a group of notifications for a room
|
||||
*/
|
||||
data class RoomEventGroupInfo(
|
||||
val roomId: String
|
||||
val roomId: String,
|
||||
val roomDisplayName: String = "",
|
||||
val isDirect: Boolean = false
|
||||
) {
|
||||
var roomDisplayName: String = ""
|
||||
var roomAvatarPath: String? = null
|
||||
//An event in the list has not yet been display
|
||||
// An event in the list has not yet been display
|
||||
var hasNewEvent: Boolean = false
|
||||
//true if at least one on the not yet displayed event is noisy
|
||||
// true if at least one on the not yet displayed event is noisy
|
||||
var shouldBing: Boolean = false
|
||||
var customSound: String? = null
|
||||
var hasSmartReplyError = false
|
||||
var isDirect = false
|
||||
}
|
||||
var hasSmartReplyError: Boolean = false
|
||||
}
|
||||
|
@ -19,10 +19,8 @@ package im.vector.riotredesign.features.rageshake
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.BindView
|
||||
import butterknife.OnCheckedChanged
|
||||
import butterknife.OnTextChanged
|
||||
import im.vector.riotredesign.R
|
||||
@ -37,36 +35,6 @@ import javax.inject.Inject
|
||||
*/
|
||||
class BugReportActivity : VectorBaseActivity() {
|
||||
|
||||
/* ==========================================================================================
|
||||
* UI
|
||||
* ========================================================================================== */
|
||||
|
||||
@BindView(R.id.bug_report_edit_text)
|
||||
lateinit var mBugReportText: EditText
|
||||
|
||||
@BindView(R.id.bug_report_button_include_logs)
|
||||
lateinit var mIncludeLogsButton: CheckBox
|
||||
|
||||
@BindView(R.id.bug_report_button_include_crash_logs)
|
||||
lateinit var mIncludeCrashLogsButton: CheckBox
|
||||
|
||||
@BindView(R.id.bug_report_button_include_screenshot)
|
||||
lateinit var mIncludeScreenShotButton: CheckBox
|
||||
|
||||
@BindView(R.id.bug_report_screenshot_preview)
|
||||
lateinit var mScreenShotPreview: ImageView
|
||||
|
||||
@BindView(R.id.bug_report_progress_view)
|
||||
lateinit var mProgressBar: ProgressBar
|
||||
|
||||
@BindView(R.id.bug_report_progress_text_view)
|
||||
lateinit var mProgressTextView: TextView
|
||||
|
||||
@BindView(R.id.bug_report_scrollview)
|
||||
lateinit var mScrollView: View
|
||||
|
||||
@BindView(R.id.bug_report_mask_view)
|
||||
lateinit var mMaskView: View
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
@ -78,11 +46,11 @@ class BugReportActivity : VectorBaseActivity() {
|
||||
configureToolbar(bugReportToolbar)
|
||||
|
||||
if (bugReporter.screenshot != null) {
|
||||
mScreenShotPreview.setImageBitmap(bugReporter.screenshot)
|
||||
bug_report_screenshot_preview.setImageBitmap(bugReporter.screenshot)
|
||||
} else {
|
||||
mScreenShotPreview.isVisible = false
|
||||
mIncludeScreenShotButton.isChecked = false
|
||||
mIncludeScreenShotButton.isEnabled = false
|
||||
bug_report_screenshot_preview.isVisible = false
|
||||
bug_report_button_include_screenshot.isChecked = false
|
||||
bug_report_button_include_screenshot.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,8 +58,8 @@ class BugReportActivity : VectorBaseActivity() {
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
menu.findItem(R.id.ic_action_send_bug_report)?.let {
|
||||
val isValid = mBugReportText.text.toString().trim().length > 10
|
||||
&& !mMaskView.isVisible
|
||||
val isValid = bug_report_edit_text.text.toString().trim().length > 10
|
||||
&& !bug_report_mask_view.isVisible
|
||||
|
||||
it.isEnabled = isValid
|
||||
it.icon.alpha = if (isValid) 255 else 100
|
||||
@ -115,22 +83,22 @@ class BugReportActivity : VectorBaseActivity() {
|
||||
* Send the bug report
|
||||
*/
|
||||
private fun sendBugReport() {
|
||||
mScrollView.alpha = 0.3f
|
||||
mMaskView.isVisible = true
|
||||
bug_report_scrollview.alpha = 0.3f
|
||||
bug_report_mask_view.isVisible = true
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
mProgressTextView.isVisible = true
|
||||
mProgressTextView.text = getString(R.string.send_bug_report_progress, 0.toString() + "")
|
||||
bug_report_progress_text_view.isVisible = true
|
||||
bug_report_progress_text_view.text = getString(R.string.send_bug_report_progress, "0")
|
||||
|
||||
mProgressBar.isVisible = true
|
||||
mProgressBar.progress = 0
|
||||
bug_report_progress_view.isVisible = true
|
||||
bug_report_progress_view.progress = 0
|
||||
|
||||
bugReporter.sendBugReport(this,
|
||||
mIncludeLogsButton.isChecked,
|
||||
mIncludeCrashLogsButton.isChecked,
|
||||
mIncludeScreenShotButton.isChecked,
|
||||
mBugReportText.text.toString(),
|
||||
bug_report_button_include_logs.isChecked,
|
||||
bug_report_button_include_crash_logs.isChecked,
|
||||
bug_report_button_include_screenshot.isChecked,
|
||||
bug_report_edit_text.text.toString(),
|
||||
object : BugReporter.IMXBugReportListener {
|
||||
override fun onUploadFailed(reason: String?) {
|
||||
try {
|
||||
@ -142,10 +110,10 @@ class BugReportActivity : VectorBaseActivity() {
|
||||
Timber.e(e, "## onUploadFailed() : failed to display the toast " + e.message)
|
||||
}
|
||||
|
||||
mMaskView.isVisible = false
|
||||
mProgressBar.isVisible = false
|
||||
mProgressTextView.isVisible = false
|
||||
mScrollView.alpha = 1.0f
|
||||
bug_report_mask_view.isVisible = false
|
||||
bug_report_progress_view.isVisible = false
|
||||
bug_report_progress_text_view.isVisible = false
|
||||
bug_report_scrollview.alpha = 1.0f
|
||||
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
@ -155,17 +123,10 @@ class BugReportActivity : VectorBaseActivity() {
|
||||
}
|
||||
|
||||
override fun onProgress(progress: Int) {
|
||||
var progress = progress
|
||||
if (progress > 100) {
|
||||
Timber.e("## onProgress() : progress > 100")
|
||||
progress = 100
|
||||
} else if (progress < 0) {
|
||||
Timber.e("## onProgress() : progress < 0")
|
||||
progress = 0
|
||||
}
|
||||
val myProgress = progress.coerceIn(0, 100)
|
||||
|
||||
mProgressBar.progress = progress
|
||||
mProgressTextView.text = getString(R.string.send_bug_report_progress, progress.toString() + "")
|
||||
bug_report_progress_view.progress = myProgress
|
||||
bug_report_progress_text_view.text = getString(R.string.send_bug_report_progress, "$myProgress")
|
||||
}
|
||||
|
||||
override fun onUploadSucceed() {
|
||||
@ -180,7 +141,6 @@ class BugReportActivity : VectorBaseActivity() {
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onUploadSucceed() : failed to dismiss the dialog " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -196,7 +156,7 @@ class BugReportActivity : VectorBaseActivity() {
|
||||
|
||||
@OnCheckedChanged(R.id.bug_report_button_include_screenshot)
|
||||
internal fun onSendScreenshotChanged() {
|
||||
mScreenShotPreview.isVisible = mIncludeScreenShotButton.isChecked && bugReporter.screenshot != null
|
||||
bug_report_screenshot_preview.isVisible = bug_report_button_include_screenshot.isChecked && bugReporter.screenshot != null
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
@ -205,12 +165,4 @@ class BugReportActivity : VectorBaseActivity() {
|
||||
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Companion
|
||||
* ========================================================================================== */
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = BugReportActivity::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import im.vector.riotredesign.core.extensions.toOnOff
|
||||
import im.vector.riotredesign.core.utils.getDeviceLocale
|
||||
import im.vector.riotredesign.features.settings.VectorLocale
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import im.vector.riotredesign.features.version.getVersion
|
||||
import okhttp3.*
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
@ -198,13 +199,11 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
|
||||
|
||||
var deviceId = "undefined"
|
||||
var userId = "undefined"
|
||||
var matrixSdkVersion = "undefined"
|
||||
var olmVersion = "undefined"
|
||||
|
||||
activeSessionHolder.getActiveSession().let { session ->
|
||||
userId = session.sessionParams.credentials.userId
|
||||
deviceId = session.sessionParams.credentials.deviceId ?: "undefined"
|
||||
// TODO matrixSdkVersion = session.getVersion(true);
|
||||
olmVersion = session.getCryptoVersion(context, true)
|
||||
}
|
||||
|
||||
@ -216,9 +215,9 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
|
||||
.addFormDataPart("user_agent", Matrix.getInstance(context).getUserAgent())
|
||||
.addFormDataPart("user_id", userId)
|
||||
.addFormDataPart("device_id", deviceId)
|
||||
// TODO .addFormDataPart("version", Matrix.getInstance(context).getVersion(true, false))
|
||||
.addFormDataPart("version", getVersion(longFormat = true, useBuildNumber = false))
|
||||
.addFormDataPart("branch_name", context.getString(R.string.git_branch_name))
|
||||
.addFormDataPart("matrix_sdk_version", matrixSdkVersion)
|
||||
.addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion())
|
||||
.addFormDataPart("olm_version", olmVersion)
|
||||
.addFormDataPart("device", Build.MODEL.trim())
|
||||
.addFormDataPart("lazy_loading", true.toOnOff())
|
||||
|
@ -20,7 +20,9 @@ import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.features.version.getVersion
|
||||
import timber.log.Timber
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
@ -35,8 +37,8 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter
|
||||
private const val PREFS_CRASH_KEY = "PREFS_CRASH_KEY"
|
||||
}
|
||||
|
||||
private var vectorVersion: String = ""
|
||||
private var matrixSdkVersion: String = ""
|
||||
private val vectorVersion = getVersion(longFormat = true, useBuildNumber = true)
|
||||
private val matrixSdkVersion = Matrix.getSdkVersion()
|
||||
|
||||
private var previousHandler: Thread.UncaughtExceptionHandler? = null
|
||||
|
||||
@ -116,12 +118,6 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter
|
||||
previousHandler?.uncaughtException(thread, throwable)
|
||||
}
|
||||
|
||||
// TODO Call me
|
||||
fun setVersions(vectorVersion: String, matrixSdkVersion: String) {
|
||||
this.vectorVersion = vectorVersion
|
||||
this.matrixSdkVersion = matrixSdkVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the application crashed
|
||||
*
|
||||
|
@ -123,7 +123,7 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
|
||||
|
||||
when (joinState) {
|
||||
JoinState.JOINED -> {
|
||||
navigator.openRoom(publicRoom.roomId, requireActivity())
|
||||
navigator.openRoom(requireActivity(), publicRoom.roomId)
|
||||
}
|
||||
JoinState.NOT_JOINED,
|
||||
JoinState.JOINING_ERROR -> {
|
||||
|
@ -98,7 +98,7 @@ class CreateRoomFragment : VectorBaseFragment(), CreateRoomController.Listener {
|
||||
val async = state.asyncCreateRoomRequest
|
||||
if (async is Success) {
|
||||
// Navigate to freshly created room
|
||||
navigator.openRoom(async(), requireActivity())
|
||||
navigator.openRoom(requireActivity(), async())
|
||||
|
||||
navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Close)
|
||||
} else {
|
||||
|
@ -110,7 +110,7 @@ class RoomPreviewNoPreviewFragment : VectorBaseFragment() {
|
||||
// Quit this screen
|
||||
requireActivity().finish()
|
||||
// Open room
|
||||
navigator.openRoom(roomPreviewData.roomId, requireActivity())
|
||||
navigator.openRoom(requireActivity(), roomPreviewData.roomId)
|
||||
}
|
||||
}
|
||||
}
|
@ -40,21 +40,23 @@ import timber.log.Timber;
|
||||
|
||||
public class PreferencesManager {
|
||||
|
||||
public static final String VERSION_BUILD = "VERSION_BUILD";
|
||||
|
||||
public static final String SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY = "SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY_2";
|
||||
public static final String SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY = "SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_VERSION_PREFERENCE_KEY = "SETTINGS_VERSION_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_SDK_VERSION_PREFERENCE_KEY = "SETTINGS_SDK_VERSION_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_OLM_VERSION_PREFERENCE_KEY = "SETTINGS_OLM_VERSION_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_LOGGED_IN_PREFERENCE_KEY = "SETTINGS_LOGGED_IN_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY = "SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY = "SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY";
|
||||
|
||||
//TODO delete
|
||||
public static final String SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_COPYRIGHT_PREFERENCE_KEY = "SETTINGS_COPYRIGHT_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY";
|
||||
public static final String SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY";
|
||||
@ -241,6 +243,10 @@ public class PreferencesManager {
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static boolean areNotificationEnabledForDevice(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if we have already asked the user to disable battery optimisations on android >= M devices.
|
||||
*
|
||||
|
@ -26,6 +26,7 @@ import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.di.ScreenComponent
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import kotlinx.android.synthetic.main.activity_vector_settings.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@ -36,7 +37,6 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||
FragmentManager.OnBackStackChangedListener,
|
||||
VectorSettingsFragmentInteractionListener {
|
||||
|
||||
private lateinit var vectorSettingsPreferencesFragment: VectorSettingsPreferencesFragment
|
||||
|
||||
override fun getLayoutRes() = R.layout.activity_vector_settings
|
||||
|
||||
@ -53,14 +53,15 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||
override fun initUiAndData() {
|
||||
configureToolbar(settingsToolbar)
|
||||
|
||||
var vectorSettingsPreferencesFragment: Fragment? = null
|
||||
if (isFirstCreation()) {
|
||||
vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId)
|
||||
vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragmentV2.newInstance()
|
||||
// display the fragment
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.vector_settings_page, vectorSettingsPreferencesFragment, FRAGMENT_TAG)
|
||||
.commit()
|
||||
} else {
|
||||
vectorSettingsPreferencesFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as VectorSettingsPreferencesFragment
|
||||
vectorSettingsPreferencesFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
|
||||
@ -82,19 +83,33 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat?, pref: Preference?): Boolean {
|
||||
var oFragment: Fragment? = null
|
||||
|
||||
if (PreferencesManager.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) {
|
||||
if ("Legacy" == pref?.title) {
|
||||
oFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId)
|
||||
} else if (PreferencesManager.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) {
|
||||
oFragment = VectorSettingsNotificationsTroubleshootFragment.newInstance(session.sessionParams.credentials.userId)
|
||||
} else if (PreferencesManager.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY == pref?.key) {
|
||||
oFragment = VectorSettingsAdvancedNotificationPreferenceFragment.newInstance(session.sessionParams.credentials.userId)
|
||||
} else {
|
||||
try {
|
||||
pref?.fragment?.let {
|
||||
oFragment = supportFragmentManager.fragmentFactory
|
||||
.instantiate(
|
||||
classLoader,
|
||||
it, pref.extras)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
showSnackbar(getString(R.string.not_implemented))
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (oFragment != null) {
|
||||
oFragment.setTargetFragment(caller, 0)
|
||||
oFragment!!.setTargetFragment(caller, 0)
|
||||
// Replace the existing Fragment with the new Fragment
|
||||
supportFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom,
|
||||
R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom)
|
||||
.replace(R.id.vector_settings_page, oFragment, pref?.title.toString())
|
||||
.setCustomAnimations(R.anim.right_in, R.anim.fade_out,
|
||||
R.anim.fade_in, R.anim.right_out)
|
||||
.replace(R.id.vector_settings_page, oFragment!!, pref?.title.toString())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
return true
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user