EmptyRoom tile with quick actions

This commit is contained in:
Valere 2020-11-10 09:43:54 +01:00 committed by Benoit Marty
parent ca0da2c509
commit 2626a761ea
18 changed files with 468 additions and 71 deletions

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.permalinks package org.matrix.android.sdk.api.session.permalinks
import android.text.Spannable import android.text.Spannable
import org.matrix.android.sdk.api.MatrixPatterns
/** /**
* MatrixLinkify take a piece of text and turns all of the * MatrixLinkify take a piece of text and turns all of the
@ -35,7 +36,7 @@ object MatrixLinkify {
* I disable it because it mess up with pills, and even with pills, it does not work correctly: * I disable it because it mess up with pills, and even with pills, it does not work correctly:
* The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to * The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to
*/ */
/*
// sanity checks // sanity checks
if (spannable.isEmpty()) { if (spannable.isEmpty()) {
return false return false
@ -48,14 +49,21 @@ object MatrixLinkify {
val startPos = match.range.first val startPos = match.range.first
if (startPos == 0 || text[startPos - 1] != '/') { if (startPos == 0 || text[startPos - 1] != '/') {
val endPos = match.range.last + 1 val endPos = match.range.last + 1
val url = text.substring(match.range) var url = text.substring(match.range)
if (MatrixPatterns.isUserId(url)
|| MatrixPatterns.isRoomAlias(url)
|| MatrixPatterns.isRoomId(url)
|| MatrixPatterns.isGroupId(url)
|| MatrixPatterns.isEventId(url)) {
url = PermalinkService.MATRIX_TO_URL_BASE + url
}
val span = MatrixPermalinkSpan(url, callback) val span = MatrixPermalinkSpan(url, callback)
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
} }
} }
return hasMatch return hasMatch
*/
return false // return false
} }
} }

View File

@ -46,12 +46,14 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId
val roomMembers = RoomMemberHelper(realm, roomId) val roomMembers = RoomMemberHelper(realm, roomId)
val members = roomMembers.queryActiveRoomMembersEvent().findAll() val members = roomMembers.queryActiveRoomMembersEvent().findAll()
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
if (roomMembers.isDirectRoom()) {
if (members.size == 1) { if (members.size == 1) {
res = members.firstOrNull()?.avatarUrl res = members.firstOrNull()?.avatarUrl
} else if (members.size == 2) { } else if (members.size == 2) {
val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst()
res = firstOtherMember?.avatarUrl res = firstOtherMember?.avatarUrl
} }
}
return res return res
} }
} }

View File

@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import io.realm.Realm import io.realm.Realm
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> { internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> {
@ -50,9 +51,14 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor(
} else if (!params.searchOnServer) { } else if (!params.searchOnServer) {
Optional.from<String>(null) Optional.from<String>(null)
} else { } else {
roomId = executeRequest<RoomAliasDescription>(eventBus) { roomId = try {
executeRequest<RoomAliasDescription>(eventBus) {
apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) apiCall = roomAPI.getRoomIdByAlias(params.roomAlias)
}.roomId }.roomId
} catch (throwable: Throwable) {
Timber.d(throwable, "## Failed to get roomId from alias")
null
}
Optional.from(roomId) Optional.from(roomId)
} }
} }

View File

@ -93,6 +93,9 @@ internal class RoomDisplayNameResolver @Inject constructor(
} }
} else if (roomEntity?.membership == Membership.JOIN) { } else if (roomEntity?.membership == Membership.JOIN) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val invitedCount = roomSummary?.invitedMembersCount ?: 0
val joinedCount = roomSummary?.joinedMembersCount ?: 0
val othersTotalCount = invitedCount + joinedCount - 1
val otherMembersSubset: List<RoomMemberSummaryEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) { val otherMembersSubset: List<RoomMemberSummaryEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes.mapNotNull { userId -> roomSummary.heroes.mapNotNull { userId ->
roomMembers.getLastRoomMember(userId)?.takeIf { roomMembers.getLastRoomMember(userId)?.takeIf {
@ -102,22 +105,29 @@ internal class RoomDisplayNameResolver @Inject constructor(
} else { } else {
activeMembers.where() activeMembers.where()
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.limit(3) .limit(5)
.findAll() .findAll()
.createSnapshot() .createSnapshot()
} }
val otherMembersCount = otherMembersSubset.count() val otherMembersCount = otherMembersSubset.count()
name = when (otherMembersCount) { name = when (otherMembersCount) {
0 -> stringProvider.getString(R.string.room_displayname_empty_room) 0 -> {
stringProvider.getString(R.string.room_displayname_empty_room)
// TODO (was xx and yyy) ...
}
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
2 -> stringProvider.getString(R.string.room_displayname_two_members, else -> {
resolveRoomMemberName(otherMembersSubset[0], roomMembers), val names = otherMembersSubset.map {
resolveRoomMemberName(otherMembersSubset[1], roomMembers) resolveRoomMemberName(it, roomMembers) ?: ""
) }
else -> stringProvider.getQuantityString(R.plurals.room_displayname_three_and_more_members, if (otherMembersCount <= othersTotalCount) {
roomMembers.getNumberOfJoinedMembers() - 1, val remainingCount = invitedCount + joinedCount - names.size
resolveRoomMemberName(otherMembersSubset[0], roomMembers), (names.joinToString("${stringProvider.getString(R.string.room_displayname_separator)} ")
roomMembers.getNumberOfJoinedMembers() - 1) + " " + stringProvider.getQuantityString(R.plurals.and_n_others, remainingCount, remainingCount))
} else {
names.dropLast(1).joinToString(", ") + " & ${names.last()}"
}
}
} }
} }
return name ?: roomId return name ?: roomId

View File

@ -98,6 +98,10 @@ internal class RoomMemberHelper(private val realm: Realm,
return getNumberOfJoinedMembers() + getNumberOfInvitedMembers() return getNumberOfJoinedMembers() + getNumberOfInvitedMembers()
} }
fun isDirectRoom() : Boolean {
return roomSummary?.isDirect ?: false
}
/** /**
* Return all the roomMembers ids which are joined or invited to the room * Return all the roomMembers ids which are joined or invited to the room
* *

View File

@ -180,8 +180,14 @@
<item quantity="one">%1$s and 1 other</item> <item quantity="one">%1$s and 1 other</item>
<item quantity="other">%1$s and %2$d others</item> <item quantity="other">%1$s and %2$d others</item>
</plurals> </plurals>
<plurals name="and_n_others">
<item quantity="one">&amp; %d other</item>
<item quantity="other">&amp; %d others</item>
</plurals>
<string name="room_displayname_separator">,</string>
<string name="room_displayname_empty_room">Empty room</string> <string name="room_displayname_empty_room">Empty room</string>
<string name="room_displayname_empty_room_was">Empty room (was %s)</string>
<string name="initial_sync_start_importing_account">Initial Sync:\nImporting account…</string> <string name="initial_sync_start_importing_account">Initial Sync:\nImporting account…</string>
<string name="initial_sync_start_importing_account_crypto">Initial Sync:\nImporting crypto</string> <string name="initial_sync_start_importing_account_crypto">Initial Sync:\nImporting crypto</string>

View File

@ -16,6 +16,8 @@
package im.vector.app.features.home.room.detail package im.vector.app.features.home.room.detail
import android.net.Uri
import android.view.View
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -24,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.util.MatrixItem
sealed class RoomDetailAction : VectorViewModelAction { sealed class RoomDetailAction : VectorViewModelAction {
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction() data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
@ -90,4 +93,9 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
data class JumpToReadReceipt(val userId: String) : RoomDetailAction() data class JumpToReadReceipt(val userId: String) : RoomDetailAction()
object QuickActionInvitePeople : RoomDetailAction()
object QuickActionSetAvatar : RoomDetailAction()
data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction()
object QuickActionSetTopic : RoomDetailAction()
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction()
} }

View File

@ -71,6 +71,7 @@ import com.google.android.material.textfield.TextInputEditText
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.ConfirmationDialogBuilder
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.dialogs.withColoredButton import im.vector.app.core.dialogs.withColoredButton
import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
@ -82,6 +83,7 @@ import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.extensions.trackItemsVisibilityChange import im.vector.app.core.extensions.trackItemsVisibilityChange
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.core.glide.GlideRequests import im.vector.app.core.glide.GlideRequests
import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.intent.getMimeTypeFromUri
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
@ -149,6 +151,7 @@ import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.NavigationInterceptor
import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.reactions.EmojiReactionPickerActivity import im.vector.app.features.reactions.EmojiReactionPickerActivity
import im.vector.app.features.roomprofile.RoomProfileActivity
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.share.SharedData import im.vector.app.features.share.SharedData
@ -196,6 +199,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.net.URL import java.net.URL
import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -229,7 +233,7 @@ class RoomDetailFragment @Inject constructor(
JumpToReadMarkerView.Callback, JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback, AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback, AttachmentsHelper.Callback,
// RoomWidgetsBannerView.Callback, GalleryOrCameraDialogHelper.Listener,
ActiveCallView.Callback { ActiveCallView.Callback {
companion object { companion object {
@ -250,6 +254,8 @@ class RoomDetailFragment @Inject constructor(
private const val ircPattern = " (IRC)" private const val ircPattern = " (IRC)"
} }
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
private val roomDetailArgs: RoomDetailArgs by args() private val roomDetailArgs: RoomDetailArgs by args()
private val glideRequests by lazy { private val glideRequests by lazy {
GlideApp.with(this) GlideApp.with(this)
@ -364,6 +370,12 @@ class RoomDetailFragment @Inject constructor(
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it)
RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId)
RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show()
RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings()
is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item ->
navigator.openBigImageViewer(requireActivity(), it.view, item)
}
}.exhaustive }.exhaustive
} }
@ -372,6 +384,24 @@ class RoomDetailFragment @Inject constructor(
} }
} }
override fun onImageReady(uri: Uri?) {
uri ?: return
roomDetailViewModel.handle(
RoomDetailAction.SetAvatarAction(
newAvatarUri = uri,
newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()
)
)
}
private fun handleOpenRoomSettings() {
navigator.openRoomProfile(
requireContext(),
roomDetailArgs.roomId,
RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS
)
}
private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) { private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) {
navigator.openRoom(requireContext(), openRoom.roomId, null) navigator.openRoom(requireContext(), openRoom.roomId, null)
} }

View File

@ -17,10 +17,12 @@
package im.vector.app.features.home.room.detail package im.vector.app.features.home.room.detail
import android.net.Uri import android.net.Uri
import android.view.View
import androidx.annotation.StringRes import androidx.annotation.StringRes
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.features.command.Command import im.vector.app.features.command.Command
import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import java.io.File import java.io.File
@ -43,6 +45,11 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents() data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents() data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
object OpenInvitePeople : RoomDetailViewEvents()
object OpenSetRoomAvatarDialog : RoomDetailViewEvents()
object OpenRoomSettings : RoomDetailViewEvents()
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents()
object ShowWaitingView : RoomDetailViewEvents() object ShowWaitingView : RoomDetailViewEvents()
object HideWaitingView : RoomDetailViewEvents() object HideWaitingView : RoomDetailViewEvents()

View File

@ -277,9 +277,39 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.CancelSend -> handleCancel(action) is RoomDetailAction.CancelSend -> handleCancel(action)
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
_viewEvents.post(
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
)
}
}.exhaustive }.exhaustive
} }
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
viewModelScope.launch(Dispatchers.IO) {
try {
awaitCallback<Unit> {
room.updateAvatar(action.newAvatarUri, action.newAvatarFileName, it)
}
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
}
}
}
private fun handleInvitePeople() {
_viewEvents.post(RoomDetailViewEvents.OpenInvitePeople)
}
private fun handleQuickSetAvatar() {
_viewEvents.post(RoomDetailViewEvents.OpenSetRoomAvatarDialog)
}
private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) { private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) {
val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId) val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId)
if (existingDmRoomId == null) { if (existingDmRoomId == null) {

View File

@ -31,10 +31,14 @@ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEve
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_ import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
@ -187,6 +191,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
collapsedEventIds.removeAll(mergedEventIds) collapsedEventIds.removeAll(mergedEventIds)
} }
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val powerLevelsHelper = roomSummaryHolder.roomSummary?.roomId
?.let { activeSessionHolder.getSafeActiveSession()?.getRoom(it) }
?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)?.content?.toModel<PowerLevelsContent>() }
?.let { PowerLevelsHelper(it) }
val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: ""
val attributes = MergedRoomCreationItem.Attributes( val attributes = MergedRoomCreationItem.Attributes(
isCollapsed = isCollapsed, isCollapsed = isCollapsed,
mergeData = mergedData, mergeData = mergedData,
@ -198,13 +207,19 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
hasEncryptionEvent = hasEncryption, hasEncryptionEvent = hasEncryption,
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
readReceiptsCallback = callback, readReceiptsCallback = callback,
currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: "" callback = callback,
currentUserId = currentUserId,
roomSummary = roomSummaryHolder.roomSummary,
canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
) )
MergedRoomCreationItem_() MergedRoomCreationItem_()
.id(mergeId) .id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(isCollapsed && highlighted) .highlighted(isCollapsed && highlighted)
.attributes(attributes) .attributes(attributes)
.movementMethod(createLinkMovementMethod(callback))
.also { .also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
} }

View File

@ -16,11 +16,14 @@
package im.vector.app.features.home.room.detail.timeline.item package im.vector.app.features.home.room.detail.timeline.item
import android.text.SpannableString
import android.text.method.MovementMethod
import android.text.style.ClickableSpan
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -28,8 +31,16 @@ import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.core.utils.tappableMatchingText
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.Holder>() { abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.Holder>() {
@ -37,11 +48,16 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
@EpoxyAttribute @EpoxyAttribute
override lateinit var attributes: Attributes override lateinit var attributes: Attributes
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var movementMethod: MovementMethod? = null
override fun getViewType() = STUB_ID override fun getViewType() = STUB_ID
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
bindCreationSummaryTile(holder)
if (attributes.isCollapsed) { if (attributes.isCollapsed) {
// Take the oldest data // Take the oldest data
val data = distinctMergeData.lastOrNull() val data = distinctMergeData.lastOrNull()
@ -70,9 +86,20 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
holder.avatarView.visibility = View.GONE holder.avatarView.visibility = View.GONE
} }
bindEncryptionTile(holder, data)
} else {
holder.avatarView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE
holder.encryptionTile.isGone = true
}
// No read receipt for this item
holder.readReceiptsView.isVisible = false
}
private fun bindEncryptionTile(holder: Holder, data: Data?) {
if (attributes.hasEncryptionEvent) { if (attributes.hasEncryptionEvent) {
holder.encryptionTile.isVisible = true holder.encryptionTile.isVisible = true
holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> { holder.encryptionTile.updateLayoutParams<ConstraintLayout.LayoutParams> {
this.marginEnd = leftGuideline this.marginEnd = leftGuideline
} }
if (attributes.isEncryptionAlgorithmSecure) { if (attributes.isEncryptionAlgorithmSecure) {
@ -98,13 +125,76 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
} else { } else {
holder.encryptionTile.isVisible = false holder.encryptionTile.isVisible = false
} }
} else {
holder.avatarView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE
holder.encryptionTile.isGone = true
} }
// No read receipt for this item
holder.readReceiptsView.isVisible = false private fun bindCreationSummaryTile(holder: Holder) {
val roomSummary = attributes.roomSummary
val roomDisplayName = roomSummary?.displayName
holder.roomNameText.setTextOrHide(roomDisplayName)
val isDirect = roomSummary?.isDirect == true
val membersCount = roomSummary?.otherMemberIds?.size ?: 0
if (isDirect) {
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_dm, roomSummary?.displayName ?: "")
} else if (roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank()) {
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
} else {
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
}
val topic = roomSummary?.topic
if (topic.isNullOrBlank()) {
// do not show hint for DMs or group DMs
if (!isDirect) {
val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text)
val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink))
holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() {
override fun onClick(widget: View) {
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic)
}
}))
}
} else {
holder.roomTopicText.setTextOrHide(span {
span(holder.view.resources.getString(R.string.topic_prefix)) {
textStyle = "bold"
}
+topic.linkify(attributes.callback)
}
)
}
holder.roomTopicText.movementMethod = movementMethod
val roomItem = roomSummary?.toMatrixItem()
if (roomItem != null) {
holder.roomAvatarImageView.isVisible = true
attributes.avatarRenderer.render(roomItem, holder.roomAvatarImageView)
holder.roomAvatarImageView.setOnClickListener(DebouncedClickListener({ view ->
attributes.callback?.onTimelineItemAction(RoomDetailAction.ShowRoomAvatarFullScreen(roomItem, view))
}))
} else {
holder.roomAvatarImageView.isVisible = false
}
if (isDirect) {
holder.addPeopleButton.isVisible = false
} else {
holder.addPeopleButton.isVisible = true
holder.addPeopleButton.setOnClickListener(DebouncedClickListener({ _ ->
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionInvitePeople)
}))
}
val shouldShowSetAvatar = attributes.canChangeAvatar
&& (roomSummary?.isDirect == false || (isDirect && membersCount >= 2))
if (shouldShowSetAvatar && roomItem?.avatarUrl.isNullOrBlank()) {
holder.setAvatarButton.isVisible = true
holder.setAvatarButton.setOnClickListener(DebouncedClickListener({ _ ->
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar)
}))
} else {
holder.setAvatarButton.isVisible = false
}
} }
class Holder : BasedMergedItem.Holder(STUB_ID) { class Holder : BasedMergedItem.Holder(STUB_ID) {
@ -114,6 +204,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView) val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView) val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView)
val roomNameText by bind<TextView>(R.id.roomNameTileText)
val roomDescriptionText by bind<TextView>(R.id.roomNameDescriptionText)
val roomTopicText by bind<TextView>(R.id.roomNameTopicText)
val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val addPeopleButton by bind<View>(R.id.creationTileAddPeopleButton)
val setAvatarButton by bind<View>(R.id.creationTileSetAvatarButton)
} }
companion object { companion object {
@ -126,8 +223,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
override val avatarRenderer: AvatarRenderer, override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit, override val onCollapsedStateChanged: (Boolean) -> Unit,
val callback: TimelineEventController.Callback? = null,
val currentUserId: String, val currentUserId: String,
val hasEncryptionEvent: Boolean, val hasEncryptionEvent: Boolean,
val isEncryptionAlgorithmSecure: Boolean val isEncryptionAlgorithmSecure: Boolean,
val roomSummary: RoomSummary?,
val canChangeAvatar: Boolean = false,
val canChangeName: Boolean = false,
val canChangeTopic: Boolean = false
) : BasedMergedItem.Attributes ) : BasedMergedItem.Attributes
} }

View File

@ -248,8 +248,8 @@ class DefaultNavigator @Inject constructor(
context.startActivity(KeysBackupManageActivity.intent(context)) context.startActivity(KeysBackupManageActivity.intent(context))
} }
override fun openRoomProfile(context: Context, roomId: String) { override fun openRoomProfile(context: Context, roomId: String, directAccess: Int?) {
context.startActivity(RoomProfileActivity.newIntent(context, roomId)) context.startActivity(RoomProfileActivity.newIntent(context, roomId, directAccess))
} }
override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) { override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) {

View File

@ -78,7 +78,7 @@ interface Navigator {
fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean = false) fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean = false)
fun openRoomProfile(context: Context, roomId: String) fun openRoomProfile(context: Context, roomId: String, directAccess: Int? = null)
fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem)

View File

@ -46,10 +46,16 @@ class RoomProfileActivity :
companion object { companion object {
fun newIntent(context: Context, roomId: String): Intent { private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS"
const val EXTRA_DIRECT_ACCESS_ROOM_ROOT = 0
const val EXTRA_DIRECT_ACCESS_ROOM_SETTINGS = 1
fun newIntent(context: Context, roomId: String, directAccess: Int?): Intent {
val roomProfileArgs = RoomProfileArgs(roomId) val roomProfileArgs = RoomProfileArgs(roomId)
return Intent(context, RoomProfileActivity::class.java).apply { return Intent(context, RoomProfileActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, roomProfileArgs) putExtra(MvRx.KEY_ARG, roomProfileArgs)
putExtra(EXTRA_DIRECT_ACCESS, directAccess)
} }
} }
} }
@ -80,7 +86,13 @@ class RoomProfileActivity :
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java) sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return
if (isFirstCreation()) { if (isFirstCreation()) {
when (intent?.extras?.getInt(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOM_ROOT)) {
EXTRA_DIRECT_ACCESS_ROOM_SETTINGS -> {
addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs) addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomSettingsFragment::class.java, roomProfileArgs)
}
else -> addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
}
} }
sharedActionViewModel sharedActionViewModel
.observe() .observe()

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19.1001,9C18.7779,9 18.5168,8.7388 18.5168,8.4167V6.0833H16.1834C15.8613,6.0833 15.6001,5.8222 15.6001,5.5C15.6001,5.1778 15.8613,4.9167 16.1834,4.9167H18.5168V2.5833C18.5168,2.2612 18.7779,2 19.1001,2C19.4223,2 19.6834,2.2612 19.6834,2.5833V4.9167H22.0168C22.3389,4.9167 22.6001,5.1778 22.6001,5.5C22.6001,5.8222 22.3389,6.0833 22.0168,6.0833H19.6834V8.4167C19.6834,8.7388 19.4223,9 19.1001,9ZM19.6001,11C20.0669,11 20.5212,10.9467 20.9574,10.8458C21.1161,11.5383 21.2,12.2594 21.2,13C21.2,16.1409 19.6917,18.9294 17.3598,20.6808V20.6807C16.0014,21.7011 14.3635,22.3695 12.5815,22.5505C12.2588,22.5832 11.9314,22.6 11.6,22.6C6.2981,22.6 2,18.302 2,13C2,7.6981 6.2981,3.4 11.6,3.4C12.3407,3.4 13.0618,3.4839 13.7543,3.6427C13.6534,4.0788 13.6001,4.5332 13.6001,5C13.6001,8.3137 16.2864,11 19.6001,11ZM11.5999,20.68C13.6754,20.68 15.5585,19.8567 16.9407,18.5189C16.0859,16.4086 14.0167,14.92 11.5998,14.92C9.183,14.92 7.1138,16.4086 6.259,18.5189C7.6411,19.8567 9.5244,20.68 11.5999,20.68ZM11.7426,7.4117C10.3168,7.5417 9.2,8.7404 9.2,10.2C9.2,11.7464 10.4536,13 12,13C13.0308,13 13.9315,12.443 14.4176,11.6135C13.0673,10.6058 12.0929,9.1225 11.7426,7.4117Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -1,31 +1,168 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/creationTile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginTop="0dp">
<FrameLayout <FrameLayout
android:id="@+id/creationEncryptionTile" android:id="@+id/creationEncryptionTile"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_gravity="center"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:layout_marginEnd="52dp" android:layout_marginEnd="52dp"
android:layout_marginBottom="2dp" android:layout_marginBottom="2dp"
android:background="@drawable/rounded_rect_shape_8" android:background="@drawable/rounded_rect_shape_8"
android:padding="8dp"> android:padding="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<include layout="@layout/item_timeline_event_status_tile_stub" /> <include layout="@layout/item_timeline_event_status_tile_stub" />
</FrameLayout> </FrameLayout>
<ImageView
android:id="@+id/roomAvatarImageView"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginTop="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/creationEncryptionTile"
tools:srcCompat="@tools:sample/avatars" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button.Icon"
android:id="@+id/creationTileSetAvatarButton"
android:layout_width="30dp"
android:layout_height="30dp"
android:backgroundTint="?riotx_bottom_nav_background_color"
android:contentDescription="@string/room_settings_set_avatar"
android:elevation="2dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:cornerRadius="30dp"
app:icon="@drawable/ic_camera"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconSize="20dp"
app:iconTint="?riot_primary_text_color"
app:layout_constraintCircle="@+id/roomAvatarImageView"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="34dp"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/roomNameTileText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomAvatarImageView"
tools:text="Room Name" />
<TextView
android:id="@+id/roomNameDescriptionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
android:textStyle="normal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomNameTileText"
tools:text="This is the beginning of Room name. " />
<TextView
android:id="@+id/roomNameTopicText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:textColor="?riotx_text_secondary"
android:textColorLink="@color/riotx_accent"
android:textSize="15sp"
android:textStyle="normal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomNameDescriptionText"
tools:text="Add a topic to let people know what this room is about. " />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/creationTileAddPeopleButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/add_people"
android:focusable="true"
android:maxWidth="90dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomNameTopicText">
<View
android:id="@+id/addPeopleButtonBg"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/circle"
android:backgroundTint="@color/riotx_accent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/addPeopleButtonIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@drawable/circle"
android:backgroundTint="@color/riotx_accent"
android:src="@drawable/ic_add_people"
app:layout_constraintBottom_toBottomOf="@id/addPeopleButtonBg"
app:layout_constraintEnd_toEndOf="@id/addPeopleButtonBg"
app:layout_constraintStart_toStartOf="@id/addPeopleButtonBg"
app:layout_constraintTop_toTopOf="@id/addPeopleButtonBg"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/add_people"
android:textColor="@color/riotx_accent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/addPeopleButtonBg" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<RelativeLayout <RelativeLayout
android:id="@+id/mergedSumContainer" android:id="@+id/mergedSumContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/creationEncryptionTile"> android:layout_below="@+id/creationTile"
android:layout_marginTop="8dp">
<ImageView <ImageView
android:id="@+id/itemNoticeAvatarView" android:id="@+id/itemNoticeAvatarView"

View File

@ -2403,6 +2403,13 @@
<string name="room_created_summary_item_by_you">You created and configured the room.</string> <string name="room_created_summary_item_by_you">You created and configured the room.</string>
<string name="direct_room_created_summary_item">%s joined.</string> <string name="direct_room_created_summary_item">%s joined.</string>
<string name="direct_room_created_summary_item_by_you">You joined.</string> <string name="direct_room_created_summary_item_by_you">You joined.</string>
<string name="this_is_the_beginning_of_room">This is the beginning of %s.</string>
<string name="this_is_the_beginning_of_room_no_name">This is the beginning of this conversation.</string>
<string name="this_is_the_beginning_of_dm">This is the beginning of your direct message history with %s.</string>
<!-- First param will be replaced by the value of add_a_topic_link_text, that will be clickable-->
<string name="room_created_summary_no_topic_creation_text">%s to let people know what this room is about. </string>
<string name="add_a_topic_link_text">Add a topic</string>
<string name="topic_prefix">"Topic: "</string>
<string name="qr_code_scanned_self_verif_notice">Almost there! Is the other device showing the same shield?</string> <string name="qr_code_scanned_self_verif_notice">Almost there! Is the other device showing the same shield?</string>
<string name="qr_code_scanned_verif_waiting_notice">Almost there! Waiting for confirmation…</string> <string name="qr_code_scanned_verif_waiting_notice">Almost there! Waiting for confirmation…</string>
@ -2511,6 +2518,7 @@
<string name="create_room_dm_failure">"We couldn't create your DM. Please check the users you want to invite and try again."</string> <string name="create_room_dm_failure">"We couldn't create your DM. Please check the users you want to invite and try again."</string>
<string name="add_members_to_room">Add members</string> <string name="add_members_to_room">Add members</string>
<string name="add_people">Add people</string>
<string name="invite_users_to_room_action_invite">INVITE</string> <string name="invite_users_to_room_action_invite">INVITE</string>
<string name="inviting_users_to_room">Inviting users…</string> <string name="inviting_users_to_room">Inviting users…</string>
<string name="invite_users_to_room_title">Invite Users</string> <string name="invite_users_to_room_title">Invite Users</string>
@ -2575,6 +2583,8 @@
<string name="room_settings_name_hint">Room Name</string> <string name="room_settings_name_hint">Room Name</string>
<string name="room_settings_topic_hint">Topic</string> <string name="room_settings_topic_hint">Topic</string>
<string name="room_settings_save_success">You changed room settings successfully</string> <string name="room_settings_save_success">You changed room settings successfully</string>
<string name="room_settings_set_avatar">Set avatar</string>
<string name="notice_crypto_unable_to_decrypt_final">You cannot access this message</string> <string name="notice_crypto_unable_to_decrypt_final">You cannot access this message</string>
<string name="notice_crypto_unable_to_decrypt_friendly">Waiting for this message, this may take a while</string> <string name="notice_crypto_unable_to_decrypt_friendly">Waiting for this message, this may take a while</string>