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" }