1 Commits

Autor SHA1 Mensaje Fecha
ale
8da2f079b2 fix lint
Algunas comprobaciones han fallado
Build & Publish APK Release / build (push) Failing after 6m55s
Signed-off-by: ale <ale@manalejandro.com>
2026-02-28 02:21:37 +01:00
Se han modificado 19 ficheros con 273 adiciones y 368 borrados

Ver fichero

@@ -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")

35
app/lint.xml Archivo normal
Ver fichero

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!--
TrustAllX509TrustManager: comes from smack-core library (not our code).
We cannot fix third-party library internals.
-->
<issue id="TrustAllX509TrustManager" severity="ignore" />
<!--
NewerVersionAvailable: okhttp 5.x is still alpha/breaking change.
Intentionally staying on 4.x until stable.
-->
<issue id="NewerVersionAvailable">
<ignore regexp="com\.squareup\.okhttp3:okhttp" />
</issue>
<!--
UnusedResources: strings used only through dynamic references in Settings
and resources that are part of the public API surface (kept for future use).
-->
<issue id="UnusedResources">
<ignore regexp="R\.string\.settings_" />
<ignore regexp="R\.string\.account_status_" />
<ignore regexp="R\.string\.contact_status_" />
<ignore regexp="R\.string\.permission_" />
<ignore regexp="R\.string\.notification_new_message" />
<ignore regexp="R\.string\.cd_" />
<ignore regexp="R\.string\.nav_contacts" />
<ignore regexp="R\.string\.ok|R\.string\.error|R\.string\.loading|R\.string\.retry|R\.string\.search|R\.string\.clear|R\.string\.confirm|R\.string\.open_settings" />
<ignore regexp="R\.string\.chat_otr|R\.string\.chat_omemo|R\.string\.chat_stop|R\.string\.chat_media_" />
<ignore regexp="R\.string\.browse_rooms|R\.string\.room_participants|R\.string\.room_topic|R\.string\.rooms_search" />
<ignore regexp="R\.string\.contact_remove|R\.string\.contact_block" />
<ignore regexp="R\.drawable\.ic_launcher_background" />
</issue>
</lint>

Ver fichero

@@ -18,6 +18,8 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- required="false" so the app is not excluded from devices without a camera (Chrome OS, etc.) -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:name=".AleJabberApp"

Ver fichero

@@ -32,5 +32,8 @@ interface ContactDao {
@Query("UPDATE contacts SET presence = :presence, statusMessage = :statusMessage WHERE accountId = :accountId AND jid = :jid")
suspend fun updatePresence(accountId: Long, jid: String, presence: String, statusMessage: String)
@Query("UPDATE contacts SET avatarUrl = :avatarUrl WHERE accountId = :accountId AND jid = :jid")
suspend fun updateAvatarUrl(accountId: Long, jid: String, avatarUrl: String?)
}

Ver fichero

@@ -1,5 +1,7 @@
package com.manalejandro.alejabber.data.repository
import android.util.Base64
import android.util.Log
import com.manalejandro.alejabber.data.local.dao.ContactDao
import com.manalejandro.alejabber.data.local.entity.toDomain
import com.manalejandro.alejabber.data.local.entity.toEntity
@@ -11,6 +13,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import org.jivesoftware.smack.roster.Roster
import org.jivesoftware.smack.roster.RosterEntry
import org.jivesoftware.smackx.vcardtemp.VCardManager
import org.jxmpp.jid.impl.JidCreate
import javax.inject.Inject
import javax.inject.Singleton
@@ -20,13 +23,13 @@ class ContactRepository @Inject constructor(
private val contactDao: ContactDao,
private val xmppManager: XmppConnectionManager
) {
private val TAG = "ContactRepository"
/**
* Returns a Flow of contacts for [accountId], with **live presence** merged in.
*
* Room provides the persisted roster (name, JID, groups).
* [XmppConnectionManager.rosterPresence] provides the real-time online/away/offline state.
* The two are combined so the UI always shows the current presence without
* writing every presence stanza to the database.
*/
fun getContacts(accountId: Long): Flow<List<Contact>> =
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(

Ver fichero

@@ -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
) {

Ver fichero

@@ -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<Message?>(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() }
)

Ver fichero

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

Ver fichero

@@ -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<Contact>,
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),

