From 8da2f079b2424130f708df28c58b6f7fd9d6dd18 Mon Sep 17 00:00:00 2001 From: ale Date: Sat, 28 Feb 2026 02:21:37 +0100 Subject: [PATCH] fix lint Signed-off-by: ale --- app/build.gradle.kts | 10 +- app/lint.xml | 35 ++++++ app/src/main/AndroidManifest.xml | 2 + .../alejabber/data/local/dao/ContactDao.kt | 3 + .../data/repository/ContactRepository.kt | 56 +++++++-- .../alejabber/media/HttpUploadManager.kt | 2 +- .../alejabber/ui/chat/ChatScreen.kt | 111 ++++++++---------- .../ui/components/AvatarComponents.kt | 108 +++++++++++++---- .../alejabber/ui/contacts/ContactsScreen.kt | 38 ++---- .../alejabber/ui/rooms/RoomsScreen.kt | 2 +- .../res/drawable/ic_launcher_background.xml | 18 --- .../res/mipmap-anydpi-v26/ic_launcher.xml | 1 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 1 + app/src/main/res/values-es/strings.xml | 74 ++---------- app/src/main/res/values-zh/strings.xml | 96 +++------------ app/src/main/res/values/colors.xml | 9 +- .../res/values/ic_launcher_background.xml | 4 - app/src/main/res/values/strings.xml | 63 +--------- gradle/libs.versions.toml | 8 +- 19 files changed, 273 insertions(+), 368 deletions(-) create mode 100644 app/lint.xml delete mode 100644 app/src/main/res/drawable/ic_launcher_background.xml delete mode 100644 app/src/main/res/values/ic_launcher_background.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 997bee8..6e7d983 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,6 +31,12 @@ android { buildFeatures { compose = true } + lint { + lintConfig = file("lint.xml") + // Warnings do not abort the CI build; only errors do. + warningsAsErrors = false + abortOnError = true + } packaging { resources { excludes += setOf( @@ -53,8 +59,8 @@ android { } configurations.all { resolutionStrategy { - force("org.bouncycastle:bcprov-jdk18on:1.78.1") - force("org.bouncycastle:bcpg-jdk18on:1.78.1") + force("org.bouncycastle:bcprov-jdk18on:1.83") + force("org.bouncycastle:bcpg-jdk18on:1.83") } // Exclude old BouncyCastle and duplicate xpp3 versions exclude(group = "org.bouncycastle", module = "bcprov-jdk15on") diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 0000000..6ddbe2d --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d41d0d..ec47bc3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,8 @@ + + > = contactDao.getContactsByAccount(accountId) @@ -34,11 +37,8 @@ class ContactRepository @Inject constructor( val accountPresence = presenceMap[accountId] ?: emptyMap() entities.map { entity -> val livePresence = accountPresence[entity.jid] - if (livePresence != null) { - entity.toDomain().copy(presence = livePresence) - } else { - entity.toDomain() - } + if (livePresence != null) entity.toDomain().copy(presence = livePresence) + else entity.toDomain() } } @@ -54,9 +54,7 @@ class ContactRepository @Inject constructor( roster.createItemAndRequestSubscription( jid, contact.nickname.ifBlank { contact.jid }, null ) - } catch (e: Exception) { - // Proceed to save locally even if the server call fails - } + } catch (e: Exception) { /* save locally anyway */ } } return contactDao.insertContact(contact.toEntity()) } @@ -73,9 +71,18 @@ class ContactRepository @Inject constructor( contactDao.deleteContact(accountId, jid) } + /** + * Syncs the server roster to the local DB and then fetches each contact's + * vCard avatar in the background. Avatar bytes are stored as a + * `data:image/png;base64,…` URI so Coil can display them with no extra + * network request. + */ suspend fun syncRoster(accountId: Long) { - val entries = xmppManager.getRosterEntries(accountId) - val contacts = entries.map { entry -> + val connection = xmppManager.getConnection(accountId) ?: return + val entries = xmppManager.getRosterEntries(accountId) + + // 1. Upsert the basic roster entries + val entities = entries.map { entry -> Contact( accountId = accountId, jid = entry.jid.asBareJid().toString(), @@ -83,7 +90,30 @@ class ContactRepository @Inject constructor( groups = entry.groups.map { it.name } ).toEntity() } - if (contacts.isNotEmpty()) contactDao.insertContacts(contacts) + if (entities.isNotEmpty()) contactDao.insertContacts(entities) + + // 2. Fetch vCard avatars for each contact (best-effort, non-blocking errors) + if (!connection.isConnected || !connection.isAuthenticated) return + val vcardManager = VCardManager.getInstanceFor(connection) + entries.forEach { entry -> + val bareJid = entry.jid.asBareJid().toString() + try { + val vcard = vcardManager.loadVCard( + JidCreate.entityBareFrom(bareJid) + ) + val avatarBytes = vcard?.avatar + if (avatarBytes != null && avatarBytes.isNotEmpty()) { + val mime = vcard.avatarMimeType?.ifBlank { "image/png" } ?: "image/png" + val b64 = Base64.encodeToString(avatarBytes, Base64.NO_WRAP) + val dataUri = "data:$mime;base64,$b64" + contactDao.updateAvatarUrl(accountId, bareJid, dataUri) + Log.d(TAG, "Avatar loaded for $bareJid (${avatarBytes.size} bytes)") + } + } catch (e: Exception) { + // vCard not found or server error — keep whatever was stored before + Log.d(TAG, "No vCard avatar for $bareJid: ${e.message}") + } + } } suspend fun updatePresence( diff --git a/app/src/main/java/com/manalejandro/alejabber/media/HttpUploadManager.kt b/app/src/main/java/com/manalejandro/alejabber/media/HttpUploadManager.kt index d28c5e4..357ca50 100644 --- a/app/src/main/java/com/manalejandro/alejabber/media/HttpUploadManager.kt +++ b/app/src/main/java/com/manalejandro/alejabber/media/HttpUploadManager.kt @@ -18,7 +18,7 @@ import javax.inject.Singleton @Singleton class HttpUploadManager @Inject constructor( - @ApplicationContext private val context: Context, + @param:ApplicationContext private val context: Context, private val xmppManager: XmppConnectionManager, private val okHttpClient: OkHttpClient ) { diff --git a/app/src/main/java/com/manalejandro/alejabber/ui/chat/ChatScreen.kt b/app/src/main/java/com/manalejandro/alejabber/ui/chat/ChatScreen.kt index a7aa57c..8c392b0 100644 --- a/app/src/main/java/com/manalejandro/alejabber/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/manalejandro/alejabber/ui/chat/ChatScreen.kt @@ -28,23 +28,27 @@ import androidx.compose.runtime.* import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.launch import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.ClipboardManager -import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalUriHandler 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.LinkAnnotation import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -76,7 +80,7 @@ fun ChatScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() val listState = rememberLazyListState() val micPermission = rememberPermissionState(Manifest.permission.RECORD_AUDIO) - val clipboardManager = LocalClipboardManager.current + val clipboard = LocalClipboard.current // Message selected via long-press → shows the action bottom sheet var selectedMessage by remember { mutableStateOf(null) } @@ -226,8 +230,8 @@ fun ChatScreen( // ── Message action bottom sheet ─────────────────────────────────────── selectedMessage?.let { msg -> MessageActionsSheet( - message = msg, - clipboardManager = clipboardManager, + message = msg, + clipboard = clipboard, onDelete = { selectedMessage = null messageToDelete = msg @@ -281,24 +285,28 @@ private val URL_PATTERN: Pattern = Pattern.compile( "(https?://|www\\.)[\\w\\-]+(\\.[\\w\\-]+)+([\\w.,@?^=%&:/~+#\\-_]*[\\w@?^=%&/~+#\\-_])?" ) -/** Converts a plain string into an [AnnotatedString] with clickable URL spans. */ +/** Converts a plain string into an [AnnotatedString] with clickable URL spans (modern API). */ fun buildMessageText(text: String, linkColor: Color): AnnotatedString = buildAnnotatedString { val matcher = URL_PATTERN.matcher(text) var last = 0 while (matcher.find()) { - // Append plain text before the URL append(text.substring(last, matcher.start())) val url = matcher.group() val fullUrl = if (url.startsWith("http")) url else "https://$url" - // Append the URL with a distinct style and a string annotation - pushStringAnnotation(tag = "URL", annotation = fullUrl) - withStyle(SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)) { - append(url) - } - pop() + // Use LinkAnnotation.Url — opens the browser automatically on click. + withLink( + LinkAnnotation.Url( + url = fullUrl, + styles = TextLinkStyles( + style = SpanStyle( + color = linkColor, + textDecoration = TextDecoration.Underline + ) + ) + ) + ) { append(url) } last = matcher.end() } - // Append remaining plain text append(text.substring(last)) } @@ -344,29 +352,16 @@ fun MessageBubble(message: Message, onLongPress: () -> Unit) { .padding(horizontal = 14.dp, vertical = 8.dp) ) { when (message.mediaType) { - MediaType.TEXT, MediaType.LINK, null -> { - // Build annotated text with clickable URLs + MediaType.TEXT, MediaType.LINK -> { + // Build annotated text with clickable URLs via LinkAnnotation. + // Text handles link clicks automatically — no ClickableText needed. val annotated = remember(message.body) { buildMessageText(message.body, linkColor) } - val hasLinks = annotated.getStringAnnotations("URL", 0, annotated.length).isNotEmpty() - if (hasLinks) { - // ClickableText for messages that contain URLs - androidx.compose.foundation.text.ClickableText( - text = annotated, - style = MaterialTheme.typography.bodyMedium.copy(color = textColor), - onClick = { offset -> - annotated.getStringAnnotations("URL", offset, offset) - .firstOrNull()?.let { uriHandler.openUri(it.item) } - } - ) - } else { - Text( - text = message.body, - color = textColor, - style = MaterialTheme.typography.bodyMedium - ) - } + Text( + text = annotated, + style = MaterialTheme.typography.bodyMedium.copy(color = textColor) + ) } MediaType.IMAGE -> { AsyncImage( @@ -457,20 +452,20 @@ fun MessageBubble(message: Message, onLongPress: () -> Unit) { @Composable fun MessageActionsSheet( message: Message, - clipboardManager: ClipboardManager, + clipboard: androidx.compose.ui.platform.Clipboard, onDelete: () -> Unit, onDismiss: () -> Unit ) { val uriHandler = LocalUriHandler.current val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val hasUrl = URL_PATTERN.matcher(message.body).find() + val scope = rememberCoroutineScope() ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState ) { Column(modifier = Modifier.padding(bottom = 24.dp)) { - // Header preview Text( text = message.body.take(120) + if (message.body.length > 120) "…" else "", style = MaterialTheme.typography.bodySmall, @@ -479,17 +474,21 @@ fun MessageActionsSheet( ) HorizontalDivider() - // ── Copy ────────────────────────────────────────────────────── ListItem( headlineContent = { Text("Copy text") }, leadingContent = { Icon(Icons.Default.ContentCopy, null) }, modifier = Modifier.clickable { - clipboardManager.setText(AnnotatedString(message.body)) + scope.launch { + clipboard.setClipEntry( + androidx.compose.ui.platform.ClipEntry( + android.content.ClipData.newPlainText("message", message.body) + ) + ) + } onDismiss() } ) - // ── Open URL ───────────────────────────────────────────────── if (hasUrl) { val matcher = URL_PATTERN.matcher(message.body) if (matcher.find()) { @@ -498,25 +497,23 @@ fun MessageActionsSheet( ListItem( headlineContent = { Text("Open link") }, supportingContent = { - Text( - fullUrl, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.primary, - maxLines = 1 - ) + Text(fullUrl, style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, maxLines = 1) }, leadingContent = { Icon(Icons.Default.OpenInBrowser, null) }, - modifier = Modifier.clickable { - uriHandler.openUri(fullUrl) - onDismiss() - } + modifier = Modifier.clickable { uriHandler.openUri(fullUrl); onDismiss() } ) - // Copy link separately ListItem( headlineContent = { Text("Copy link") }, leadingContent = { Icon(Icons.Default.Link, null) }, modifier = Modifier.clickable { - clipboardManager.setText(AnnotatedString(fullUrl)) + scope.launch { + clipboard.setClipEntry( + androidx.compose.ui.platform.ClipEntry( + android.content.ClipData.newPlainText("link", fullUrl) + ) + ) + } onDismiss() } ) @@ -525,17 +522,9 @@ fun MessageActionsSheet( HorizontalDivider() - // ── Delete ──────────────────────────────────────────────────── ListItem( - headlineContent = { - Text("Delete message", color = MaterialTheme.colorScheme.error) - }, - leadingContent = { - Icon( - Icons.Default.DeleteForever, null, - tint = MaterialTheme.colorScheme.error - ) - }, + headlineContent = { Text("Delete message", color = MaterialTheme.colorScheme.error) }, + leadingContent = { Icon(Icons.Default.DeleteForever, null, tint = MaterialTheme.colorScheme.error) }, modifier = Modifier.clickable { onDelete() } ) diff --git a/app/src/main/java/com/manalejandro/alejabber/ui/components/AvatarComponents.kt b/app/src/main/java/com/manalejandro/alejabber/ui/components/AvatarComponents.kt index 9a1a08b..32a9c7c 100644 --- a/app/src/main/java/com/manalejandro/alejabber/ui/components/AvatarComponents.kt +++ b/app/src/main/java/com/manalejandro/alejabber/ui/components/AvatarComponents.kt @@ -1,16 +1,20 @@ package com.manalejandro.alejabber.ui.components +import android.graphics.BitmapFactory +import android.util.Base64 +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -35,26 +39,82 @@ fun AvatarWithStatus( contentDescription: String = "" ) { Box(modifier = modifier) { - if (!avatarUrl.isNullOrBlank()) { - AsyncImage( - model = avatarUrl, - contentDescription = contentDescription, - contentScale = ContentScale.Crop, - modifier = Modifier - .size(size) - .clip(CircleShape) - ) - } else { - InitialsAvatar(name = name, size = size, contentDescription = contentDescription) + when { + // ── data URI (vCard Base64 avatar) ──────────────────────────── + avatarUrl != null && avatarUrl.startsWith("data:") -> { + DataUriAvatar( + dataUri = avatarUrl, + size = size, + contentDescription = contentDescription + ) + } + // ── Regular http/https URL ──────────────────────────────────── + !avatarUrl.isNullOrBlank() -> { + AsyncImage( + model = avatarUrl, + contentDescription = contentDescription, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(size) + .clip(CircleShape) + ) + } + // ── No avatar — show initials ───────────────────────────────── + else -> { + InitialsAvatar( + name = name, + size = size, + contentDescription = contentDescription + ) + } } - // Presence dot + // Presence dot (bottom-right) Box( modifier = Modifier .align(Alignment.BottomEnd) .size(size * 0.27f) .clip(CircleShape) .background(presence.toColor()) - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0f)) + ) + } +} + +/** + * Decodes a `data:image/...;base64,...` URI and renders it as a circular avatar. + * Uses [remember] to avoid re-decoding on every recomposition. + */ +@Composable +private fun DataUriAvatar( + dataUri: String, + size: Dp, + contentDescription: String +) { + val bitmap = remember(dataUri) { + runCatching { + // Strip the "data:image/...;base64," prefix + val commaIndex = dataUri.indexOf(',') + if (commaIndex < 0) return@runCatching null + val base64 = dataUri.substring(commaIndex + 1) + val bytes = Base64.decode(base64, Base64.DEFAULT) + BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + }.getOrNull() + } + + if (bitmap != null) { + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = contentDescription, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(size) + .clip(CircleShape) + ) + } else { + // Fallback to initials if decoding failed + InitialsAvatar( + name = contentDescription, + size = size, + contentDescription = contentDescription ) } } @@ -80,26 +140,26 @@ fun InitialsAvatar( .semantics { this.contentDescription = contentDescription } ) { Text( - text = initials, - color = Color.White, - fontSize = (size.value * 0.38f).sp, + text = initials, + color = Color.White, + fontSize = (size.value * 0.38f).sp, fontWeight = FontWeight.Bold ) } } fun PresenceStatus.toColor(): Color = when (this) { - PresenceStatus.ONLINE -> StatusOnline + PresenceStatus.ONLINE -> StatusOnline PresenceStatus.AWAY, PresenceStatus.XA -> StatusAway - PresenceStatus.DND -> StatusDnd - PresenceStatus.OFFLINE -> StatusOffline + PresenceStatus.DND -> StatusDnd + PresenceStatus.OFFLINE -> StatusOffline } fun PresenceStatus.toLabel(): String = when (this) { - PresenceStatus.ONLINE -> "Online" - PresenceStatus.AWAY -> "Away" - PresenceStatus.XA -> "Extended Away" - PresenceStatus.DND -> "Do Not Disturb" + PresenceStatus.ONLINE -> "Online" + PresenceStatus.AWAY -> "Away" + PresenceStatus.XA -> "Extended Away" + PresenceStatus.DND -> "Do Not Disturb" PresenceStatus.OFFLINE -> "Offline" } diff --git a/app/src/main/java/com/manalejandro/alejabber/ui/contacts/ContactsScreen.kt b/app/src/main/java/com/manalejandro/alejabber/ui/contacts/ContactsScreen.kt index 91d71a2..6787ce9 100644 --- a/app/src/main/java/com/manalejandro/alejabber/ui/contacts/ContactsScreen.kt +++ b/app/src/main/java/com/manalejandro/alejabber/ui/contacts/ContactsScreen.kt @@ -135,14 +135,11 @@ fun ContactsScreen( modifier = Modifier.align(Alignment.Center) ) } - else -> { - ContactList( - contacts = uiState.filteredContacts, - onContactClick = { onNavigateToChat(accountId, it.jid) }, - onContactLongPress = { detailContact = it }, - onRemoveContact = { removeTarget = it } - ) - } + else -> ContactList( + contacts = uiState.filteredContacts, + onContactClick = { onNavigateToChat(accountId, it.jid) }, + onContactLongPress = { detailContact = it } + ) } } } @@ -320,8 +317,7 @@ fun SearchBar( fun ContactList( contacts: List, onContactClick: (Contact) -> Unit, - onContactLongPress: (Contact) -> Unit, - onRemoveContact: (Contact) -> Unit + onContactLongPress: (Contact) -> Unit ) { val presenceOrder = listOf( PresenceStatus.ONLINE, PresenceStatus.AWAY, PresenceStatus.DND, @@ -351,10 +347,9 @@ fun ContactList( } items(group, key = { "${presence.name}_${it.jid}" }) { contact -> ContactItem( - contact = contact, - onClick = { onContactClick(contact) }, - onLongPress = { onContactLongPress(contact) }, - onRemove = { onRemoveContact(contact) } + contact = contact, + onClick = { onContactClick(contact) }, + onLongPress = { onContactLongPress(contact) } ) } } @@ -367,8 +362,7 @@ fun ContactList( fun ContactItem( contact: Contact, onClick: () -> Unit, - onLongPress: () -> Unit, - onRemove: () -> Unit + onLongPress: () -> Unit ) { val displayName = contact.nickname.ifBlank { contact.jid } @@ -389,14 +383,6 @@ fun ContactItem( contentDescription = stringResource(R.string.cd_avatar, displayName) ) }, - trailingContent = { - IconButton(onClick = onRemove) { - Icon( - Icons.Default.PersonRemove, null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, modifier = Modifier .fillMaxWidth() .combinedClickable(onClick = onClick, onLongClick = onLongPress) @@ -444,9 +430,9 @@ fun AddContactDialog( fun EmptyState( icon: androidx.compose.ui.graphics.vector.ImageVector, message: String, + modifier: Modifier = Modifier, actionLabel: String? = null, - onAction: (() -> Unit)? = null, - modifier: Modifier = Modifier + onAction: (() -> Unit)? = null ) { Column( modifier = modifier.padding(32.dp), diff --git a/app/src/main/java/com/manalejandro/alejabber/ui/rooms/RoomsScreen.kt b/app/src/main/java/com/manalejandro/alejabber/ui/rooms/RoomsScreen.kt index 33ee2a8..3781e95 100644 --- a/app/src/main/java/com/manalejandro/alejabber/ui/rooms/RoomsScreen.kt +++ b/app/src/main/java/com/manalejandro/alejabber/ui/rooms/RoomsScreen.kt @@ -222,7 +222,7 @@ fun JoinRoomDialog( onJoin: (Long, String, String, String) -> Unit ) { var selectedAccountId by remember { - mutableStateOf(connectedAccounts.firstOrNull()?.id ?: 0L) + mutableLongStateOf(connectedAccounts.firstOrNull()?.id ?: 0L) } var roomJid by remember { mutableStateOf("") } var nickname by remember { mutableStateOf("") } diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index c6c3ea1..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd..ef49c99 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd..ef49c99 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c93af9c..eb96770 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,8 +1,8 @@ + AleJabber - Contactos Salas Cuentas Ajustes @@ -17,12 +17,6 @@ Servidor Puerto Usar TLS - Conectado - Desconectado - Conectando… - Error de conexión - Conectar - Desconectar No hay cuentas configuradas.\nPulsa + para añadir una. ¿Eliminar la cuenta %1$s? usuario@ejemplo.com @@ -32,42 +26,27 @@ Contactos Buscar contactos… - Sin contactos.\nAñade personas con su ID de Jabber. + Aún no hay contactos.\nAñade personas con su Jabber ID. Añadir contacto - ID de Jabber (JID) + Jabber ID (JID) Apodo - Eliminar contacto - Bloquear contacto - En línea - Ausente - No molestar - Desconectado Salas - Buscar salas… - No te has unido a ninguna sala. + Aún no te has unido a ninguna sala. Unirse a sala JID de la sala Tu apodo - Contraseña (opcional) + Contraseña de la sala Salir de la sala - Participantes - Tema - Explorar salas Escribe un mensaje… Enviar Adjuntar archivo Grabar audio - Detener grabación Enviar audio Cancelar audio - Sin cifrado - OTR - OMEMO - OpenPGP Seleccionar cifrado Entregado Leído @@ -75,17 +54,9 @@ Error al enviar %1$s está escribiendo… Imagen - Vídeo Audio Archivo - Subiendo… - Descargar - Sin mensajes aún.\n¡Di hola! - Sesión OTR iniciada. La conversación está cifrada. - Sesión OTR finalizada. - Advertencia: Huella OTR no verificada. - OMEMO: Todos los dispositivos son de confianza. - OMEMO: Dispositivos no verificados detectados. + Aún no hay mensajes.\n¡Di hola! Ajustes @@ -95,62 +66,31 @@ Claro Oscuro Idioma - English - Español - 中文 Notificaciones Notificaciones de mensajes Vibrar Sonido Cifrado - Dispositivos OMEMO - Claves OpenPGP - Huellas OTR Cifrado por defecto Acerca de Versión - Aceptar Cancelar Guardar Eliminar - Confirmar - Error - Cargando… - Reintentar Cerrar - Buscar - Limpiar Atrás Más opciones - - Permiso de micrófono - AleJabber necesita acceso al micrófono para grabar mensajes de audio. - Permiso de almacenamiento - AleJabber necesita acceso al almacenamiento para enviar y recibir archivos. - Permiso de cámara - AleJabber necesita acceso a la cámara para tomar fotos. - Permiso denegado. Por favor, concédelo en Ajustes. - Abrir Ajustes - Mensajes - Mensajes de chat entrantes + Mensajes entrantes Servicio XMPP Conexión XMPP en segundo plano AleJabber está conectado - Nuevo mensaje de %1$s Avatar de %1$s - Estado: %1$s Cifrado: %1$s - Enviar mensaje - Adjuntar archivo - Grabar mensaje de audio - Mensaje entregado - Mensaje leído - diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index e21b518..7e0a4d1 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,14 +1,14 @@ + AleJabber - 联系人 - 聊天室 + 房间 账号 设置 - 账号管理 + 账号 添加账号 编辑账号 删除账号 @@ -17,13 +17,7 @@ 服务器 端口 使用 TLS - 已连接 - 已断开 - 连接中… - 连接错误 - 连接 - 断开连接 - 尚未配置账号。\n点击 + 添加账号。 + 没有已配置的账号。\n点击 + 添加一个。 删除账号 %1$s? user@example.com 资源 @@ -32,42 +26,27 @@ 联系人 搜索联系人… - 暂无联系人。\n通过 Jabber ID 添加好友。 + 还没有联系人。\n使用 Jabber ID 添加朋友。 添加联系人 Jabber ID (JID) 昵称 - 删除联系人 - 屏蔽联系人 - 在线 - 离开 - 请勿打扰 - 离线 - - 聊天室 - 搜索聊天室… - 尚未加入任何聊天室。 - 加入聊天室 - 聊天室 JID - 您的昵称 - 聊天室密码(可选) - 退出聊天室 - 参与者 - 主题 - 浏览聊天室 + + 房间 + 还没有加入任何房间。 + 加入房间 + 房间 JID + 你的昵称 + 房间密码 + 离开房间 输入消息… 发送 - 附件 - 录音 - 停止录音 + 附加文件 + 录制音频 发送音频 取消录音 - 无加密 - OTR - OMEMO - OpenPGP 选择加密方式 已送达 已读 @@ -75,17 +54,9 @@ 发送失败 %1$s 正在输入… 图片 - 视频 音频 文件 - 上传中… - 下载 - 暂无消息。\n说声你好! - OTR 会话已开始,您的对话已加密。 - OTR 会话已结束。 - 警告:OTR 指纹未验证。 - OMEMO:所有设备已信任。 - OMEMO:检测到不受信任的设备。 + 还没有消息。\n说声你好吧! 设置 @@ -95,62 +66,31 @@ 浅色 深色 语言 - English - Español - 中文 通知 消息通知 - 震动 + 振动 声音 加密 - OMEMO 设备 - OpenPGP 密钥 - OTR 指纹 默认加密方式 关于 版本 - 确定 取消 保存 删除 - 确认 - 错误 - 加载中… - 重试 关闭 - 搜索 - 清除 返回 更多选项 - - 麦克风权限 - AleJabber 需要麦克风权限来录制语音消息。 - 存储权限 - AleJabber 需要存储权限来发送和接收文件。 - 相机权限 - AleJabber 需要相机权限来拍照。 - 权限被拒绝,请在设置中授予权限。 - 打开设置 - 消息 - 收到的聊天消息 + 传入的聊天消息 XMPP 服务 后台 XMPP 连接 AleJabber 已连接 - 来自 %1$s 的新消息 %1$s 的头像 - 状态:%1$s 加密:%1$s - 发送消息 - 附件 - 录制语音消息 - 消息已送达 - 消息已读 - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..5c49f7c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,10 +1,5 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF + + #FF1E3A8A \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index 98eea79..0000000 --- a/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #24308B - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7cbcfba..602b26d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,7 +2,6 @@ AleJabber - Contacts Rooms Accounts Settings @@ -17,12 +16,6 @@ Server Port Use TLS - Online - Offline - Connecting… - Connection Error - Connect - Disconnect No accounts configured.\nTap + to add one. Delete account %1$s? user@example.com @@ -36,38 +29,23 @@ Add Contact Jabber ID (JID) Nickname - Remove Contact - Block Contact - Online - Away - Do Not Disturb - Offline Rooms - Search rooms… No rooms joined yet. Join Room Room JID Your Nickname - Room Password (optional) + Room Password Leave Room - Participants - Topic - Browse Rooms Type a message… Send Attach file Record audio - Stop recording Send audio Cancel audio - No encryption - OTR - OMEMO - OpenPGP Select encryption Delivered Read @@ -75,17 +53,9 @@ Failed to send %1$s is typing… Image - Video Audio File - Uploading… - Download No messages yet.\nSay hello! - OTR session started. Your conversation is now encrypted. - OTR session ended. - Warning: OTR fingerprint not verified. - OMEMO: All devices trusted. - OMEMO: Untrusted devices detected. Settings @@ -95,62 +65,31 @@ Light Dark Language - English - Español - 中文 Notifications Message notifications Vibrate Sound Encryption - OMEMO Devices - OpenPGP Keys - OTR Fingerprints Default encryption About Version - OK Cancel Save Delete - Confirm - Error - Loading… - Retry Close - Search - Clear Back More options - - Microphone Permission - AleJabber needs microphone access to record audio messages. - Storage Permission - AleJabber needs storage access to send and receive files. - Camera Permission - AleJabber needs camera access to take photos. - Permission denied. Please grant it in Settings. - Open Settings - Messages Incoming chat messages XMPP Service Background XMPP connection AleJabber is connected - New message from %1$s %1$s\'s avatar - Status: %1$s Encryption: %1$s - Send message - Attach file - Record audio message - Message delivered - Message read - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 29d79ef..b897d3d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ junitVersion = "1.3.0" espressoCore = "3.7.0" lifecycleRuntimeKtx = "2.10.0" activityCompose = "1.12.4" -kotlin = "2.0.21" +kotlin = "2.1.21" composeBom = "2024.09.00" hilt = "2.59.2" room = "2.7.1" @@ -15,11 +15,11 @@ datastore = "1.1.7" coil = "2.7.0" smack = "4.4.8" okhttp = "4.12.0" -coroutines = "1.9.0" +coroutines = "1.10.2" viewmodelCompose = "2.10.0" materialIconsExtended = "1.7.8" -bouncycastle = "1.78.1" -accompanist = "0.36.0" +bouncycastle = "1.83" +accompanist = "0.37.3" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }