Skip to content

Commit 48de493

Browse files
committed
feat(fc): add report user for message self defense control
Signed-off-by: Brandon McAnsh <[email protected]>
1 parent 712d552 commit 48de493

File tree

10 files changed

+174
-27
lines changed

10 files changed

+174
-27
lines changed

definitions/flipchat/protos/src/main/proto/chat/v1/flipchat_service.proto

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ service Chat {
4242
rpc SetCoverCharge(SetCoverChargeRequest) returns (SetCoverChargeResponse);
4343
// RemoveUser removes a user from a chat
4444
rpc RemoveUser(RemoveUserRequest) returns (RemoveUserResponse);
45+
// ReportUser reports a user for a given message
46+
//
47+
// todo: might belong in a different service long-term
48+
rpc ReportUser(ReportUserRequest) returns (ReportUserResponse);
4549
}
4650
message StreamChatEventsRequest {
4751
oneof type {
@@ -256,6 +260,17 @@ message RemoveUserResponse{
256260
DENIED = 1;
257261
}
258262
}
263+
message ReportUserRequest{
264+
common.v1.UserId user_id = 1;
265+
messaging.v1.MessageId message_id = 2;
266+
common.v1.Auth auth = 3;
267+
}
268+
message ReportUserResponse{
269+
Result result = 1;
270+
enum Result {
271+
OK = 0;
272+
}
273+
}
259274
message Metadata {
260275
common.v1.ChatId chat_id = 1;
261276
// The type of chat

flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ private data class MessageActionContextSheet(val actions: List<MessageControlAct
288288
is MessageControlAction.Copy -> stringResource(R.string.action_copyMessage)
289289
is MessageControlAction.Delete -> stringResource(R.string.action_deleteMessage)
290290
is MessageControlAction.RemoveUser -> stringResource(R.string.action_removeUser, action.name)
291+
is MessageControlAction.ReportUserForMessage -> stringResource(R.string.action_report)
291292
},
292293
style = CodeTheme.typography.textMedium,
293294
modifier = Modifier

flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationViewModel.kt

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import kotlinx.coroutines.launch
5555
import kotlinx.datetime.Instant
5656
import timber.log.Timber
5757
import xyz.flipchat.chat.RoomController
58+
import xyz.flipchat.services.domain.model.chat.ConversationMember
59+
import xyz.flipchat.services.domain.model.chat.ConversationMessage
5860
import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastPointers
5961
import xyz.flipchat.services.user.UserManager
6062
import java.util.UUID
@@ -145,6 +147,7 @@ class ConversationViewModel @Inject constructor(
145147
data class CopyMessage(val text: String) : Event
146148
data class DeleteMessage(val conversationId: ID, val messageId: ID) : Event
147149
data class RemoveUser(val conversationId: ID, val userId: ID) : Event
150+
data class ReportUser(val userId: ID, val messageId: ID) : Event
148151

149152
data object OnUserTypingStarted : Event
150153
data object OnUserTypingStopped : Event
@@ -354,6 +357,20 @@ class ConversationViewModel @Inject constructor(
354357
}
355358
.launchIn(viewModelScope)
356359

360+
eventFlow
361+
.filterIsInstance<Event.ReportUser>()
362+
.map { (userId, messageId) ->
363+
roomController.reportUserForMessage(userId, messageId)
364+
}.onError {
365+
TopBarManager.showMessage(
366+
TopBarManager.TopBarMessage(
367+
title = resources.getString(R.string.error_title_failedToReportUserForMessage),
368+
message = resources.getString(R.string.error_description_failedToReportUserForMessage)
369+
)
370+
)
371+
}
372+
.launchIn(viewModelScope)
373+
357374
eventFlow
358375
.filterIsInstance<Event.CopyMessage>()
359376
.map { it.text }
@@ -406,30 +423,6 @@ class ConversationViewModel @Inject constructor(
406423
fallback = if (contents.isFromSelf) MessageStatus.Sent else MessageStatus.Unknown
407424
)
408425

409-
val selfDefenseActions = mutableListOf<MessageControlAction>().apply {
410-
if (stateFlow.value.isHost) {
411-
// add(
412-
// MessageControlAction.Delete {
413-
// confirmMessageDelete(
414-
// conversationId = message.conversationId,
415-
// messageId = message.id
416-
// )
417-
// }
418-
// )
419-
if (member?.memberName?.isNotEmpty() == true && !contents.isFromSelf) {
420-
add(
421-
MessageControlAction.RemoveUser(member.memberName.orEmpty()) {
422-
confirmUserRemoval(
423-
conversationId = message.conversationId,
424-
user = member.memberName,
425-
userId = message.senderId
426-
)
427-
}
428-
)
429-
}
430-
}
431-
}
432-
433426
ChatItem.Message(
434427
chatMessageId = message.id,
435428
message = contents,
@@ -457,13 +450,55 @@ class ConversationViewModel @Inject constructor(
457450
)
458451
)
459452
}
460-
) + selfDefenseActions,
453+
) + buildSelfDefenseControls(message, member, contents),
461454
),
462455
key = contents.hashCode() + message.id.hashCode()
463456
)
464457
}
465458
}
466459

460+
private fun buildSelfDefenseControls(
461+
message: ConversationMessage,
462+
member: ConversationMember?,
463+
contents: MessageContent
464+
): List<MessageControlAction> {
465+
return mutableListOf<MessageControlAction>().apply {
466+
if (stateFlow.value.isHost) {
467+
// add(
468+
// MessageControlAction.Delete {
469+
// confirmMessageDelete(
470+
// conversationId = message.conversationId,
471+
// messageId = message.id
472+
// )
473+
// }
474+
// )
475+
if (member?.memberName?.isNotEmpty() == true && !contents.isFromSelf) {
476+
add(
477+
MessageControlAction.RemoveUser(member.memberName.orEmpty()) {
478+
confirmUserRemoval(
479+
conversationId = message.conversationId,
480+
user = member.memberName,
481+
userId = message.senderId,
482+
)
483+
}
484+
)
485+
}
486+
}
487+
488+
if (!contents.isFromSelf) {
489+
add(
490+
MessageControlAction.ReportUserForMessage(member?.memberName.orEmpty()) {
491+
confirmUserReport(
492+
user = member?.memberName,
493+
userId = message.senderId,
494+
messageId = message.id
495+
)
496+
}
497+
)
498+
}
499+
}.toList()
500+
}
501+
467502
private fun confirmMessageDelete(conversationId: ID, messageId: ID) {
468503
BottomBarManager.showMessage(
469504
BottomBarManager.BottomBarMessage(
@@ -494,6 +529,22 @@ class ConversationViewModel @Inject constructor(
494529
)
495530
}
496531

532+
private fun confirmUserReport(user: String?, userId: ID, messageId: ID) {
533+
BottomBarManager.showMessage(
534+
BottomBarManager.BottomBarMessage(
535+
title = resources.getString(R.string.title_reportUserForMessage, user ?: "User"),
536+
subtitle = resources.getString(R.string.subtitle_reportUserForMessage),
537+
positiveText = resources.getString(R.string.action_report),
538+
negativeText = "",
539+
tertiaryText = resources.getString(R.string.action_cancel),
540+
onPositive = { dispatchEvent(Event.ReportUser(userId, messageId)) },
541+
onNegative = { },
542+
type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE,
543+
showScrim = true,
544+
)
545+
)
546+
}
547+
497548
override fun onCleared() {
498549
super.onCleared()
499550
roomController.closeMessageStream()
@@ -574,6 +625,7 @@ class ConversationViewModel @Inject constructor(
574625
is Event.DeleteMessage,
575626
is Event.CopyMessage,
576627
is Event.RemoveUser,
628+
is Event.ReportUser,
577629
is Event.ReopenStream,
578630
is Event.CloseStream,
579631
is Event.SendMessage -> { state -> state }

flipchatApp/src/main/res/values/strings.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@
3838
<string name="error_description_failedToLeaveRoom">You were unable to leave the room at this time. Please try again later.</string>
3939

4040
<string name="error_title_failedToDeleteMessage">Something Went Wrong</string>
41-
<string name="error_description_failedToDeleteMessage">This message could not be deleted. Please check your network connection and try again</string>
41+
<string name="error_description_failedToDeleteMessage">This message could not be deleted. Please check your network connection and try again.</string>
4242

4343
<string name="error_title_failedToRemoveUser">Something Went Wrong</string>
4444
<string name="error_description_failedToRemoveUser">This user could not be removed. Please check your network connection and try again</string>
4545

46+
<string name="error_title_failedToReportUserForMessage">Something Went Wrong</string>
47+
<string name="error_description_failedToReportUserForMessage">This user\'s message could not be reported. Please check your network connection and try again</string>
4648

4749
<string name="prompt_title_revealIdentity">Reveal your identity?</string>
4850
<string name="prompt_subtitle_revealIdentity">%1$s will be able to see that you are %2$s.</string>
@@ -73,11 +75,16 @@
7375
<string name="subtitle_removeUserFromRoom">They will be able to rejoin after waiting an hour, but will have to pay the cover charge again.</string>
7476
<string name="action_remove">Remove</string>
7577

78+
<string name="title_reportUserForMessage">Report %1$s?</string>
79+
<string name="subtitle_reportUserForMessage">This message will be forwarded to Flipchat. This contact will not be notified.</string>
80+
<string name="action_report">Report</string>
81+
7682
<string name="permissions_description_push_messages">Receive push notifications when people message you.</string>
7783

7884
<string name="action_copyMessage">Copy</string>
7985
<string name="action_deleteMessage">Delete</string>
8086
<string name="action_removeUser">Remove %1$s</string>
87+
<string name="action_reportUser">Report %1$s</string>
8188

8289
<string name="title_roomCardHostedBy">Hosted by %1$s</string>
8390
<string name="title_roomCardJoinCost">Cover Charge: ⬢ %1$d Kin</string>

services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/ChatApi.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import xyz.flipchat.services.domain.model.query.QueryOptions
1717
import xyz.flipchat.services.internal.annotations.ChatManagedChannel
1818
import xyz.flipchat.services.internal.network.extensions.toChatId
1919
import xyz.flipchat.services.internal.network.extensions.toIntentId
20+
import xyz.flipchat.services.internal.network.extensions.toMessageId
2021
import xyz.flipchat.services.internal.network.extensions.toPaymentAmount
2122
import xyz.flipchat.services.internal.network.extensions.toProto
2223
import xyz.flipchat.services.internal.network.extensions.toUserId
@@ -200,6 +201,23 @@ class ChatApi @Inject constructor(
200201
.flowOn(Dispatchers.IO)
201202
}
202203

204+
// ReportUser reports a user for a given message
205+
fun reportUser(
206+
owner: KeyPair,
207+
userId: ID,
208+
messageId: ID,
209+
): Flow<FlipchatService.ReportUserResponse> {
210+
val request = FlipchatService.ReportUserRequest.newBuilder()
211+
.setUserId(userId.toUserId())
212+
.setMessageId(messageId.toMessageId())
213+
.apply { setAuth(authenticate(owner)) }
214+
.build()
215+
216+
return api::reportUser
217+
.callAsCancellableFlow(request)
218+
.flowOn(Dispatchers.IO)
219+
}
220+
203221
// StreamChatEvents streams all chat events for the requesting user.
204222
//
205223
// Chat events will include any update to a chat, including:

services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/chat/ChatRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ interface ChatRepository {
3434

3535
// Self Defense Room Controls
3636
suspend fun removeUser(conversationId: ID, userId: ID): Result<Unit>
37+
suspend fun reportUserForMessage(userId: ID, messageId: ID): Result<Unit>
3738
}

services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/chat/RealChatRepository.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,13 @@ internal class RealChatRepository @Inject constructor(
206206
return service.removeUser(owner, conversationId, userId)
207207
.onFailure { ErrorUtils.handleError(it) }
208208
}
209+
210+
override suspend fun reportUserForMessage(
211+
userId: ID,
212+
messageId: ID
213+
): Result<Unit> {
214+
val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner"))
215+
return service.reportUser(owner, userId, messageId)
216+
.onFailure { ErrorUtils.handleError(it) }
217+
}
209218
}

services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/ChatService.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,38 @@ internal class ChatService @Inject constructor(
346346
}
347347
}
348348

349+
suspend fun reportUser(
350+
owner: KeyPair,
351+
userId: ID,
352+
messageId: ID
353+
): Result<Unit> {
354+
return try {
355+
networkOracle.managedRequest(api.reportUser(owner, userId, messageId))
356+
.map { response ->
357+
when (response.result) {
358+
FlipchatService.ReportUserResponse.Result.OK -> {
359+
Result.success(Unit)
360+
}
361+
362+
FlipchatService.ReportUserResponse.Result.UNRECOGNIZED -> {
363+
val error = ReportUserError.Unrecognized()
364+
Timber.e(t = error)
365+
Result.failure(error)
366+
}
367+
368+
else -> {
369+
val error = ReportUserError.Other()
370+
Timber.e(t = error)
371+
Result.failure(error)
372+
}
373+
}
374+
}.first()
375+
} catch (e: Exception) {
376+
val error = ReportUserError.Other(cause = e)
377+
Result.failure(error)
378+
}
379+
}
380+
349381
fun openChatStream(
350382
scope: CoroutineScope,
351383
owner: KeyPair,
@@ -507,4 +539,9 @@ internal class ChatService @Inject constructor(
507539
class Denied : RemoveUserError()
508540
data class Other(override val cause: Throwable? = null) : RemoveUserError()
509541
}
542+
543+
sealed class ReportUserError : Throwable() {
544+
class Unrecognized : RemoveUserError()
545+
data class Other(override val cause: Throwable? = null) : RemoveUserError()
546+
}
510547
}

services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/RoomController.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,13 @@ class RoomController @Inject constructor(
192192
}
193193
}
194194

195+
suspend fun reportUserForMessage(
196+
userId: ID,
197+
messageId: ID,
198+
): Result<Unit> {
199+
return chatRepository.reportUserForMessage(userId, messageId)
200+
}
201+
195202
suspend fun setCoverCharge(
196203
conversationId: ID,
197204
amount: KinAmount

ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessageTextContent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import androidx.compose.ui.draw.drawWithContent
2020
import androidx.compose.ui.graphics.Shape
2121
import androidx.compose.ui.platform.LocalDensity
2222
import androidx.compose.ui.text.TextStyle
23-
import androidx.compose.ui.text.font.FontWeight
2423
import androidx.compose.ui.text.rememberTextMeasurer
2524
import androidx.compose.ui.unit.dp
2625
import com.getcode.model.chat.MessageStatus
@@ -37,6 +36,7 @@ sealed interface MessageControlAction {
3736
data class Copy(override val onSelect: () -> Unit): MessageControlAction
3837
data class Delete(override val onSelect: () -> Unit): MessageControlAction
3938
data class RemoveUser(val name: String, override val onSelect: () -> Unit): MessageControlAction
39+
data class ReportUserForMessage(val name: String, override val onSelect: () -> Unit): MessageControlAction
4040
}
4141

4242
data class MessageControls(

0 commit comments

Comments
 (0)