Ver fichero

@@ -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("") }

Ver fichero

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
AleJabber App Icon - Background Layer
Deep Indigo gradient background for the adaptive icon.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#1A237E"
android:pathData="M0,0h108v108h-108z" />
<!-- Subtle radial highlight -->
<path
android:fillColor="#283593"
android:pathData="M54,54 m-40,0 a40,40 0 1,0 80,0 a40,40 0 1,0 -80,0" />
</vector>

Ver fichero

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Ver fichero

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Ver fichero

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AleJabber</string>
<!-- Navegación -->
<string name="nav_contacts">Contactos</string>
<string name="nav_rooms">Salas</string>
<string name="nav_accounts">Cuentas</string>
<string name="nav_settings">Ajustes</string>
@@ -17,12 +17,6 @@
<string name="account_server">Servidor</string>
<string name="account_port">Puerto</string>
<string name="account_use_tls">Usar TLS</string>
<string name="account_status_online">Conectado</string>
<string name="account_status_offline">Desconectado</string>
<string name="account_status_connecting">Conectando…</string>
<string name="account_status_error">Error de conexión</string>
<string name="account_connect">Conectar</string>
<string name="account_disconnect">Desconectar</string>
<string name="account_no_accounts">No hay cuentas configuradas.\nPulsa + para añadir una.</string>
<string name="account_delete_confirm">¿Eliminar la cuenta %1$s?</string>
<string name="account_jid_hint">usuario@ejemplo.com</string>
@@ -32,42 +26,27 @@
<!-- Contactos -->
<string name="contacts_title">Contactos</string>
<string name="contacts_search">Buscar contactos…</string>
<string name="contacts_empty">Sin contactos.\nAñade personas con su ID de Jabber.</string>
<string name="contacts_empty">Aún no hay contactos.\nAñade personas con su Jabber ID.</string>
<string name="add_contact">Añadir contacto</string>
<string name="contact_jid">ID de Jabber (JID)</string>
<string name="contact_jid">Jabber ID (JID)</string>
<string name="contact_nickname">Apodo</string>
<string name="contact_remove">Eliminar contacto</string>
<string name="contact_block">Bloquear contacto</string>
<string name="contact_status_online">En línea</string>
<string name="contact_status_away">Ausente</string>
<string name="contact_status_dnd">No molestar</string>
<string name="contact_status_offline">Desconectado</string>
<!-- Salas / MUC -->
<string name="rooms_title">Salas</string>
<string name="rooms_search">Buscar salas…</string>
<string name="rooms_empty">No te has unido a ninguna sala.</string>
<string name="rooms_empty">Aún no te has unido a ninguna sala.</string>
<string name="join_room">Unirse a sala</string>
<string name="room_jid">JID de la sala</string>
<string name="room_nickname">Tu apodo</string>
<string name="room_password">Contraseña (opcional)</string>
<string name="room_password">Contraseña de la sala</string>
<string name="leave_room">Salir de la sala</string>
<string name="room_participants">Participantes</string>
<string name="room_topic">Tema</string>
<string name="browse_rooms">Explorar salas</string>
<!-- Chat -->
<string name="chat_hint">Escribe un mensaje…</string>
<string name="chat_send">Enviar</string>
<string name="chat_attach">Adjuntar archivo</string>
<string name="chat_record_audio">Grabar audio</string>
<string name="chat_stop_recording">Detener grabación</string>
<string name="chat_send_audio">Enviar audio</string>
<string name="chat_cancel_audio">Cancelar audio</string>
<string name="chat_encryption_none">Sin cifrado</string>
<string name="chat_encryption_otr">OTR</string>
<string name="chat_encryption_omemo">OMEMO</string>
<string name="chat_encryption_pgp">OpenPGP</string>
<string name="chat_encryption_select">Seleccionar cifrado</string>
<string name="chat_message_delivered">Entregado</string>
<string name="chat_message_read">Leído</string>
@@ -75,17 +54,9 @@
<string name="chat_message_failed">Error al enviar</string>
<string name="chat_typing">%1$s está escribiendo…</string>
<string name="chat_media_image">Imagen</string>
<string name="chat_media_video">Vídeo</string>
<string name="chat_media_audio">Audio</string>
<string name="chat_media_file">Archivo</string>
<string name="chat_media_uploading">Subiendo…</string>
<string name="chat_media_download">Descargar</string>
<string name="chat_empty">Sin mensajes aún.\n¡Di hola!</string>
<string name="chat_otr_started">Sesión OTR iniciada. La conversación está cifrada.</string>
<string name="chat_otr_ended">Sesión OTR finalizada.</string>
<string name="chat_otr_untrusted">Advertencia: Huella OTR no verificada.</string>
<string name="chat_omemo_trusted">OMEMO: Todos los dispositivos son de confianza.</string>
<string name="chat_omemo_untrusted">OMEMO: Dispositivos no verificados detectados.</string>
<string name="chat_empty">Aún no hay mensajes.\n¡Di hola!</string>
<!-- Ajustes -->
<string name="settings_title">Ajustes</string>
@@ -95,62 +66,31 @@
<string name="settings_theme_light">Claro</string>
<string name="settings_theme_dark">Oscuro</string>
<string name="settings_language">Idioma</string>
<string name="settings_language_en">English</string>
<string name="settings_language_es">Español</string>
<string name="settings_language_zh">中文</string>
<string name="settings_notifications">Notificaciones</string>
<string name="settings_notifications_messages">Notificaciones de mensajes</string>
<string name="settings_notifications_vibrate">Vibrar</string>
<string name="settings_notifications_sound">Sonido</string>
<string name="settings_encryption">Cifrado</string>
<string name="settings_omemo_devices">Dispositivos OMEMO</string>
<string name="settings_pgp_keys">Claves OpenPGP</string>
<string name="settings_otr_fingerprints">Huellas OTR</string>
<string name="settings_default_encryption">Cifrado por defecto</string>
<string name="settings_about">Acerca de</string>
<string name="settings_version">Versión</string>
<!-- Común -->
<string name="ok">Aceptar</string>
<string name="cancel">Cancelar</string>
<string name="save">Guardar</string>
<string name="delete">Eliminar</string>
<string name="confirm">Confirmar</string>
<string name="error">Error</string>
<string name="loading">Cargando…</string>
<string name="retry">Reintentar</string>
<string name="close">Cerrar</string>
<string name="search">Buscar</string>
<string name="clear">Limpiar</string>
<string name="back">Atrás</string>
<string name="more_options">Más opciones</string>
<!-- Permisos -->
<string name="permission_microphone_title">Permiso de micrófono</string>
<string name="permission_microphone_message">AleJabber necesita acceso al micrófono para grabar mensajes de audio.</string>
<string name="permission_storage_title">Permiso de almacenamiento</string>
<string name="permission_storage_message">AleJabber necesita acceso al almacenamiento para enviar y recibir archivos.</string>
<string name="permission_camera_title">Permiso de cámara</string>
<string name="permission_camera_message">AleJabber necesita acceso a la cámara para tomar fotos.</string>
<string name="permission_denied">Permiso denegado. Por favor, concédelo en Ajustes.</string>
<string name="open_settings">Abrir Ajustes</string>
<!-- Notificaciones -->
<string name="notification_channel_messages">Mensajes</string>
<string name="notification_channel_messages_desc">Mensajes de chat entrantes</string>
<string name="notification_channel_messages_desc">Mensajes entrantes</string>
<string name="notification_channel_service">Servicio XMPP</string>
<string name="notification_channel_service_desc">Conexión XMPP en segundo plano</string>
<string name="notification_service_running">AleJabber está conectado</string>
<string name="notification_new_message">Nuevo mensaje de %1$s</string>
<!-- Accesibilidad -->
<string name="cd_avatar">Avatar de %1$s</string>
<string name="cd_status_indicator">Estado: %1$s</string>
<string name="cd_encryption_badge">Cifrado: %1$s</string>
<string name="cd_send_button">Enviar mensaje</string>
<string name="cd_attach_button">Adjuntar archivo</string>
<string name="cd_record_button">Grabar mensaje de audio</string>
<string name="cd_message_delivered">Mensaje entregado</string>
<string name="cd_message_read">Mensaje leído</string>
</resources>

