Merge branch 'develop' into feature/dla/emojibase_integration

This commit is contained in:
David Langley
2023-08-31 14:14:01 +01:00
committed by GitHub
24 changed files with 168 additions and 38 deletions

1
changelog.d/1198.bugfix Normal file
View File

@@ -0,0 +1 @@
Re-enable `SyncService.withEncryptionSync` to improve decryption of notifications.

1
changelog.d/1995.bugfix Normal file
View File

@@ -0,0 +1 @@
Crash with `aspectRatio` modifier when `Float.NaN` was used as input.

View File

@@ -202,6 +202,7 @@ class MessagesPresenter @AssistedInject constructor(
TimelineItemAction.Developer -> handleShowDebugInfoAction(targetEvent)
TimelineItemAction.Forward -> handleForwardAction(targetEvent)
TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent)
}
}
@@ -310,6 +311,11 @@ class MessagesPresenter @AssistedInject constructor(
navigator.onReportContentClicked(event.eventId, event.senderId)
}
private suspend fun handleEndPollAction(event: TimelineItem.Event) {
event.eventId?.let { room.endPoll(it, "The poll with event id: $it has ended.") }
// TODO Polls: Send poll end analytic
}
private suspend fun handleCopyContents(event: TimelineItem.Event) {
val content = when (event.content) {
is TimelineItemTextBasedContent -> event.content.body

View File

@@ -99,6 +99,16 @@ class ActionListPresenter @Inject constructor(
}
is TimelineItemPollContent -> {
buildList {
val isMineOrCanRedact = timelineItem.isMine || userCanRedact
// TODO Poll: Reply to poll
// if (timelineItem.isRemote) {
// // Can only reply or forward messages already uploaded to the server
// add(TimelineItemAction.Reply)
// }
if (!timelineItem.content.isEnded && timelineItem.isRemote && isMineOrCanRedact) {
add(TimelineItemAction.EndPoll)
}
if (timelineItem.content.canBeCopied()) {
add(TimelineItemAction.Copy)
}
@@ -108,7 +118,7 @@ class ActionListPresenter @Inject constructor(
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
if (timelineItem.isMine || userCanRedact) {
if (isMineOrCanRedact) {
add(TimelineItemAction.Redact)
}
}

View File

@@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -83,6 +84,15 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
),
displayEmojiReactions = false,
),
anActionListState().copy(
target = ActionListState.Target.Success(
event = aTimelineItemEvent(content = aTimelineItemPollContent()).copy(
reactionsState = reactionsState
),
actions = aTimelineItemPollActionList(),
),
displayEmojiReactions = false,
),
)
}
}
@@ -104,3 +114,11 @@ fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
TimelineItemAction.Developer,
)
}
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
return persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.EndPoll,
TimelineItemAction.Developer,
TimelineItemAction.Redact,
)
}

View File

@@ -35,4 +35,5 @@ sealed class TimelineItemAction(
data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit)
data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode)
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true)
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.EndPoll)
}

View File

@@ -132,10 +132,12 @@ class TimelineItemContentMessageFactory @Inject constructor(
}
private fun aspectRatioOf(width: Long?, height: Long?): Float? {
return if (height != null && width != null) {
val result = if (height != null && width != null) {
width.toFloat() / height.toFloat()
} else {
null
}
return result?.takeIf { it.isFinite() }
}
}

View File

@@ -18,6 +18,10 @@ package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.location.api.Location
import io.element.android.features.poll.api.PollAnswerItem
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
open class TimelineItemLocationContentProvider : PreviewParameterProvider<TimelineItemLocationContent> {
override val values: Sequence<TimelineItemLocationContent>
@@ -36,3 +40,32 @@ fun aTimelineItemLocationContent(description: String? = null) = TimelineItemLoca
),
description = description,
)
fun aTimelineItemPollContent(
isEnded: Boolean = false,
) = TimelineItemPollContent(
eventId = EventId("\$anEventId"),
question = "Some question?",
answerItems = listOf(
PollAnswerItem(
answer = PollAnswer("id_1", "Answer1"),
isSelected = false,
isEnabled = false,
isWinner = false,
isDisclosed = false,
votesCount = 0,
percentage = 0.0f,
),
PollAnswerItem(
answer = PollAnswer("id_2", "Answer2"),
isSelected = false,
isEnabled = false,
isWinner = false,
isDisclosed = false,
votesCount = 0,
percentage = 0.0f,
),
),
pollKind = PollKind.Disclosed,
isEnded = isEnded,
)

View File

@@ -73,6 +73,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.element.android.tests.testutils.waitForPredicate
import io.mockk.mockk
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -560,6 +561,24 @@ class MessagesPresenterTest {
}
}
@Test
fun `present - handle poll end`() = runTest {
val room = FakeMatrixRoom()
val presenter = createMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent()))
waitForPredicate { room.endPollInvocations.size == 1 }
cancelAndIgnoreRemainingEvents()
assertThat(room.endPollInvocations.size).isEqualTo(1)
assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.")
// TODO Polls: Test poll end analytic
}
}
private fun TestScope.createMessagePresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixRoom: MatrixRoom = FakeMatrixRoom(),

View File

@@ -26,15 +26,11 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
import io.element.android.features.poll.api.PollAnswerItem
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.core.aBuildMeta
import kotlinx.collections.immutable.persistentListOf
@@ -384,34 +380,34 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = true,
content = TimelineItemPollContent(
eventId = EventId("\$anEventId"),
question = "Some question?",
answerItems = listOf(
PollAnswerItem(
answer = PollAnswer("id_1", "Answer1"),
isSelected = false,
isEnabled = false,
isWinner = false,
isDisclosed = false,
votesCount = 0,
percentage = 0.0f,
),
PollAnswerItem(
answer = PollAnswer("id_2", "Answer2"),
isSelected = false,
isEnabled = false,
isWinner = false,
isDisclosed = false,
votesCount = 0,
percentage = 0.0f,
),
),
pollKind = PollKind.Disclosed,
isEnded = false,
content = aTimelineItemPollContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
messageEvent,
persistentListOf(
TimelineItemAction.EndPoll,
TimelineItemAction.Redact,
)
)
)
assertThat(successState.displayEmojiReactions).isTrue()
}
}
@Test
fun `present - compute for ended poll message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = false)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = true,
content = aTimelineItemPollContent(isEnded = true),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(

View File

@@ -71,7 +71,7 @@ fun CreatePollView(
val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) }
BackHandler(onBack = navBack)
if (state.showConfirmation) ConfirmationDialog(
content = stringResource(id = R.string.screen_create_poll_confirmation),
content = stringResource(id = R.string.screen_create_poll_discard_confirmation),
onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) },
onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) }
)

View File

@@ -4,7 +4,7 @@
<string name="screen_create_poll_anonymous_desc">"Show results only after poll ends"</string>
<string name="screen_create_poll_anonymous_headline">"Anonymous Poll"</string>
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
<string name="screen_create_poll_confirmation">"Are you sure you would like to go back?"</string>
<string name="screen_create_poll_discard_confirmation">"Are you sure you would like to go back?"</string>
<string name="screen_create_poll_question_desc">"Question or topic"</string>
<string name="screen_create_poll_question_hint">"What is the poll about?"</string>
<string name="screen_create_poll_title">"Create Poll"</string>

View File

@@ -83,7 +83,8 @@ class DeveloperSettingsPresenter @Inject constructor(
features,
enabledFeatures,
event.feature,
event.isEnabled
event.isEnabled,
triggerClearCache = { handleEvents(DeveloperSettingsEvents.ClearCache) }
)
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
}
@@ -122,12 +123,17 @@ class DeveloperSettingsPresenter @Inject constructor(
features: SnapshotStateMap<String, Feature>,
enabledFeatures: SnapshotStateMap<String, Boolean>,
featureUiModel: FeatureUiModel,
enabled: Boolean
enabled: Boolean,
triggerClearCache: () -> Unit,
) = launch {
val feature = features[featureUiModel.key] ?: return@launch
if (featureFlagService.setFeatureEnabled(feature, enabled)) {
enabledFeatures[featureUiModel.key] = enabled
}
if (featureUiModel.key == FeatureFlags.UseEncryptionSync.key) {
triggerClearCache()
}
}
private fun CoroutineScope.computeCacheSize(cacheSize: MutableState<Async<String>>) = launch {

View File

@@ -27,4 +27,5 @@ object VectorIcons {
val ReportContent = R.drawable.ic_report_content
val Groups = R.drawable.ic_groups
val Share = R.drawable.ic_share
val EndPoll = R.drawable.ic_done_24
}

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M382,720L154,492L211,435L382,606L749,239L806,296L382,720Z"/>
</vector>

View File

@@ -31,5 +31,11 @@ enum class FeatureFlags(
title = "Polls",
description = "Create poll and render poll events in the timeline",
defaultValue = false,
),
UseEncryptionSync(
key = "feature.useencryptionsync",
title = "Use encryption sync",
description = "Use the encryption sync API for decrypting notifications.",
defaultValue = true,
)
}

View File

@@ -31,6 +31,7 @@ class BuildtimeFeatureFlagProvider @Inject constructor() :
when (feature) {
FeatureFlags.LocationSharing -> true
FeatureFlags.Polls -> false
FeatureFlags.UseEncryptionSync -> true
}
} else {
false

View File

@@ -35,6 +35,7 @@ dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.libraries.network)
implementation(projects.services.toolbox.api)
implementation(projects.libraries.featureflag.api)
api(projects.libraries.matrix.api)
implementation(libs.dagger)
implementation(projects.libraries.core)

View File

@@ -19,6 +19,8 @@ package io.element.android.libraries.matrix.impl
import android.content.Context
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
@@ -39,6 +41,7 @@ class RustMatrixClientFactory @Inject constructor(
private val sessionStore: SessionStore,
private val userAgentProvider: UserAgentProvider,
private val clock: SystemClock,
private val featureFlagsService: FeatureFlagService,
) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
@@ -53,7 +56,12 @@ class RustMatrixClientFactory @Inject constructor(
client.restoreSession(sessionData.toSession())
val syncService = client.syncService().finish()
val syncService = client.syncService().apply {
if (featureFlagsService.isFeatureEnabled(FeatureFlags.UseEncryptionSync)) {
withEncryptionSync(withCrossProcessLock = false, appIdentifier = null)
}
}
.finish()
RustMatrixClient(
client = client,

View File

@@ -61,6 +61,7 @@
<string name="action_take_photo">"Take photo"</string>
<string name="action_view_source">"View Source"</string>
<string name="action_yes">"Yes"</string>
<string name="action_end_poll">"End poll"</string>
<string name="common_about">"About"</string>
<string name="common_acceptable_use_policy">"Acceptable use policy"</string>
<string name="common_analytics">"Analytics"</string>

View File

@@ -63,6 +63,7 @@ dependencies {
implementation(projects.features.login.impl)
implementation(projects.features.networkmonitor.impl)
implementation(projects.services.toolbox.impl)
implementation(projects.libraries.featureflag.impl)
implementation(libs.coroutines.core)
coreLibraryDesugaring(libs.android.desugar)
}

View File

@@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowCompat
import io.element.android.libraries.featureflag.impl.DefaultFeatureFlagService
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService
@@ -54,7 +55,8 @@ class MainActivity : ComponentActivity() {
coroutineDispatchers = Singleton.coroutineDispatchers,
sessionStore = sessionStore,
userAgentProvider = userAgentProvider,
clock = DefaultSystemClock()
clock = DefaultSystemClock(),
featureFlagsService = DefaultFeatureFlagService(emptySet())
)
)
}