mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
feat(chat): add bubbles chat UI mode with peer-color bubbles
Add a toggleable chat transcript style alongside the existing matrix transcript. Bubbles mode renders text messages as classic messenger bubbles: own messages on the right, peers on the left, each bubble washed with the author's stable identity-derived peer colour so the speaker stays identifiable without changing any theme, surface, or background colours. - ChatUiMode preference (Matrix / Bubbles, default Bubbles) persisted via ChatUiModeManager, mirroring ThemePreferenceManager - BubbleTextMessageLayout: rounded bubble with a subtle tail on the speaker's side, width capped at 80% so long messages wrap; sender labels, grouping, timestamps, mentions, links, long-press, and the existing spring entry/placement animations are unchanged - Delivery status for own private messages moves beneath the bubble in bubbles mode so it never overlaps the tail - Chat style picker in About -> Settings reusing the ThemeChip pattern
This commit is contained in:
parent
13f15ca1ef
commit
94d5fa3084
@ -48,6 +48,9 @@ class BitchatApplication : Application() {
|
||||
// Initialize theme preference
|
||||
ThemePreferenceManager.init(this)
|
||||
|
||||
// Initialize chat UI mode (matrix transcript vs bubbles)
|
||||
com.bitchat.android.ui.theme.ChatUiModeManager.init(this)
|
||||
|
||||
// Initialize debug preference manager (persists debug toggles)
|
||||
try { com.bitchat.android.ui.debug.DebugPreferenceManager.init(this) } catch (_: Exception) { }
|
||||
|
||||
|
||||
@ -418,6 +418,40 @@ fun AboutSheet(
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "chat_style") {
|
||||
Column {
|
||||
AboutSectionLabel(text = stringResource(R.string.about_section_chat_style))
|
||||
val chatUiMode by com.bitchat.android.ui.theme.ChatUiModeManager.modeFlow.collectAsState()
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ThemeChip(
|
||||
label = stringResource(R.string.chat_ui_matrix),
|
||||
selected = chatUiMode.isMatrix,
|
||||
onClick = { com.bitchat.android.ui.theme.ChatUiModeManager.set(context, com.bitchat.android.ui.theme.ChatUiMode.Matrix) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
ThemeChip(
|
||||
label = stringResource(R.string.chat_ui_bubbles),
|
||||
selected = chatUiMode.isBubbles,
|
||||
onClick = { com.bitchat.android.ui.theme.ChatUiModeManager.set(context, com.bitchat.android.ui.theme.ChatUiMode.Bubbles) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "language") {
|
||||
val selectedLanguageName = supportedLanguages
|
||||
.firstOrNull { it.languageTag == selectedLanguageTag }
|
||||
|
||||
@ -27,6 +27,7 @@ import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
@ -36,6 +37,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
@ -49,6 +51,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -67,6 +70,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@ -83,10 +87,12 @@ import com.bitchat.android.model.DeliveryStatus
|
||||
import com.bitchat.android.ui.media.FileMessageItem
|
||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
import com.bitchat.android.ui.theme.BitchatMotion
|
||||
import com.bitchat.android.ui.theme.ChatUiModeManager
|
||||
import com.bitchat.android.ui.theme.ChatVisualTokens
|
||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||
import com.bitchat.android.ui.theme.MessageBodyTextStyle
|
||||
import com.bitchat.android.ui.theme.MessageSenderTextStyle
|
||||
import com.bitchat.android.ui.theme.colorForPeer
|
||||
import kotlinx.coroutines.delay
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
@ -223,6 +229,10 @@ fun MessagesList(
|
||||
mentionPeerIdentities ?: buildMentionPeerIdentityMap(messages)
|
||||
}
|
||||
|
||||
// Collected once here so individual rows never subscribe to the preference flow; a mode
|
||||
// switch simply recomposes the list against the new layout.
|
||||
val bubbles by ChatUiModeManager.modeFlow.collectAsState()
|
||||
|
||||
// A fresh scroll position per conversation. Sharing one state meant a switch inherited the
|
||||
// previous channel's offset and then had to correct itself, which is what the jump was.
|
||||
//
|
||||
@ -352,6 +362,7 @@ fun MessagesList(
|
||||
meshService = meshService,
|
||||
mentionPeerIdentities = resolvedMentionPeerIdentities,
|
||||
showSender = !isGrouped,
|
||||
bubbles = bubbles.isBubbles,
|
||||
topSpacing = MessageGrouping.topSpacingFor(
|
||||
isGrouped = isGrouped,
|
||||
isFirstInList = originalIndex == 0
|
||||
@ -385,6 +396,7 @@ fun MessageItem(
|
||||
messages: List<BitchatMessage> = emptyList(),
|
||||
mentionPeerIdentities: Map<String, PeerIdentity> = emptyMap(),
|
||||
showSender: Boolean = true,
|
||||
bubbles: Boolean = false,
|
||||
topSpacing: Dp = 0.dp,
|
||||
onNicknameClick: ((String) -> Unit)? = null,
|
||||
onMessageLongPress: ((BitchatMessage) -> Unit)? = null,
|
||||
@ -407,8 +419,9 @@ fun MessageItem(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
// Provide a small end padding for own private messages so overlay doesn't cover text
|
||||
val endPad = if (message.isPrivate && message.sender == currentUserNickname) 16.dp else 0.dp
|
||||
// Provide a small end padding for own private messages so overlay doesn't cover text.
|
||||
// Bubble mode draws the status beneath the bubble instead, so no inset is needed.
|
||||
val endPad = if (!bubbles && message.isPrivate && message.sender == currentUserNickname) 16.dp else 0.dp
|
||||
// Create a custom layout that combines selectable text with clickable nickname areas
|
||||
MessageTextWithClickableNicknames(
|
||||
message = message,
|
||||
@ -419,6 +432,7 @@ fun MessageItem(
|
||||
colorScheme = colorScheme,
|
||||
timeFormatter = timeFormatter,
|
||||
showSender = showSender,
|
||||
bubbles = bubbles,
|
||||
onNicknameClick = onNicknameClick,
|
||||
onMessageLongPress = onMessageLongPress,
|
||||
onCancelTransfer = onCancelTransfer,
|
||||
@ -429,8 +443,9 @@ fun MessageItem(
|
||||
)
|
||||
}
|
||||
|
||||
// Delivery status for private messages (overlay, non-displacing)
|
||||
if (message.isPrivate && message.sender == currentUserNickname) {
|
||||
// Delivery status for private messages (overlay, non-displacing). Bubble mode aligns
|
||||
// own messages to the end edge where this overlay lives, so it renders below instead.
|
||||
if (!bubbles && message.isPrivate && message.sender == currentUserNickname) {
|
||||
message.deliveryStatus?.let { status ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -442,7 +457,21 @@ fun MessageItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Bubble mode: a small end-aligned marker beneath the bubble, clear of the tail.
|
||||
if (bubbles && message.isPrivate && message.sender == currentUserNickname) {
|
||||
message.deliveryStatus?.let { status ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 2.dp, end = 4.dp),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
DeliveryStatusIcon(status = status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Link previews removed; links are now highlighted inline and clickable within the message text
|
||||
}
|
||||
}
|
||||
@ -458,6 +487,7 @@ fun MessageItem(
|
||||
colorScheme: ColorScheme,
|
||||
timeFormatter: SimpleDateFormat,
|
||||
showSender: Boolean,
|
||||
bubbles: Boolean = false,
|
||||
onNicknameClick: ((String) -> Unit)?,
|
||||
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
||||
onCancelTransfer: ((BitchatMessage) -> Unit)?,
|
||||
@ -624,6 +654,7 @@ fun MessageItem(
|
||||
meshService = meshService,
|
||||
colorScheme = colorScheme,
|
||||
timeFormatter = timeFormatter,
|
||||
bubbles = bubbles,
|
||||
onNicknameClick = onNicknameClick,
|
||||
onMessageLongPress = onMessageLongPress,
|
||||
modifier = modifier
|
||||
@ -668,6 +699,7 @@ fun MessageItem(
|
||||
colorScheme = colorScheme,
|
||||
timeFormatter = timeFormatter,
|
||||
showSender = showSender,
|
||||
bubbles = bubbles,
|
||||
onNicknameClick = onNicknameClick,
|
||||
onMessageLongPress = onMessageLongPress,
|
||||
modifier = modifier,
|
||||
@ -687,6 +719,7 @@ internal fun TextMessageLayout(
|
||||
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
showSender: Boolean = true,
|
||||
bubbles: Boolean = false,
|
||||
bodyContent: String = message.content,
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
@ -731,6 +764,20 @@ internal fun TextMessageLayout(
|
||||
onMessageLongPress?.invoke(message)
|
||||
}
|
||||
|
||||
if (bubbles) {
|
||||
BubbleTextMessageLayout(
|
||||
message = message,
|
||||
senderText = senderText,
|
||||
bodyText = bodyText,
|
||||
isSelf = isSelf,
|
||||
showSender = showSender,
|
||||
onNicknameClick = onNicknameClick,
|
||||
onLongPress = handleLongPress,
|
||||
modifier = modifier,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(MessageGrouping.SENDER_TO_BODY_SPACING),
|
||||
@ -788,6 +835,129 @@ internal fun TextMessageLayout(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classic messenger rendering of a text message: a rounded bubble that hugs its content, own
|
||||
* messages on the right and everyone else on the left, with the corner on the speaker's side
|
||||
* tightened into a subtle tail.
|
||||
*
|
||||
* The bubble is washed with the author's stable peer colour — the same identity-derived colour
|
||||
* the `@name` label and mention chips already use — so the speaker stays identifiable at a
|
||||
* glance without touching any surface, background, or theme colour. Body text keeps the
|
||||
* standard `onSurface` tone; only the bubble shell carries the identity.
|
||||
*/
|
||||
@Composable
|
||||
private fun BubbleTextMessageLayout(
|
||||
message: BitchatMessage,
|
||||
senderText: AnnotatedString,
|
||||
bodyText: AnnotatedString,
|
||||
isSelf: Boolean,
|
||||
showSender: Boolean,
|
||||
onNicknameClick: ((String) -> Unit)?,
|
||||
onLongPress: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val authorColor = remember(message, isSelf, palette) {
|
||||
if (isSelf) palette.accentOrange else colorForPeer(peerIdentityForMessage(message), palette)
|
||||
}
|
||||
|
||||
val corner = ChatVisualTokens.BubbleCornerRadius
|
||||
val tail = ChatVisualTokens.BubbleTailRadius
|
||||
val bubbleShape = if (isSelf) {
|
||||
RoundedCornerShape(topStart = corner, topEnd = corner, bottomEnd = tail, bottomStart = corner)
|
||||
} else {
|
||||
RoundedCornerShape(topStart = corner, topEnd = corner, bottomEnd = corner, bottomStart = tail)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(MessageGrouping.SENDER_TO_BODY_SPACING),
|
||||
horizontalAlignment = if (isSelf) Alignment.End else Alignment.Start,
|
||||
) {
|
||||
if (showSender) {
|
||||
AnnotatedClickableText(
|
||||
text = senderText,
|
||||
annotationTags = listOf("nickname_click"),
|
||||
onAnnotationClick = { tag, item ->
|
||||
if (tag == "nickname_click" && !isSelf && onNicknameClick != null) {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
onNicknameClick.invoke(item)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
onLongPress = onLongPress,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = MessageGrouping.SENDER_TOP_PADDING,
|
||||
// Nudge the label off the bubble's rounded edge so it lines up with the text.
|
||||
start = if (isSelf) 0.dp else ChatVisualTokens.BubblePaddingHorizontal,
|
||||
end = if (isSelf) ChatVisualTokens.BubblePaddingHorizontal else 0.dp,
|
||||
),
|
||||
fontFamily = BitchatFontFamily,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MessageSenderTextStyle,
|
||||
)
|
||||
}
|
||||
|
||||
// Cap the bubble at a fraction of the row so long messages wrap instead of touching the
|
||||
// opposite edge, while short ones hug their content.
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
||||
val maxBubbleWidth = maxWidth * ChatVisualTokens.BubbleMaxWidthFraction
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(if (isSelf) Alignment.CenterEnd else Alignment.CenterStart)
|
||||
.widthIn(max = maxBubbleWidth)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = authorColor.copy(alpha = ChatVisualTokens.BubbleBorderAlpha),
|
||||
shape = bubbleShape
|
||||
)
|
||||
.background(
|
||||
color = authorColor.copy(alpha = ChatVisualTokens.BubbleBackgroundAlpha),
|
||||
shape = bubbleShape
|
||||
)
|
||||
.padding(
|
||||
horizontal = ChatVisualTokens.BubblePaddingHorizontal,
|
||||
vertical = ChatVisualTokens.BubblePaddingVertical,
|
||||
)
|
||||
) {
|
||||
AnnotatedClickableText(
|
||||
text = bodyText,
|
||||
annotationTags = listOf("geohash_click", "url_click"),
|
||||
onAnnotationClick = { tag, item ->
|
||||
when (tag) {
|
||||
"geohash_click" -> {
|
||||
navigateToGeohash(context, item)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
true
|
||||
}
|
||||
|
||||
"url_click" -> {
|
||||
openMessageUrl(context, item)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
},
|
||||
onLongPress = onLongPress,
|
||||
fontFamily = BitchatFontFamily,
|
||||
softWrap = true,
|
||||
overflow = TextOverflow.Visible,
|
||||
style = MessageBodyTextStyle.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun CashuMessageContent(
|
||||
@ -797,6 +967,7 @@ private fun CashuMessageContent(
|
||||
meshService: MeshService,
|
||||
colorScheme: ColorScheme,
|
||||
timeFormatter: SimpleDateFormat,
|
||||
bubbles: Boolean = false,
|
||||
onNicknameClick: ((String) -> Unit)?,
|
||||
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
||||
modifier: Modifier = Modifier
|
||||
@ -816,6 +987,7 @@ private fun CashuMessageContent(
|
||||
meshService = meshService,
|
||||
colorScheme = colorScheme,
|
||||
timeFormatter = timeFormatter,
|
||||
bubbles = bubbles,
|
||||
onNicknameClick = onNicknameClick,
|
||||
onMessageLongPress = onMessageLongPress,
|
||||
bodyContent = remainingText,
|
||||
|
||||
49
app/src/main/java/com/bitchat/android/ui/theme/ChatUiMode.kt
Normal file
49
app/src/main/java/com/bitchat/android/ui/theme/ChatUiMode.kt
Normal file
@ -0,0 +1,49 @@
|
||||
package com.bitchat.android.ui.theme
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Chat transcript presentation.
|
||||
*
|
||||
* [Matrix] is the established terminal-style transcript: a flat, left-aligned monochrome
|
||||
* stream where colour is reserved for `@names` and links.
|
||||
*
|
||||
* [Bubbles] is the classic messenger layout: messages hug their content inside rounded
|
||||
* bubbles, own messages on the right and everyone else on the left. Each bubble is tinted
|
||||
* with its author's stable peer colour, so the speaker stays identifiable without reading
|
||||
* the name. Colours, surfaces, and typography are untouched — only the message layout
|
||||
* changes.
|
||||
*/
|
||||
enum class ChatUiMode {
|
||||
Matrix,
|
||||
Bubbles;
|
||||
|
||||
val isMatrix: Boolean get() = this == Matrix
|
||||
val isBubbles: Boolean get() = this == Bubbles
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple SharedPreferences-backed manager for the chat UI mode with a StateFlow.
|
||||
* Mirrors [ThemePreferenceManager].
|
||||
*/
|
||||
object ChatUiModeManager {
|
||||
private const val PREFS_NAME = "bitchat_settings"
|
||||
private const val KEY_CHAT_UI_MODE = "chat_ui_mode"
|
||||
|
||||
private val _modeFlow = MutableStateFlow(ChatUiMode.Bubbles)
|
||||
val modeFlow: StateFlow<ChatUiMode> = _modeFlow
|
||||
|
||||
fun init(context: Context) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val saved = prefs.getString(KEY_CHAT_UI_MODE, ChatUiMode.Bubbles.name)
|
||||
_modeFlow.value = runCatching { ChatUiMode.valueOf(saved!!) }.getOrDefault(ChatUiMode.Bubbles)
|
||||
}
|
||||
|
||||
fun set(context: Context, mode: ChatUiMode) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit().putString(KEY_CHAT_UI_MODE, mode.name).apply()
|
||||
_modeFlow.value = mode
|
||||
}
|
||||
}
|
||||
@ -37,6 +37,30 @@ internal object ChatVisualTokens {
|
||||
val SenderTopPadding: Dp = 8.dp
|
||||
val SenderToBodySpacing: Dp = 4.dp
|
||||
|
||||
// MARK: - Bubble geometry (ChatUiMode.Bubbles)
|
||||
|
||||
/** Rounded corner on the three "free" corners of a message bubble. */
|
||||
val BubbleCornerRadius: Dp = 16.dp
|
||||
|
||||
/** Tightened corner on the speaker's own side, giving the bubble a subtle tail. */
|
||||
val BubbleTailRadius: Dp = 4.dp
|
||||
|
||||
/** Padding inside a bubble, around the text. */
|
||||
val BubblePaddingHorizontal: Dp = 12.dp
|
||||
val BubblePaddingVertical: Dp = 8.dp
|
||||
|
||||
/** A bubble never grows past this fraction of the list width, so long lines still wrap. */
|
||||
const val BubbleMaxWidthFraction: Float = 0.80f
|
||||
|
||||
/**
|
||||
* Author-colour wash inside a bubble. Matches the mention-chip treatment so a tinted
|
||||
* bubble stays legible on both the near-black and near-white chat surfaces.
|
||||
*/
|
||||
const val BubbleBackgroundAlpha: Float = 0.18f
|
||||
|
||||
/** Author-colour hairline around a bubble; stronger than the fill so the shape reads. */
|
||||
const val BubbleBorderAlpha: Float = 0.38f
|
||||
|
||||
const val SenderSuffixAlpha: Float = 0.60f
|
||||
const val HighlightAlpha: Float = 0.20f
|
||||
const val MutedTextAlpha: Float = 0.50f
|
||||
|
||||
@ -171,6 +171,7 @@
|
||||
<!-- About sheet: section labels -->
|
||||
<string name="about_section_about">About</string>
|
||||
<string name="about_section_theme">Theme</string>
|
||||
<string name="about_section_chat_style">Chat style</string>
|
||||
<string name="about_section_settings">Settings</string>
|
||||
|
||||
<!-- About sheet: feature list -->
|
||||
@ -196,6 +197,8 @@
|
||||
<string name="about_system">System</string>
|
||||
<string name="about_light">Light</string>
|
||||
<string name="about_dark">Dark</string>
|
||||
<string name="chat_ui_matrix">Matrix</string>
|
||||
<string name="chat_ui_bubbles">Bubbles</string>
|
||||
<string name="about_pow">Proof of Work</string>
|
||||
<string name="about_pow_off">PoW Off</string>
|
||||
<string name="about_pow_on">PoW On</string>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user