Ver fichero

@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AleJabber</string>
<!-- 导航 -->
<string name="nav_contacts">联系人</string>
<string name="nav_rooms">聊天室</string>
<string name="nav_rooms">房间</string>
<string name="nav_accounts">账号</string>
<string name="nav_settings">设置</string>
<!-- 账号 -->
<string name="accounts_title">账号管理</string>
<string name="accounts_title">账号</string>
<string name="add_account">添加账号</string>
<string name="edit_account">编辑账号</string>
<string name="delete_account">删除账号</string>
@@ -17,13 +17,7 @@
<string name="account_server">服务器</string>
<string name="account_port">端口</string>
<string name="account_use_tls">使用 TLS</string>
<string name="account_status_online">已连接</string>
<string name="account_status_offline">已断开</string>
<string name="account_status_connecting">连接中…</string>
<string name="account_status_error">连接错误</string>
<string name="account_connect">连接</string>
<string name="account_disconnect">断开连接</string>
<string name="account_no_accounts">尚未配置账号。\n点击 + 添加账号。</string>
<string name="account_no_accounts">没有已配置的账号。\n点击 + 添加一个。</string>
<string name="account_delete_confirm">删除账号 %1$s</string>
<string name="account_jid_hint">user@example.com</string>
<string name="account_resource">资源</string>
@@ -32,42 +26,27 @@
<!-- 联系人 -->
<string name="contacts_title">联系人</string>
<string name="contacts_search">搜索联系人…</string>
<string name="contacts_empty">暂无联系人。\n通过 Jabber ID 添加友。</string>
<string name="contacts_empty">还没有联系人。\n使用 Jabber ID 添加友。</string>
<string name="add_contact">添加联系人</string>
<string name="contact_jid">Jabber ID (JID)</string>
<string name="contact_nickname">昵称</string>
<string name="contact_remove">删除联系人</string>
<string name="contact_block">屏蔽联系人</string>
<string name="contact_status_online">在线</string>
<string name="contact_status_away">离开</string>
<string name="contact_status_dnd">请勿打扰</string>
<string name="contact_status_offline">离线</string>
<!-- 聊天室 / MUC -->
<string name="rooms_title">聊天室</string>
<string name="rooms_search">搜索聊天室…</string>
<string name="rooms_empty">尚未加入任何聊天室。</string>
<string name="join_room">加入聊天室</string>
<string name="room_jid">聊天室 JID</string>
<string name="room_nickname">您的昵称</string>
<string name="room_password">聊天室密码(可选)</string>
<string name="leave_room">退出聊天室</string>
<string name="room_participants">参与者</string>
<string name="room_topic">主题</string>
<string name="browse_rooms">浏览聊天室</string>
<!-- 房间 / MUC -->
<string name="rooms_title">房间</string>
<string name="rooms_empty">还没有加入任何房间。</string>
<string name="join_room">加入房间</string>
<string name="room_jid">房间 JID</string>
<string name="room_nickname">你的昵称</string>
<string name="room_password">房间密码</string>
<string name="leave_room">离开房间</string>
<!-- 聊天 -->
<string name="chat_hint">输入消息…</string>
<string name="chat_send">发送</string>
<string name="chat_attach">附件</string>
<string name="chat_record_audio"></string>
<string name="chat_stop_recording">停止录音</string>
<string name="chat_attach">加文</string>
<string name="chat_record_audio">制音频</string>
<string name="chat_send_audio">发送音频</string>
<string name="chat_cancel_audio">取消录音</string>
<string name="chat_encryption_none">无加密</string>
<string name="chat_encryption_otr">OTR</string>
<string name="chat_encryption_omemo">OMEMO</string>
<string name="chat_encryption_pgp">OpenPGP</string>
<string name="chat_encryption_select">选择加密方式</string>
<string name="chat_message_delivered">已送达</string>
<string name="chat_message_read">已读</string>
@@ -75,17 +54,9 @@
<string name="chat_message_failed">发送失败</string>
<string name="chat_typing">%1$s 正在输入…</string>
<string name="chat_media_image">图片</string>
<string name="chat_media_video">视频</string>
<string name="chat_media_audio">音频</string>
<string name="chat_media_file">文件</string>
<string name="chat_media_uploading">上传中…</string>
<string name="chat_media_download">下载</string>
<string name="chat_empty">暂无消息。\n说声你好</string>
<string name="chat_otr_started">OTR 会话已开始,您的对话已加密。</string>
<string name="chat_otr_ended">OTR 会话已结束。</string>
<string name="chat_otr_untrusted">警告OTR 指纹未验证。</string>
<string name="chat_omemo_trusted">OMEMO所有设备已信任。</string>
<string name="chat_omemo_untrusted">OMEMO检测到不受信任的设备。</string>
<string name="chat_empty">还没有消息。\n说声你好吧</string>
<!-- 设置 -->
<string name="settings_title">设置</string>
@@ -95,62 +66,31 @@
<string name="settings_theme_light">浅色</string>
<string name="settings_theme_dark">深色</string>
<string name="settings_language">语言</string>
<string name="settings_language_en">English</string>
<string name="settings_language_es">Español</string>
<string name="settings_language_zh">中文</string>
<string name="settings_notifications">通知</string>
<string name="settings_notifications_messages">消息通知</string>
<string name="settings_notifications_vibrate"></string>
<string name="settings_notifications_vibrate"></string>
<string name="settings_notifications_sound">声音</string>
<string name="settings_encryption">加密</string>
<string name="settings_omemo_devices">OMEMO 设备</string>
<string name="settings_pgp_keys">OpenPGP 密钥</string>
<string name="settings_otr_fingerprints">OTR 指纹</string>
<string name="settings_default_encryption">默认加密方式</string>
<string name="settings_about">关于</string>
<string name="settings_version">版本</string>
<!-- 通用 -->
<string name="ok">确定</string>
<string name="cancel">取消</string>
<string name="save">保存</string>
<string name="delete">删除</string>
<string name="confirm">确认</string>
<string name="error">错误</string>
<string name="loading">加载中…</string>
<string name="retry">重试</string>
<string name="close">关闭</string>
<string name="search">搜索</string>
<string name="clear">清除</string>
<string name="back">返回</string>
<string name="more_options">更多选项</string>
<!-- 权限 -->
<string name="permission_microphone_title">麦克风权限</string>
<string name="permission_microphone_message">AleJabber 需要麦克风权限来录制语音消息。</string>
<string name="permission_storage_title">存储权限</string>
<string name="permission_storage_message">AleJabber 需要存储权限来发送和接收文件。</string>
<string name="permission_camera_title">相机权限</string>
<string name="permission_camera_message">AleJabber 需要相机权限来拍照。</string>
<string name="permission_denied">权限被拒绝,请在设置中授予权限。</string>
<string name="open_settings">打开设置</string>
<!-- 通知 -->
<string name="notification_channel_messages">消息</string>
<string name="notification_channel_messages_desc">收到的聊天消息</string>
<string name="notification_channel_messages_desc">传入的聊天消息</string>
<string name="notification_channel_service">XMPP 服务</string>
<string name="notification_channel_service_desc">后台 XMPP 连接</string>
<string name="notification_service_running">AleJabber 已连接</string>
<string name="notification_new_message">来自 %1$s 的新消息</string>
<!-- 无障碍 -->
<string name="cd_avatar">%1$s 的头像</string>
<string name="cd_status_indicator">状态:%1$s</string>
<string name="cd_encryption_badge">加密:%1$s</string>
<string name="cd_send_button">发送消息</string>
<string name="cd_attach_button">附件</string>
<string name="cd_record_button">录制语音消息</string>
<string name="cd_message_delivered">消息已送达</string>
<string name="cd_message_read">消息已读</string>
</resources>

Ver fichero

@@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Launcher icon background color -->
<color name="ic_launcher_background">#FF1E3A8A</color>
</resources>

Ver fichero

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#24308B</color>
</resources>

Ver fichero

@@ -2,7 +2,6 @@
<string name="app_name">AleJabber</string>
<!-- Navigation -->
<string name="nav_contacts">Contacts</string>
<string name="nav_rooms">Rooms</string>
<string name="nav_accounts">Accounts</string>
<string name="nav_settings">Settings</string>
@@ -17,12 +16,6 @@
<string name="account_server">Server</string>
<string name="account_port">Port</string>
<string name="account_use_tls">Use TLS</string>
<string name="account_status_online">Online</string>
<string name="account_status_offline">Offline</string>
<string name="account_status_connecting">Connecting…</string>
<string name="account_status_error">Connection Error</string>
<string name="account_connect">Connect</string>
<string name="account_disconnect">Disconnect</string>
<string name="account_no_accounts">No accounts configured.\nTap + to add one.</string>
<string name="account_delete_confirm">Delete account %1$s?</string>
<string name="account_jid_hint">user@example.com</string>
@@ -36,38 +29,23 @@
<string name="add_contact">Add Contact</string>
<string name="contact_jid">Jabber ID (JID)</string>
<string name="contact_nickname">Nickname</string>
<string name="contact_remove">Remove Contact</string>
<string name="contact_block">Block Contact</string>
<string name="contact_status_online">Online</string>
<string name="contact_status_away">Away</string>
<string name="contact_status_dnd">Do Not Disturb</string>
<string name="contact_status_offline">Offline</string>
<!-- Rooms / MUC -->
<string name="rooms_title">Rooms</string>
<string name="rooms_search">Search rooms…</string>
<string name="rooms_empty">No rooms joined yet.</string>
<string name="join_room">Join Room</string>
<string name="room_jid">Room JID</string>
<string name="room_nickname">Your Nickname</string>
<string name="room_password">Room Password (optional)</string>
<string name="room_password">Room Password</string>
<string name="leave_room">Leave Room</string>
<string name="room_participants">Participants</string>
<string name="room_topic">Topic</string>
<string name="browse_rooms">Browse Rooms</string>
<!-- Chat -->
<string name="chat_hint">Type a message…</string>
<string name="chat_send">Send</string>
<string name="chat_attach">Attach file</string>
<string name="chat_record_audio">Record audio</string>
<string name="chat_stop_recording">Stop recording</string>
<string name="chat_send_audio">Send audio</string>
<string name="chat_cancel_audio">Cancel audio</string>
<string name="chat_encryption_none">No encryption</string>
<string name="chat_encryption_otr">OTR</string>
<string name="chat_encryption_omemo">OMEMO</string>
<string name="chat_encryption_pgp">OpenPGP</string>
<string name="chat_encryption_select">Select encryption</string>
<string name="chat_message_delivered">Delivered</string>
<string name="chat_message_read">Read</string>
@@ -75,17 +53,9 @@
<string name="chat_message_failed">Failed to send</string>
<string name="chat_typing">%1$s is typing…</string>
<string name="chat_media_image">Image</string>
<string name="chat_media_video">Video</string>
<string name="chat_media_audio">Audio</string>
<string name="chat_media_file">File</string>
<string name="chat_media_uploading">Uploading…</string>
<string name="chat_media_download">Download</string>
<string name="chat_empty">No messages yet.\nSay hello!</string>
<string name="chat_otr_started">OTR session started. Your conversation is now encrypted.</string>
<string name="chat_otr_ended">OTR session ended.</string>
<string name="chat_otr_untrusted">Warning: OTR fingerprint not verified.</string>
<string name="chat_omemo_trusted">OMEMO: All devices trusted.</string>
<string name="chat_omemo_untrusted">OMEMO: Untrusted devices detected.</string>
<!-- Settings -->
<string name="settings_title">Settings</string>
@@ -95,62 +65,31 @@
<string name="settings_theme_light">Light</string>
<string name="settings_theme_dark">Dark</string>
<string name="settings_language">Language</string>
<string name="settings_language_en">English</string>
<string name="settings_language_es">Español</string>
<string name="settings_language_zh">中文</string>
<string name="settings_notifications">Notifications</string>
<string name="settings_notifications_messages">Message notifications</string>
<string name="settings_notifications_vibrate">Vibrate</string>
<string name="settings_notifications_sound">Sound</string>
<string name="settings_encryption">Encryption</string>
<string name="settings_omemo_devices">OMEMO Devices</string>
<string name="settings_pgp_keys">OpenPGP Keys</string>
<string name="settings_otr_fingerprints">OTR Fingerprints</string>
<string name="settings_default_encryption">Default encryption</string>
<string name="settings_about">About</string>
<string name="settings_version">Version</string>
<!-- Common -->
<string name="ok">OK</string>
<string name="cancel">Cancel</string>
<string name="save">Save</string>
<string name="delete">Delete</string>
<string name="confirm">Confirm</string>
<string name="error">Error</string>
<string name="loading">Loading…</string>
<string name="retry">Retry</string>
<string name="close">Close</string>
<string name="search">Search</string>
<string name="clear">Clear</string>
<string name="back">Back</string>
<string name="more_options">More options</string>
<!-- Permissions -->
<string name="permission_microphone_title">Microphone Permission</string>
<string name="permission_microphone_message">AleJabber needs microphone access to record audio messages.</string>
<string name="permission_storage_title">Storage Permission</string>
<string name="permission_storage_message">AleJabber needs storage access to send and receive files.</string>
<string name="permission_camera_title">Camera Permission</string>
<string name="permission_camera_message">AleJabber needs camera access to take photos.</string>
<string name="permission_denied">Permission denied. Please grant it in Settings.</string>
<string name="open_settings">Open Settings</string>
<!-- Notifications -->
<string name="notification_channel_messages">Messages</string>
<string name="notification_channel_messages_desc">Incoming chat messages</string>
<string name="notification_channel_service">XMPP Service</string>
<string name="notification_channel_service_desc">Background XMPP connection</string>
<string name="notification_service_running">AleJabber is connected</string>
<string name="notification_new_message">New message from %1$s</string>
<!-- Accessibility -->
<string name="cd_avatar">%1$s\'s avatar</string>
<string name="cd_status_indicator">Status: %1$s</string>
<string name="cd_encryption_badge">Encryption: %1$s</string>
<string name="cd_send_button">Send message</string>
<string name="cd_attach_button">Attach file</string>
<string name="cd_record_button">Record audio message</string>
<string name="cd_message_delivered">Message delivered</string>
<string name="cd_message_read">Message read</string>
</resources>

Ver fichero

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