diff --git a/CHANGES.md b/CHANGES.md index 58daa4b8e4..6d2bb0152b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,7 @@ Bugfix: - Fix opening a permalink paginates all the history up to the last event (#282) - after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267) - Picture uploads are unreliable, pictures are shown in wrong aspect ratio on desktop client (#517) + - Invitation notifications are not dismissed automatically if room is joined from another client (#347) Translations: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt index c0204d181d..38153d7bc5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt @@ -42,6 +42,7 @@ interface PushRuleService { interface PushRuleListener { fun onMatchRule(event: Event, actions: List) + fun onRoomJoined(roomId: String) fun onRoomLeft(roomId: String) fun onEventRedacted(redactedEventId: String) fun batchFinish() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt index fb436c3b21..ddcd095e58 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt @@ -131,10 +131,20 @@ internal class DefaultPushRuleService @Inject constructor(private val getPushRul } } - fun dispatchRoomLeft(roomid: String) { + fun dispatchRoomJoined(roomId: String) { try { listeners.forEach { - it.onRoomLeft(roomid) + it.onRoomJoined(roomId) + } + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching room left") + } + } + + fun dispatchRoomLeft(roomId: String) { + try { + listeners.forEach { + it.onRoomLeft(roomId) } } catch (e: Throwable) { Timber.e(e, "Error while dispatching room left") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt index e2db0362bb..b0b25cb3b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt @@ -45,6 +45,10 @@ internal class DefaultProcessEventForPushTask @Inject constructor( params.syncResponse.leave.keys.forEach { defaultPushRuleService.dispatchRoomLeft(it) } + // Handle joined rooms + params.syncResponse.join.keys.forEach { + defaultPushRuleService.dispatchRoomJoined(it) + } val newJoinEvents = params.syncResponse.join .map { entries -> entries.value.timeline?.events?.map { it.copy(roomId = entries.key) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index 0fed679ed2..e46acfef7b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -173,8 +173,10 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room session.getRoom(roomId)?.leave(object : MatrixCallback { override fun onSuccess(data: Unit) { - // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. - // Instead, we wait for the room to be joined + // We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data. + // Instead, we wait for the room to be rejected + // Known bug: if the user is invited again (after rejecting the first invitation), the loading will be displayed instead of the buttons. + // If we update the state, the button will be displayed again, so it's not ideal... } override fun onFailure(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt index 1603ea5f00..b881dec3b8 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt @@ -17,6 +17,9 @@ package im.vector.riotx.features.notifications import java.io.Serializable +/** + * Parent interface for all events which can be displayed as a Notification + */ interface NotifiableEvent : Serializable { var matrixID: String? val eventId: String diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index 297e0b31e4..9710060c3c 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt @@ -150,7 +150,6 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St 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, diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 13a39b5828..3f9bbe2f52 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -138,7 +138,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } /** - Clear all known events and refresh the notification drawer + * Clear all known events and refresh the notification drawer */ fun clearAllEvents() { synchronized(eventList) { @@ -147,7 +147,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context refreshNotificationDrawer() } - /** Clear all known message events for this room and refresh the notification drawer */ + /** Clear all known message events for this room */ fun clearMessageEventOfRoom(roomId: String?) { Timber.v("clearMessageEventOfRoom $roomId") @@ -159,7 +159,6 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) } - refreshNotificationDrawer() } /** @@ -177,21 +176,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } } - fun homeActivityDidResume(matrixID: String?) { - synchronized(eventList) { - eventList.removeAll { e -> - // messages are cleared when entering room - e !is NotifiableMessageEvent - } - } - } - fun clearMemberShipNotificationForRoom(roomId: String) { synchronized(eventList) { eventList.removeAll { e -> e is InviteNotifiableEvent && e.roomId == roomId } } + + notificationUtils.cancelNotificationMessage(roomId, ROOM_INVITATION_NOTIFICATION_ID) } @@ -226,23 +218,26 @@ class NotificationDrawerManager @Inject constructor(private val context: Context //group events by room to create a single MessagingStyle notif val roomIdToEventMap: MutableMap> = LinkedHashMap() - val simpleEvents: MutableList = ArrayList() + val simpleEvents: MutableList = ArrayList() + val invitationEvents: MutableList = ArrayList() val eventIterator = eventList.listIterator() while (eventIterator.hasNext()) { - val event = eventIterator.next() - if (event is NotifiableMessageEvent) { - val roomId = event.roomId - val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() } + when (val event = eventIterator.next()) { + is NotifiableMessageEvent -> { + val roomId = event.roomId + val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() } - if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) { - //forget this event - eventIterator.remove() - } else { - roomEvents.add(event) + if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) { + //forget this event + eventIterator.remove() + } else { + roomEvents.add(event) + } } - } else { - simpleEvents.add(event) + is InviteNotifiableEvent -> invitationEvents.add(event) + is SimpleNotifiableEvent -> simpleEvents.add(event) + else -> Timber.w("Type not handled") } } @@ -251,7 +246,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context var globalLastMessageTimestamp = 0L - //events have been grouped by roomId + // events have been grouped by roomId for ((roomId, events) in roomIdToEventMap) { // Build the notification for the room if (events.isEmpty() || events.all { it.isRedacted }) { @@ -372,11 +367,24 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } - //Handle simple events - for (event in simpleEvents) { - //We build a simple event + // Handle invitation events + for (event in invitationEvents) { + //We build a invitation notification if (firstTime || !event.hasBeenDisplayed) { - val notification = notificationUtils.buildSimpleEventNotification(event, null, session.myUserId) + val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId) + notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification) + event.hasBeenDisplayed = true //we can consider it as displayed + hasNewEvent = true + summaryIsNoisy = summaryIsNoisy || event.noisy + summaryInboxStyle.addLine(event.description) + } + } + + // Handle simple events + for (event in simpleEvents) { + //We build a simple notification + if (firstTime || !event.hasBeenDisplayed) { + val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId) notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification) event.hasBeenDisplayed = true //we can consider it as displayed hasNewEvent = true @@ -499,7 +507,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context companion object { private const val SUMMARY_NOTIFICATION_ID = 0 private const val ROOM_MESSAGES_NOTIFICATION_ID = 1 - private const val ROOM_EVENT_NOTIFICATION_ID = 2 + private const val ROOM_INVITATION_NOTIFICATION_ID = 2 + private const val ROOM_EVENT_NOTIFICATION_ID = 3 // TODO Mutliaccount private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache" diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt index 2a4566d02c..793d056e0c 100755 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt @@ -484,8 +484,70 @@ class NotificationUtils @Inject constructor(private val context: Context, } - fun buildSimpleEventNotification(simpleNotifiableEvent: NotifiableEvent, - largeIcon: Bitmap?, + fun buildRoomInvitationNotification(inviteNotifiableEvent: InviteNotifiableEvent, + matrixId: String): Notification { + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + // Build the pending intent for when the notification is clicked + val smallIcon = R.drawable.ic_status_bar + + val channelID = if (inviteNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID + + return NotificationCompat.Builder(context, channelID) + .setContentTitle(stringProvider.getString(R.string.app_name)) + .setContentText(inviteNotifiableEvent.description) + .setGroup(stringProvider.getString(R.string.app_name)) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setSmallIcon(smallIcon) + .setColor(accentColor) + .apply { + val roomId = inviteNotifiableEvent.roomId + // offer to type a quick reject button + val rejectIntent = Intent(context, NotificationBroadcastReceiver::class.java) + rejectIntent.action = REJECT_ACTION + rejectIntent.data = Uri.parse("foobar://$roomId&$matrixId") + rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + val rejectIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), rejectIntent, + PendingIntent.FLAG_UPDATE_CURRENT) + + addAction( + R.drawable.vector_notification_reject_invitation, + stringProvider.getString(R.string.reject), + rejectIntentPendingIntent) + + // offer to type a quick accept button + val joinIntent = Intent(context, NotificationBroadcastReceiver::class.java) + joinIntent.action = JOIN_ACTION + joinIntent.data = Uri.parse("foobar://$roomId&$matrixId") + rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + val joinIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), joinIntent, + PendingIntent.FLAG_UPDATE_CURRENT) + addAction( + R.drawable.vector_notification_accept_invitation, + stringProvider.getString(R.string.join), + joinIntentPendingIntent) + + val contentIntent = Intent(context, HomeActivity::class.java) + contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + //pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that + contentIntent.data = Uri.parse("foobar://" + inviteNotifiableEvent.eventId) + setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0)) + + if (inviteNotifiableEvent.noisy) { + //Compat + priority = NotificationCompat.PRIORITY_DEFAULT + vectorPreferences.getNotificationRingTone()?.let { + setSound(it) + } + setLights(accentColor, 500, 500) + } else { + priority = NotificationCompat.PRIORITY_LOW + } + setAutoCancel(true) + } + .build() + } + + fun buildSimpleEventNotification(simpleNotifiableEvent: SimpleNotifiableEvent, matrixId: String): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked @@ -500,47 +562,14 @@ class NotificationUtils @Inject constructor(private val context: Context, .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) .setSmallIcon(smallIcon) .setColor(accentColor) + .setAutoCancel(true) .apply { - if (simpleNotifiableEvent is InviteNotifiableEvent) { - val roomId = simpleNotifiableEvent.roomId - // offer to type a quick reject button - val rejectIntent = Intent(context, NotificationBroadcastReceiver::class.java) - rejectIntent.action = REJECT_ACTION - rejectIntent.data = Uri.parse("foobar://$roomId&$matrixId") - rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) - val rejectIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), rejectIntent, - PendingIntent.FLAG_UPDATE_CURRENT) - - addAction( - R.drawable.vector_notification_reject_invitation, - stringProvider.getString(R.string.reject), - rejectIntentPendingIntent) - - // offer to type a quick accept button - val joinIntent = Intent(context, NotificationBroadcastReceiver::class.java) - joinIntent.action = JOIN_ACTION - joinIntent.data = Uri.parse("foobar://$roomId&$matrixId") - rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) - val joinIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), joinIntent, - PendingIntent.FLAG_UPDATE_CURRENT) - addAction( - R.drawable.vector_notification_accept_invitation, - stringProvider.getString(R.string.join), - joinIntentPendingIntent) - } else { - setAutoCancel(true) - } - val contentIntent = Intent(context, HomeActivity::class.java) contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP //pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that contentIntent.data = Uri.parse("foobar://" + simpleNotifiableEvent.eventId) setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0)) - if (largeIcon != null) { - setLargeIcon(largeIcon) - } - if (simpleNotifiableEvent.noisy) { //Compat priority = NotificationCompat.PRIORITY_DEFAULT diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt index 7f1c501334..f28ffb35f8 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt @@ -58,6 +58,11 @@ class PushRuleTriggerListener @Inject constructor( override fun onRoomLeft(roomId: String) { notificationDrawerManager.clearMessageEventOfRoom(roomId) + notificationDrawerManager.clearMemberShipNotificationForRoom(roomId) + } + + override fun onRoomJoined(roomId: String) { + notificationDrawerManager.clearMemberShipNotificationForRoom(roomId) } override fun onEventRedacted(redactedEventId: String) {