Compare commits

..

No commits in common. "master" and "alpha-220000003" have entirely different histories.

236 changed files with 1173 additions and 7718 deletions
.devcontainer
.drone.yml
.github/workflows
CHANGELOG.md
app
build.gradle
schemas/com.nextcloud.talk.data.source.local.TalkDatabase
src/main
java/com/nextcloud/talk
account
activities
adapters
api
bottomsheet/items
call/components
chat
components
contacts
conversationcreation
conversationinfo
conversationinfoedit
conversationlist
data
diagnose
fullscreenfile
invitation
location
messagesearch
models
openconversations
profile
receivers
repositories/conversations
settings
shareditems/activities
translate/ui
ui/dialog
utils
res

View File

@ -1,4 +1,4 @@
FROM ubuntu:noble@sha256:440dcf6a5640b2ae5c77724e68787a906afb8ddee98bf86db94eea8528c2c076
FROM ubuntu:noble@sha256:6015f66923d7afbc53558d7ccffd325d43b4e249f41a6e93eef074c9505d2233
ARG DEBIAN_FRONTEND=noninteractive
ENV ANDROID_HOME=/usr/lib/android-sdk

View File

@ -8,7 +8,7 @@ name: generic
steps:
- name: generic
image: ghcr.io/nextcloud/continuous-integration-android8:4
image: ghcr.io/nextcloud/continuous-integration-android8:3
commands:
- ./gradlew --console=plain assembleGeneric
@ -27,7 +27,7 @@ name: gplay
steps:
- name: gplay
image: ghcr.io/nextcloud/continuous-integration-android8:4
image: ghcr.io/nextcloud/continuous-integration-android8:3
commands:
- ./gradlew --console=plain assembleGplay
@ -46,7 +46,7 @@ name: tests
steps:
- name: all
image: ghcr.io/nextcloud/continuous-integration-android8:4
image: ghcr.io/nextcloud/continuous-integration-android8:3
privileged: true
commands:
- emulator -avd android -no-snapshot -gpu swiftshader_indirect -no-window -no-audio -skin 500x833 &
@ -81,6 +81,4 @@ trigger:
- pull_request
---
kind: signature
hmac: cf0c19e54fa45d1ee226f5f05202a32329b90aaf46711ea073c566a4c4a8a6c5
...
hmac: cdce3f7eea46ef85c0223f62f66d1fe53d7dad007ef095c9f70fa063450d8c75

View File

@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Disabled on forks
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
run: |
echo 'Can not analyze PRs from forks'
exit 1

View File

@ -34,7 +34,7 @@ jobs:
java-version: 17
- name: Gradle validate
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- name: Build ${{ matrix.flavor }}
run: |

View File

@ -43,7 +43,7 @@ jobs:
with:
swap-size-gb: 10
- name: Initialize CodeQL
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with:
languages: ${{ matrix.language }}
- name: Set up JDK 17
@ -57,4 +57,4 @@ jobs:
echo "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties"
./gradlew assembleDebug
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18

View File

@ -36,7 +36,7 @@ jobs:
blocklist=$(curl https://raw.githubusercontent.com/nextcloud/.github/master/non-community-usernames.txt | paste -s -d, -)
echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT"
- uses: nextcloud/pr-feedback-action@e397f3c7e655092b746e3610d121545530c6a90e # main
- uses: nextcloud/pr-feedback-action@1883b38a033fb16f576875e0cf45f98b857655c4 # main
with:
feedback-message: |
Hello there,

View File

@ -29,6 +29,8 @@ jobs:
permissions:
# for hmarr/auto-approve-action to approve PRs
pull-requests: write
# for alexwilson/enable-github-automerge-action to approve PRs
contents: write
steps:
- name: Disabled on forks
@ -44,18 +46,13 @@ jobs:
# GitHub actions bot approve
- uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 # v4.0.0
if: github.actor == 'renovate[bot]'
if: startsWith(steps.branchname.outputs.branch, 'renovate/')
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.head_ref }}
# Enable GitHub auto merge
- name: Enable Pull Request Automerge
if: github.actor == 'renovate[bot]'
run: gh pr merge --merge --auto
env:
GH_TOKEN: ${{ secrets.AUTOMERGE }}
- name: Auto merge
uses: alexwilson/enable-github-automerge-action@56e3117d1ae1540309dc8f7a9f2825bc3c5f06ff # v2.0.0
if: startsWith(steps.branchname.outputs.branch, 'renovate/')
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -12,11 +12,11 @@ name: REUSE Compliance Check
on: [pull_request]
permissions:
contents: read
contents: read
jobs:
reuse-compliance-check:
runs-on: ubuntu-latest-low
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

View File

@ -34,7 +34,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
with:
results_file: results.sarif
results_format: sarif
@ -42,6 +42,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with:
sarif_file: results.sarif

View File

@ -33,7 +33,7 @@ jobs:
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- name: Run unit tests with coverage
run: ./gradlew testGplayDebugUnit

View File

@ -9,36 +9,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Types of changes can be: Added/Changed/Deprecated/Removed/Fixed/Security
## [21.1.0] - 2025-06-05
### Added
- Allow adding participants to one-to-one chats creating a new conversation
- Handling of event conversations
- Show info about participant (organization, role, timezone, ...) in 1:1 conversation info screen
- Added gallery option in chat attachment menu (access photos and videos with one click without giving permissions)
- Add self-test button for push notifications in diagnosis screen
- Edit checkbox in chat messages
- Team mentions in chat
- Add option to mark a conversation as sensitive
- Allow bluetooth headset to be discovered and used during a call (@gavine99)
### Changed
- Design of participants grid in call
- Improve subline in conversations screen when last message is attachment
- Improve message search
- In search window, show messages at last
- Switch video capture for calls between 4:3 and 16:9 ratio depending on portrait/ landscape mode
### Fixed
- Crashes
- Videos in videocall lost after app comes back to foreground
- Open conversations not being shown in search
- Minor bugs (@MmAaXx500)
Minimum: NC 17 Server, Android 8.0 Oreo
For a full list, please see https://github.com/nextcloud/talk-android/milestone/94?closed=1
## [21.0.1] - 2025-04-15
### Fixed

View File

@ -13,9 +13,9 @@ import com.github.spotbugs.snom.Effort
import com.github.spotbugs.snom.SpotBugsTask
plugins {
id "org.jetbrains.kotlin.plugin.compose" version "2.2.0"
id "org.jetbrains.kotlin.plugin.compose" version "2.1.21"
id "org.jetbrains.kotlin.kapt"
id 'com.google.devtools.ksp' version '2.2.0-2.0.2'
id 'com.google.devtools.ksp' version '2.1.21-2.0.1'
}
apply plugin: 'com.android.application'
@ -28,19 +28,19 @@ apply plugin: "org.jlleitschuh.gradle.ktlint"
apply plugin: 'kotlinx-serialization'
android {
compileSdk 35
compileSdk 34
namespace 'com.nextcloud.talk'
defaultConfig {
minSdkVersion 26
targetSdkVersion 35
targetSdkVersion 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)
// xx .xxx .xx .xx
versionCode 220000009
versionName "22.0.0 Alpha 09"
versionCode 220000003
versionName "22.0.0 Alpha 03"
flavorDimensions "default"
renderscriptTargetApi 19
@ -155,18 +155,18 @@ ext {
daggerVersion = "2.56.2"
emojiVersion = "1.5.0"
fidoVersion = "4.1.0-patch2"
lifecycleVersion = '2.9.1'
lifecycleVersion = '2.8.7'
okhttpVersion = "4.12.0"
markwonVersion = "4.6.2"
materialDialogsVersion = "3.3.0"
parcelerVersion = "1.1.13"
prismVersion = "2.0.0"
retrofit2Version = "3.0.0"
roomVersion = "2.7.2"
workVersion = "2.10.2"
retrofit2Version = "2.11.0"
roomVersion = "2.7.1"
workVersion = "2.9.1"
espressoVersion = "3.6.1"
androidxTestVersion = "1.5.0"
media3_version = "1.7.1"
media3_version = "1.4.1"
coroutines_version = "1.10.2"
mockitoKotlinVersion = "5.4.0"
}
@ -180,10 +180,10 @@ configurations.configureEach {
dependencies {
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0'
spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.11'
spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.9'
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.8")
implementation("androidx.compose.runtime:runtime:1.8.3")
implementation("androidx.compose.runtime:runtime:1.7.8")
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.datastore:datastore-core:1.1.7'
implementation 'androidx.datastore:datastore-preferences:1.1.7'
@ -191,9 +191,9 @@ dependencies {
implementation fileTree(include: ['*'], dir: 'libs')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1"
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation "com.vanniktech:emoji-google:0.21.0"
@ -236,7 +236,7 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}"
implementation 'com.bluelinelabs:logansquare:1.3.7'
implementation 'com.fasterxml.jackson.core:jackson-core:2.19.1'
implementation 'com.fasterxml.jackson.core:jackson-core:2.14.3'
kapt 'com.bluelinelabs:logansquare-compiler:1.3.7'
implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}"
@ -301,24 +301,24 @@ dependencies {
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
})
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.activity:activity-ktx:1.10.1'
implementation 'com.github.nextcloud.android-common:ui:0.26.0'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.9.3'
implementation 'com.github.nextcloud.android-common:ui:0.23.2'
implementation 'com.github.nextcloud-deps:android-talk-webrtc:132.6834.0'
gplayImplementation 'com.google.android.gms:play-services-base:18.6.0'
gplayImplementation "com.google.firebase:firebase-messaging:24.1.2"
gplayImplementation "com.google.firebase:firebase-messaging:24.1.1"
//compose
implementation(platform("androidx.compose:compose-bom:2025.06.01"))
implementation(platform("androidx.compose:compose-bom:2025.04.00"))
implementation("androidx.compose.ui:ui")
implementation 'androidx.compose.material3:material3:1.3.2'
implementation("androidx.compose.ui:ui-tooling-preview")
implementation 'androidx.activity:activity-compose:1.10.1'
implementation 'androidx.activity:activity-compose:1.9.3'
debugImplementation("androidx.compose.ui:ui-tooling")
//tests
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.8.3")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.7.8")
debugImplementation("androidx.compose.ui:ui-test-manifest")
testImplementation 'junit:junit:4.13.2'
@ -343,11 +343,11 @@ dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.2')
androidTestImplementation(platform("androidx.compose:compose-bom:2025.06.01"))
androidTestImplementation(platform("androidx.compose:compose-bom:2025.04.00"))
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
testImplementation 'org.junit.vintage:junit-vintage-engine:5.13.3'
testImplementation 'org.junit.vintage:junit-vintage-engine:5.12.2'
}
tasks.register('installGitHooks', Copy) {

View File

@ -1,731 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 16,
"identityHash": "bbf526d5c78a99eb951635cc46f4c59f",
"entities": [
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT"
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT"
},
{
"fieldPath": "baseUrl",
"columnName": "baseUrl",
"affinity": "TEXT"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "pushConfigurationState",
"columnName": "pushConfigurationState",
"affinity": "TEXT"
},
{
"fieldPath": "capabilities",
"columnName": "capabilities",
"affinity": "TEXT"
},
{
"fieldPath": "serverVersion",
"columnName": "serverVersion",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "clientCertificate",
"columnName": "clientCertificate",
"affinity": "TEXT"
},
{
"fieldPath": "externalSignalingServer",
"columnName": "externalSignalingServer",
"affinity": "TEXT"
},
{
"fieldPath": "current",
"columnName": "current",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scheduledForDeletion",
"columnName": "scheduledForDeletion",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "ArbitraryStorage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
"fields": [
{
"fieldPath": "accountIdentifier",
"columnName": "accountIdentifier",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "storageObject",
"columnName": "object",
"affinity": "TEXT"
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"accountIdentifier",
"key"
]
}
},
{
"tableName": "Conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatarVersion",
"columnName": "avatarVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "callFlag",
"columnName": "callFlag",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callRecording",
"columnName": "callRecording",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callStartTime",
"columnName": "callStartTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canDeleteConversation",
"columnName": "canDeleteConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canLeaveConversation",
"columnName": "canLeaveConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canStartCall",
"columnName": "canStartCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasCall",
"columnName": "hasCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasPassword",
"columnName": "hasPassword",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasCustomAvatar",
"columnName": "isCustomAvatar",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favorite",
"columnName": "isFavorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastActivity",
"columnName": "lastActivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCommonReadMessage",
"columnName": "lastCommonReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessage",
"columnName": "lastMessage",
"affinity": "TEXT"
},
{
"fieldPath": "lastPing",
"columnName": "lastPing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastReadMessage",
"columnName": "lastReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lobbyState",
"columnName": "lobbyState",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lobbyTimer",
"columnName": "lobbyTimer",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageExpiration",
"columnName": "messageExpiration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationCalls",
"columnName": "notificationCalls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLevel",
"columnName": "notificationLevel",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectType",
"columnName": "objectType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectId",
"columnName": "objectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "participantType",
"columnName": "participantType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "conversationReadOnlyState",
"columnName": "readOnly",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recordingConsentRequired",
"columnName": "recordingConsent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteServer",
"columnName": "remoteServer",
"affinity": "TEXT"
},
{
"fieldPath": "remoteToken",
"columnName": "remoteToken",
"affinity": "TEXT"
},
{
"fieldPath": "sessionId",
"columnName": "sessionId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT"
},
{
"fieldPath": "statusClearAt",
"columnName": "statusClearAt",
"affinity": "INTEGER"
},
{
"fieldPath": "statusIcon",
"columnName": "statusIcon",
"affinity": "TEXT"
},
{
"fieldPath": "statusMessage",
"columnName": "statusMessage",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unreadMention",
"columnName": "unreadMention",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMentionDirect",
"columnName": "unreadMentionDirect",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMessages",
"columnName": "unreadMessages",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasArchived",
"columnName": "hasArchived",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasSensitive",
"columnName": "hasSensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasImportant",
"columnName": "hasImportant",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_Conversations_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "User",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ChatMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorDisplayName",
"columnName": "actorDisplayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationTimestamp",
"columnName": "expirationTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "replyable",
"columnName": "isReplyable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastEditActorDisplayName",
"columnName": "lastEditActorDisplayName",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorId",
"columnName": "lastEditActorId",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorType",
"columnName": "lastEditActorType",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditTimestamp",
"columnName": "lastEditTimestamp",
"affinity": "INTEGER"
},
{
"fieldPath": "renderMarkdown",
"columnName": "markdown",
"affinity": "INTEGER"
},
{
"fieldPath": "messageParameters",
"columnName": "messageParameters",
"affinity": "TEXT"
},
{
"fieldPath": "messageType",
"columnName": "messageType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentMessageId",
"columnName": "parent",
"affinity": "INTEGER"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT"
},
{
"fieldPath": "reactionsSelf",
"columnName": "reactionsSelf",
"affinity": "TEXT"
},
{
"fieldPath": "referenceId",
"columnName": "referenceId",
"affinity": "TEXT"
},
{
"fieldPath": "sendingFailed",
"columnName": "sendingFailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "silent",
"columnName": "silent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "systemMessageType",
"columnName": "systemMessage",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_ChatMessages_internalId",
"unique": true,
"columnNames": [
"internalId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
},
{
"name": "index_ChatMessages_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
},
{
"tableName": "ChatBlocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "oldestMessageId",
"columnName": "oldestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newestMessageId",
"columnName": "newestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasHistory",
"columnName": "hasHistory",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ChatBlocks_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bbf526d5c78a99eb951635cc46f4c59f')"
]
}
}

View File

@ -1,730 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 17,
"identityHash": "5bc4247e179307faa995552da5d34324",
"entities": [
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT"
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT"
},
{
"fieldPath": "baseUrl",
"columnName": "baseUrl",
"affinity": "TEXT"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "pushConfigurationState",
"columnName": "pushConfigurationState",
"affinity": "TEXT"
},
{
"fieldPath": "capabilities",
"columnName": "capabilities",
"affinity": "TEXT"
},
{
"fieldPath": "serverVersion",
"columnName": "serverVersion",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "clientCertificate",
"columnName": "clientCertificate",
"affinity": "TEXT"
},
{
"fieldPath": "externalSignalingServer",
"columnName": "externalSignalingServer",
"affinity": "TEXT"
},
{
"fieldPath": "current",
"columnName": "current",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scheduledForDeletion",
"columnName": "scheduledForDeletion",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "ArbitraryStorage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
"fields": [
{
"fieldPath": "accountIdentifier",
"columnName": "accountIdentifier",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "storageObject",
"columnName": "object",
"affinity": "TEXT"
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"accountIdentifier",
"key"
]
}
},
{
"tableName": "Conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatarVersion",
"columnName": "avatarVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "callFlag",
"columnName": "callFlag",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callRecording",
"columnName": "callRecording",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callStartTime",
"columnName": "callStartTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canDeleteConversation",
"columnName": "canDeleteConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canLeaveConversation",
"columnName": "canLeaveConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canStartCall",
"columnName": "canStartCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasCall",
"columnName": "hasCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasPassword",
"columnName": "hasPassword",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasCustomAvatar",
"columnName": "isCustomAvatar",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favorite",
"columnName": "isFavorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastActivity",
"columnName": "lastActivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCommonReadMessage",
"columnName": "lastCommonReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessage",
"columnName": "lastMessage",
"affinity": "TEXT"
},
{
"fieldPath": "lastPing",
"columnName": "lastPing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastReadMessage",
"columnName": "lastReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lobbyState",
"columnName": "lobbyState",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lobbyTimer",
"columnName": "lobbyTimer",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageExpiration",
"columnName": "messageExpiration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationCalls",
"columnName": "notificationCalls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLevel",
"columnName": "notificationLevel",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectType",
"columnName": "objectType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectId",
"columnName": "objectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "participantType",
"columnName": "participantType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "conversationReadOnlyState",
"columnName": "readOnly",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recordingConsentRequired",
"columnName": "recordingConsent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteServer",
"columnName": "remoteServer",
"affinity": "TEXT"
},
{
"fieldPath": "remoteToken",
"columnName": "remoteToken",
"affinity": "TEXT"
},
{
"fieldPath": "sessionId",
"columnName": "sessionId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT"
},
{
"fieldPath": "statusClearAt",
"columnName": "statusClearAt",
"affinity": "INTEGER"
},
{
"fieldPath": "statusIcon",
"columnName": "statusIcon",
"affinity": "TEXT"
},
{
"fieldPath": "statusMessage",
"columnName": "statusMessage",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unreadMention",
"columnName": "unreadMention",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMentionDirect",
"columnName": "unreadMentionDirect",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMessages",
"columnName": "unreadMessages",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasArchived",
"columnName": "hasArchived",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasSensitive",
"columnName": "hasSensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasImportant",
"columnName": "hasImportant",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_Conversations_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "User",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ChatMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorDisplayName",
"columnName": "actorDisplayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationTimestamp",
"columnName": "expirationTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "replyable",
"columnName": "isReplyable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastEditActorDisplayName",
"columnName": "lastEditActorDisplayName",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorId",
"columnName": "lastEditActorId",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorType",
"columnName": "lastEditActorType",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditTimestamp",
"columnName": "lastEditTimestamp",
"affinity": "INTEGER"
},
{
"fieldPath": "renderMarkdown",
"columnName": "markdown",
"affinity": "INTEGER"
},
{
"fieldPath": "messageParameters",
"columnName": "messageParameters",
"affinity": "TEXT"
},
{
"fieldPath": "messageType",
"columnName": "messageType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentMessageId",
"columnName": "parent",
"affinity": "INTEGER"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT"
},
{
"fieldPath": "reactionsSelf",
"columnName": "reactionsSelf",
"affinity": "TEXT"
},
{
"fieldPath": "referenceId",
"columnName": "referenceId",
"affinity": "TEXT"
},
{
"fieldPath": "sendStatus",
"columnName": "sendStatus",
"affinity": "TEXT"
},
{
"fieldPath": "silent",
"columnName": "silent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "systemMessageType",
"columnName": "systemMessage",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_ChatMessages_internalId",
"unique": true,
"columnNames": [
"internalId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
},
{
"name": "index_ChatMessages_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
},
{
"tableName": "ChatBlocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "oldestMessageId",
"columnName": "oldestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newestMessageId",
"columnName": "newestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasHistory",
"columnName": "hasHistory",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ChatBlocks_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bc4247e179307faa995552da5d34324')"
]
}
}

View File

@ -91,7 +91,7 @@ class AccountVerificationActivity : BaseActivity() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
actionBar?.hide()
initSystemBars()
setupSystemColors()
handleIntent()
}
@ -490,9 +490,9 @@ class AccountVerificationActivity : BaseActivity() {
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
.observeForever { workInfo: WorkInfo? ->
.observeForever { workInfo: WorkInfo ->
when (workInfo?.state) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
val intent = Intent(this, ServerSelectionActivity::class.java)
startActivity(intent)

View File

@ -78,7 +78,7 @@ class ServerSelectionActivity : BaseActivity() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
actionBar?.hide()
initSystemBars()
setupSystemColors()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}

View File

@ -86,7 +86,7 @@ class SwitchAccountActivity : BaseActivity() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
setupActionBar()
initSystemBars()
setupSystemColors()
Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))

View File

@ -114,7 +114,7 @@ class WebViewLoginActivity : BaseActivity() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
actionBar?.hide()
initSystemBars()
setupSystemColors()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
handleIntent()
@ -286,7 +286,6 @@ class WebViewLoginActivity : BaseActivity() {
}
}
@SuppressLint("DiscouragedPrivateApi")
@Suppress("Detekt.TooGenericExceptionCaught")
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
try {
@ -389,9 +388,9 @@ class WebViewLoginActivity : BaseActivity() {
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
.observeForever { workInfo: WorkInfo? ->
.observeForever { workInfo: WorkInfo ->
when (workInfo?.state) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
restartApp()
}

View File

@ -11,13 +11,11 @@ package com.nextcloud.talk.activities
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.webkit.SslErrorHandler
@ -39,7 +37,6 @@ import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.FileViewerUtils
import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.adjustUIForAPILevel35
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.preferences.AppPreferences
@ -84,7 +81,6 @@ open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
adjustUIForAPILevel35()
super.onCreate(savedInstanceState)
cleanTempCertPreference()
@ -115,22 +111,9 @@ open class BaseActivity : AppCompatActivity() {
eventBus.unregister(this)
}
/*
* May be aligned with android-common lib in the future: .../ui/util/extensions/AppCompatActivityExtensions.kt
*/
fun initSystemBars() {
window.decorView.setOnApplyWindowInsetsListener { view, insets ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
val statusBarHeight = insets.getInsets(WindowInsets.Type.statusBars()).top
view.setPadding(0, statusBarHeight, 0, 0)
val color = ResourcesCompat.getColor(resources, R.color.bg_default, context.theme)
view.setBackgroundColor(color)
} else {
colorizeStatusBar()
colorizeNavigationBar()
}
insets
}
fun setupSystemColors() {
colorizeStatusBar()
colorizeNavigationBar()
}
open fun colorizeStatusBar() {

View File

@ -376,8 +376,6 @@ class CallActivity : CallBaseActivity() {
Log.d(TAG, "onCreate")
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
rootEglBase = EglBase.create()
binding = CallActivityBinding.inflate(layoutInflater)
setContentView(binding!!.root)
hideNavigationIfNoPipAvailable()
@ -767,6 +765,7 @@ class CallActivity : CallBaseActivity() {
}
private fun basicInitialization() {
rootEglBase = EglBase.create()
createCameraEnumerator()
// Create a new PeerConnectionFactory instance.
@ -948,7 +947,8 @@ class CallActivity : CallBaseActivity() {
ParticipantGrid(
participantUiStates = participantUiStates,
eglBase = rootEglBase!!,
isVoiceOnlyCall = isVoiceOnlyCall
isVoiceOnlyCall = isVoiceOnlyCall,
isInPipMode = isInPipMode
) {
animateCallControls(true, 0)
}

View File

@ -19,7 +19,6 @@ import android.text.TextUtils
import android.text.format.DateUtils
import android.text.style.ImageSpan
import android.view.View
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.nextcloud.talk.R
@ -156,30 +155,6 @@ class ConversationItem(
} else {
holder.binding.userStatusImage.visibility = View.GONE
}
val dialogNameParams = holder.binding.dialogName.layoutParams as RelativeLayout.LayoutParams
val unreadBubbleParams = holder.binding.dialogUnreadBubble.layoutParams as RelativeLayout.LayoutParams
val relativeLayoutParams = holder.binding.relativeLayout.layoutParams as RelativeLayout.LayoutParams
if (model.hasSensitive == true) {
dialogNameParams.addRule(RelativeLayout.CENTER_VERTICAL)
relativeLayoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.dialogAvatarFrameLayout)
dialogNameParams.marginEnd =
context.resources.getDimensionPixelSize(R.dimen.standard_double_padding)
unreadBubbleParams.topMargin =
context.resources.getDimensionPixelSize(R.dimen.double_margin_between_elements)
unreadBubbleParams.addRule(RelativeLayout.CENTER_VERTICAL)
} else {
dialogNameParams.removeRule(RelativeLayout.CENTER_VERTICAL)
relativeLayoutParams.removeRule(RelativeLayout.ALIGN_TOP)
dialogNameParams.marginEnd = 0
unreadBubbleParams.topMargin = 0
unreadBubbleParams.removeRule(RelativeLayout.CENTER_VERTICAL)
}
holder.binding.relativeLayout.layoutParams = relativeLayoutParams
holder.binding.dialogUnreadBubble.layoutParams = unreadBubbleParams
holder.binding.dialogName.layoutParams = dialogNameParams
setLastMessage(holder, appContext)
showAvatar(holder)
}
@ -431,9 +406,9 @@ class ConversationItem(
)
return lastMessage
} else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == chatMessage?.getCalculateMessageType()) {
var attachmentName = chatMessage.text
var attachmentName = chatMessage.message
if (attachmentName == "{file}") {
attachmentName = chatMessage.messageParameters?.get("file")?.get("name") ?: ""
attachmentName = chatMessage.messageParameters?.get("file")?.get("name")
}
val author = authorName(chatMessage)

View File

@ -1,41 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.adapters.items
import android.view.View
import com.nextcloud.talk.R
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
class SpacerItem(private val height: Int) : AbstractFlexibleItem<SpacerItem.ViewHolder>() {
override fun getLayoutRes(): Int = R.layout.item_spacer
override fun createViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>?>?): ViewHolder {
return ViewHolder(view!!, adapter!!)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<*>?>?,
holder: ViewHolder,
position: Int,
payloads: MutableList<Any>?
) {
holder.itemView.layoutParams.height = height
}
override fun equals(other: Any?) = other is SpacerItem
override fun hashCode(): Int {
return 0
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter)
}

View File

@ -20,7 +20,6 @@ import autodagger.AutoInjector
import coil.load
import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
@ -158,7 +157,7 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
binding.messageEditIndicator.visibility = View.GONE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
}
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
binding.messageTime.setTextColor(ContextCompat.getColor(context, R.color.no_emphasis_text))
// parent message handling
if (!message.isDeleted && message.parentMessageId != null) {
processParentMessage(message)

View File

@ -29,7 +29,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
@ -106,6 +105,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
if (!hasCheckboxes) {
realView.isSelected = false
layoutParams.isWrapBefore = false
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
binding.messageText.visibility = View.VISIBLE
binding.checkboxContainer.visibility = View.GONE
@ -172,7 +172,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
binding.messageEditIndicator.visibility = View.GONE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
}
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
binding.messageTime.setTextColor(ContextCompat.getColor(context, R.color.no_emphasis_text))
setBubbleOnChatMessage(message)
// parent message handling
if (!message.isDeleted && message.parentMessageId != null) {
@ -185,7 +185,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
binding.checkMark.visibility = View.INVISIBLE
binding.sendingProgress.visibility = View.GONE
if (message.sendStatus == SendStatus.FAILED) {
if (message.sendingFailed) {
updateStatus(R.drawable.baseline_error_outline_24, context.resources?.getString(R.string.nc_message_failed))
} else if (message.isTemporary) {
updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending))

View File

@ -179,18 +179,6 @@ interface NcApiCoroutines {
@Url url: String
): GenericOverall
@POST
suspend fun markConversationAsImportant(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE
suspend fun markConversationAsUnimportant(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE
suspend fun removeConversationFromFavorites(
@Header("Authorization") authorization: String,

View File

@ -6,7 +6,6 @@
*/
package com.nextcloud.talk.bottomsheet.items
import android.annotation.SuppressLint
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
@ -66,7 +65,6 @@ internal class ListIconDialogAdapter<IT : ListItemWithImage>(
}
}
@SuppressLint("RestrictedApi")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemViewHolder {
val listItemView: View = parent.inflate(dialog.windowContext, R.layout.menu_item_sheet)
val viewHolder = ListItemViewHolder(

View File

@ -9,15 +9,14 @@
package com.nextcloud.talk.call.components
import android.annotation.SuppressLint
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
@ -30,7 +29,6 @@ import com.nextcloud.talk.adapters.ParticipantUiState
import org.webrtc.EglBase
import kotlin.math.ceil
@SuppressLint("UnusedBoxWithConstraintsScope")
@Suppress("LongParameterList")
@Composable
fun ParticipantGrid(
@ -38,6 +36,7 @@ fun ParticipantGrid(
eglBase: EglBase?,
participantUiStates: List<ParticipantUiState>,
isVoiceOnlyCall: Boolean,
isInPipMode: Boolean,
onClick: () -> Unit
) {
val configuration = LocalConfiguration.current
@ -45,61 +44,63 @@ fun ParticipantGrid(
val minItemHeight = 100.dp
if (participantUiStates.isEmpty()) return
val columns = if (isPortrait) {
when (participantUiStates.size) {
1, 2, 3 -> 1
else -> 2
val columns =
if (isPortrait) {
when (participantUiStates.size) {
1, 2, 3 -> 1
else -> 2
}
} else {
when (participantUiStates.size) {
1 -> 1
2, 4 -> 2
else -> 3
}
}
val rows = ceil(participantUiStates.size / columns.toFloat()).toInt()
val heightForNonGridComponents = if (isVoiceOnlyCall && !isInPipMode) {
// this is a workaround for now. It should ~summarize the height of callInfosLinearLayout and callControls
// Once everything is migrated to jetpack, this workaround should be obsolete or solved in a better way
240.dp
} else {
when (participantUiStates.size) {
1 -> 1
2, 4 -> 2
else -> 3
}
}.coerceAtLeast(1) // Prevent 0
val rows = ceil(participantUiStates.size / columns.toFloat()).toInt().coerceAtLeast(1)
0.dp
}
val gridHeight = LocalConfiguration.current.screenHeightDp.dp - heightForNonGridComponents
val itemSpacing = 8.dp
val edgePadding = 8.dp
val totalVerticalSpacing = itemSpacing * (rows - 1)
val totalVerticalPadding = edgePadding * 2
val availableHeight = gridHeight - totalVerticalSpacing - totalVerticalPadding
BoxWithConstraints(
modifier = modifier
val rawItemHeight = availableHeight / rows
val itemHeight = maxOf(rawItemHeight, minItemHeight)
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
modifier = Modifier
.fillMaxSize()
.clickable { onClick() }
.padding(horizontal = edgePadding)
.clickable { onClick() },
verticalArrangement = Arrangement.spacedBy(itemSpacing),
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
contentPadding = PaddingValues(vertical = edgePadding)
) {
val availableHeight = maxHeight
val gridAvailableHeight = availableHeight - totalVerticalSpacing - totalVerticalPadding
val rawItemHeight = gridAvailableHeight / rows
val itemHeight = maxOf(rawItemHeight, minItemHeight)
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
modifier = Modifier
.fillMaxWidth()
.height(availableHeight),
verticalArrangement = Arrangement.spacedBy(itemSpacing),
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
contentPadding = PaddingValues(vertical = edgePadding, horizontal = edgePadding)
) {
items(
participantUiStates,
key = { it.sessionKey }
) { participant ->
ParticipantTile(
participantUiState = participant,
modifier = Modifier
.height(itemHeight)
.fillMaxWidth(),
eglBase = eglBase,
isVoiceOnlyCall = isVoiceOnlyCall
)
}
items(
participantUiStates,
key = { it.sessionKey }
) { participant ->
ParticipantTile(
participantUiState = participant,
modifier = Modifier
.height(itemHeight)
.fillMaxWidth(),
eglBase = eglBase,
isVoiceOnlyCall = isVoiceOnlyCall
)
}
}
}
@ -110,7 +111,8 @@ fun ParticipantGridPreview() {
ParticipantGrid(
participantUiStates = getTestParticipants(1),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -120,7 +122,8 @@ fun TwoParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(2),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -130,7 +133,8 @@ fun ThreeParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(3),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -140,7 +144,8 @@ fun FourParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(4),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -150,7 +155,8 @@ fun FiveParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(5),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -160,7 +166,8 @@ fun SevenParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(7),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -170,7 +177,8 @@ fun FiftyParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(50),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -184,7 +192,8 @@ fun OneParticipantLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(1),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -198,7 +207,8 @@ fun TwoParticipantsLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(2),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -212,7 +222,8 @@ fun ThreeParticipantsLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(3),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -226,7 +237,8 @@ fun FourParticipantsLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(4),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -240,7 +252,8 @@ fun SevenParticipantsLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(7),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}
@ -254,7 +267,8 @@ fun FiftyParticipantsLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(50),
eglBase = null,
isVoiceOnlyCall = false
isVoiceOnlyCall = false,
isInPipMode = false
) {}
}

View File

@ -7,7 +7,6 @@
package com.nextcloud.talk.call.components
import android.annotation.SuppressLint
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
@ -41,7 +40,6 @@ const val NICK_OFFSET = 4f
const val NICK_BLUR_RADIUS = 4f
const val AVATAR_SIZE_FACTOR = 0.6f
@SuppressLint("UnusedBoxWithConstraintsScope")
@Suppress("Detekt.LongMethod")
@Composable
fun ParticipantTile(

View File

@ -16,6 +16,7 @@ package com.nextcloud.talk.chat
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@ -66,8 +67,6 @@ import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.core.text.bold
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.emoji2.text.EmojiCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.commit
@ -277,8 +276,6 @@ class ChatActivity :
lateinit var conversationInfoViewModel: ConversationInfoViewModel
lateinit var messageInputViewModel: MessageInputViewModel
private var chatMenu: Menu? = null
private val startSelectContactForResult = registerForActivityResult(
ActivityResultContracts
.StartActivityForResult()
@ -310,7 +307,12 @@ class ChatActivity :
runBlocking {
val id = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
id?.let {
startContextChatWindowForMessage(id)
val isSaved = chatViewModel.isMessageSaved(id.toLong())
if (isSaved) {
onMessageSearchResult(intent)
} else {
startContextChatWindowForMessage(id)
}
}
}
}
@ -364,7 +366,6 @@ class ChatActivity :
var startCallFromRoomSwitch: Boolean = false
var voiceOnly: Boolean = true
var focusInput: Boolean = false
private lateinit var path: String
var myFirstMessage: CharSequence? = null
@ -460,28 +461,7 @@ class ChatActivity :
binding = ActivityChatBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.chat_container)) { view, insets ->
val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val isKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val bottomPadding = if (isKeyboardVisible) imeInsets.bottom else navBarInsets.bottom
view.setPadding(
view.paddingLeft,
statusBarInsets.top,
view.paddingRight,
bottomPadding
)
WindowInsetsCompat.CONSUMED
}
} else {
colorizeStatusBar()
colorizeNavigationBar()
}
setupSystemColors()
conversationUser = currentUserProvider.currentUser.blockingGet()
handleIntent(intent)
@ -509,7 +489,7 @@ class ChatActivity :
initObservers()
pickMultipleMedia = registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(MAX_AMOUNT_MEDIA_FILE_PICKER)
ActivityResultContracts.PickMultipleVisualMedia(5)
) { uris ->
if (uris.isNotEmpty()) {
onChooseFileResult(uris)
@ -569,8 +549,6 @@ class ChatActivity :
startCallFromRoomSwitch = extras?.getBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, false) == true
voiceOnly = extras?.getBoolean(KEY_CALL_VOICE_ONLY, false) == true
focusInput = extras?.getBoolean(BundleKeys.KEY_FOCUS_INPUT) == true
}
override fun onStart() {
@ -662,17 +640,12 @@ class ChatActivity :
supportFragmentManager.commit {
setReorderingAllowed(true) // optimizes out redundant replace operations
replace(R.id.fragment_container_activity_chat, messageInputFragment)
runOnCommit {
if (focusInput) {
messageInputFragment.binding.fragmentMessageInputView.requestFocus()
}
}
}
joinRoomWithPassword()
if (conversationUser?.userId != "?" &&
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)
CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)
) {
binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() }
}
@ -707,7 +680,7 @@ class ChatActivity :
?.split("#")
?.getOrNull(1)
?.toLongOrNull()
val currentTimeStamp = (System.currentTimeMillis() / ONE_SECOND_IN_MILLIS).toLong()
val currentTimeStamp = (System.currentTimeMillis() / 1000).toLong()
val retentionPeriod = retentionOfEventRooms(spreedCapabilities)
val isPastEvent = eventEndTimeStamp?.let { it < currentTimeStamp }
if (isPastEvent == true && retentionPeriod != 0) {
@ -1129,8 +1102,6 @@ class ChatActivity :
context.getString(R.string.nc_room_retention),
Snackbar.LENGTH_LONG
).show()
chatMenu?.removeItem(R.id.conversation_event)
}
is ChatViewModel.UnbindRoomUiState.Error -> {
Snackbar.make(
@ -1266,17 +1237,11 @@ class ChatActivity :
bringToFront()
}
val deleteNoticeText = binding.conversationDeleteNotice.findViewById<TextView>(R.id.deletion_message)
viewThemeUtils.material.themeCardView(binding.conversationDeleteNotice)
deleteNoticeText.text = resources.getQuantityString(
R.plurals.nc_conversation_auto_delete_info,
retentionPeriod,
deleteNoticeText.text = String.format(
resources.getString(R.string.nc_conversation_auto_delete_notice),
retentionPeriod
)
viewThemeUtils.material.colorMaterialButtonPrimaryTonal(
binding.conversationDeleteNotice
.findViewById<MaterialButton>(R.id.keep_button)
)
if (ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!)) {
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.delete_now_button).visibility =
@ -1993,8 +1958,8 @@ class ChatActivity :
WorkManager.getInstance().enqueue(downloadWorker)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id)
.observeForever { workInfo: WorkInfo? ->
if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
.observeForever { workInfo: WorkInfo ->
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
funToCallWhenDownloadSuccessful()
}
}
@ -2077,7 +2042,7 @@ class ChatActivity :
private fun shouldShowLobby(): Boolean {
if (currentConversation != null) {
return hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
return CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
currentConversation?.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
!ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) &&
!participantPermissions.canIgnoreLobby()
@ -2332,8 +2297,15 @@ class ChatActivity :
}
}
private fun onMessageSearchResult(intent: Intent?) {
val messageId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
messageId?.let { id ->
scrollToAndCenterMessageWithId(id)
}
}
private fun executeIfResultOk(result: ActivityResult, onResult: (intent: Intent?) -> Unit) {
if (result.resultCode == RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) {
onResult(result.data)
} else {
Log.e(TAG, "resultCode for received intent was != ok")
@ -2347,7 +2319,7 @@ class ChatActivity :
if (position != null && position >= 0) {
binding.messagesListView.scrollToPosition(position)
} else {
Log.d(TAG, "message $messageId that should be scrolled to was not found (scrollToMessageWithId)")
startContextChatWindowForMessage(messageId)
}
}
@ -2360,12 +2332,10 @@ class ChatActivity :
binding.messagesListView.height / 2
)
} else {
Log.d(
TAG,
"message $messageId that should be scrolled " +
"to was not found (scrollToAndCenterMessageWithId)"
)
startContextChatWindowForMessage(messageId)
}
} ?: run {
startContextChatWindowForMessage(messageId)
}
}
@ -2827,7 +2797,7 @@ class ChatActivity :
}
if (this::spreedCapabilities.isInitialized) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
deleteExpiredMessages()
}
} else {
@ -3074,7 +3044,6 @@ class ChatActivity :
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.menu_conversation, menu)
chatMenu = menu
if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) {
eventConversationMenuItem = menu.findItem(R.id.conversation_event)
@ -3088,6 +3057,7 @@ class ChatActivity :
loadAvatarForStatusBar()
setActionBarTitle()
}
return true
}
@ -3095,7 +3065,7 @@ class ChatActivity :
super.onPrepareOptionsMenu(menu)
if (this::spreedCapabilities.isInitialized) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS)) {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS)) {
checkShowCallButtons()
}
@ -3116,7 +3086,7 @@ class ChatActivity :
}.collect()
}
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) {
Handler().post {
findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {
showCallButtonMenu(true)
@ -3175,10 +3145,10 @@ class ChatActivity :
else -> super.onOptionsItemSelected(item)
}
@SuppressLint("InflateParams")
private fun showPopupWindow(anchorView: View) {
val popupView = layoutInflater.inflate(R.layout.item_event_schedule, null)
val titleTextView = popupView.findViewById<TextView>(R.id.event_scheduled)
val subtitleTextView = popupView.findViewById<TextView>(R.id.meetingTime)
val popupWindow = PopupWindow(
@ -3627,7 +3597,7 @@ class ChatActivity :
fun copyMessage(message: IMessage?) {
val clipboardManager =
getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText(
resources?.getString(R.string.nc_app_product_name),
message?.text
@ -3941,7 +3911,7 @@ class ChatActivity :
val isOlderThanSixHours = message
.createdAt
.before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE))
val hasDeleteMessagesUnlimitedCapability = hasSpreedFeatureCapability(
val hasDeleteMessagesUnlimitedCapability = CapabilitiesUtil.hasSpreedFeatureCapability(
spreedCapabilities,
SpreedFeatures.DELETE_MESSAGES_UNLIMITED
)
@ -3951,7 +3921,7 @@ class ChatActivity :
!hasDeleteMessagesUnlimitedCapability && isOlderThanSixHours -> false
message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false
message.isDeleted -> false
!hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false
!CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false
!participantPermissions.hasChatPermission() -> false
hasDeleteMessagesUnlimitedCapability -> true
else -> true
@ -4216,6 +4186,5 @@ class ChatActivity :
const val OUT_OF_OFFICE_ALPHA = 76
const val ZERO_INDEX = 0
const val ONE_INDEX = 1
const val MAX_AMOUNT_MEDIA_FILE_PICKER = 10
}
}

View File

@ -200,7 +200,7 @@ class MessageInputFragment : Fragment() {
val connectionGained = (!wasOnline && isOnline)
Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained")
if (connectionGained) {
chatActivity.messageInputViewModel.sendUnsentMessages(
chatActivity.messageInputViewModel.sendTempMessages(
chatActivity.conversationUser!!.getCredentials(),
ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,

View File

@ -76,6 +76,8 @@ interface ChatMessageRepository : LifecycleAwareManager {
*/
suspend fun getMessage(messageId: Long, bundle: Bundle): Flow<ChatMessage>
suspend fun checkIfMessageIsSaved(messageId: Long): Boolean
@Suppress("LongParameterList")
suspend fun sendChatMessage(
credentials: String,
@ -110,7 +112,7 @@ interface ChatMessageRepository : LifecycleAwareManager {
suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow<Boolean>
suspend fun sendUnsentChatMessages(credentials: String, url: String)
suspend fun sendTempChatMessages(credentials: String, url: String)
suspend fun deleteTempMessage(chatMessage: ChatMessage)
}

View File

@ -183,26 +183,19 @@ class MediaPlayerManager : LifecycleAwareManager {
continue
}
mediaPlayer?.let { player ->
try {
if (!player.isPlaying) return@let
} catch (e: IllegalStateException) {
Log.e(TAG, "Seekbar updated during an improper state: $e")
return@let
}
val pos = player.currentPosition
if (mediaPlayer != null && mediaPlayer?.isPlaying == true) {
val pos = mediaPlayer!!.currentPosition
mediaPlayerPosition = pos
val progress = (pos.toFloat() / mediaPlayerDuration) * DIVIDER
val progressI = ceil(progress).toInt()
val seconds = (pos / ONE_SEC)
_mediaPlayerSeekBarPosition.emit(progressI)
currentCycledMessage?.let { msg ->
msg.isPlayingVoiceMessage = true
msg.voiceMessageSeekbarProgress = progressI
msg.voiceMessagePlayedSeconds = seconds
if (progressI >= IS_PLAYED_CUTOFF) msg.wasPlayedVoiceMessage = true
_mediaPlayerSeekBarPositionMsg.emit(msg)
currentCycledMessage?.let {
it.isPlayingVoiceMessage = true
it.voiceMessageSeekbarProgress = progressI
it.voiceMessagePlayedSeconds = seconds
if (progressI >= IS_PLAYED_CUTOFF) it.wasPlayedVoiceMessage = true
_mediaPlayerSeekBarPositionMsg.emit(it)
}
}

View File

@ -14,7 +14,6 @@ import android.util.Log
import com.bluelinelabs.logansquare.annotation.JsonIgnore
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
@ -120,7 +119,7 @@ data class ChatMessage(
var referenceId: String? = null,
var sendStatus: SendStatus? = null,
var sendingFailed: Boolean = true,
var silent: Boolean = false

View File

@ -19,7 +19,6 @@ import com.nextcloud.talk.data.database.mappers.asEntity
import com.nextcloud.talk.data.database.mappers.asModel
import com.nextcloud.talk.data.database.model.ChatBlockEntity
import com.nextcloud.talk.data.database.model.ChatMessageEntity
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.extensions.toIntOrZero
@ -215,8 +214,7 @@ class OfflineFirstChatRepository @Inject constructor(
)
}
// this call could be deleted when we have a worker to send messages..
sendUnsentChatMessages(credentials, urlForChatting)
sendTempChatMessages(credentials, urlForChatting)
// delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing
// with them (otherwise there is a race condition).
@ -367,18 +365,11 @@ class OfflineFirstChatRepository @Inject constructor(
lookIntoFuture: Boolean,
showUnreadMessagesMarker: Boolean
) {
receivedChatMessages.forEach {
Log.d(TAG, "receivedChatMessage: " + it.message)
}
// remove all temp messages from UI
val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId)
.first()
.map(ChatMessageEntity::asModel)
oldTempMessages.forEach {
Log.d(TAG, "oldTempMessage to be removed from UI: " + it.message)
_removeMessageFlow.emit(it)
}
oldTempMessages.forEach { _removeMessageFlow.emit(it) }
// add new messages to UI
val tripleChatMessages = Triple(lookIntoFuture, showUnreadMessagesMarker, receivedChatMessages)
@ -387,9 +378,6 @@ class OfflineFirstChatRepository @Inject constructor(
// remove temp messages from DB that are now found in the new messages
val chatMessagesReferenceIds = receivedChatMessages.mapTo(HashSet(receivedChatMessages.size)) { it.referenceId }
val tempChatMessagesThatCanBeReplaced = oldTempMessages.filter { it.referenceId in chatMessagesReferenceIds }
tempChatMessagesThatCanBeReplaced.forEach {
Log.d(TAG, "oldTempMessage that was identified in newMessages: " + it.message)
}
chatDao.deleteTempChatMessages(
internalConversationId,
tempChatMessagesThatCanBeReplaced.map { it.referenceId!! }
@ -401,10 +389,6 @@ class OfflineFirstChatRepository @Inject constructor(
.sortedBy { it.internalId }
.map(ChatMessageEntity::asModel)
remainingTempMessages.forEach {
Log.d(TAG, "remainingTempMessage: " + it.message)
}
val triple = Triple(true, false, remainingTempMessages)
_messageFlow.emit(triple)
}
@ -491,6 +475,15 @@ class OfflineFirstChatRepository @Inject constructor(
.map(ChatMessageEntity::asModel)
}
override suspend fun checkIfMessageIsSaved(messageId: Long): Boolean {
try {
chatDao.getChatMessageForConversation(internalConversationId, messageId)
return true
} catch (_: Exception) {
return false
}
}
@Suppress("UNCHECKED_CAST", "MagicNumber", "Detekt.TooGenericExceptionCaught")
private fun getMessagesFromServer(bundle: Bundle): Pair<Int, List<ChatMessageJson>>? {
val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap<String, Int>
@ -859,17 +852,6 @@ class OfflineFirstChatRepository @Inject constructor(
val chatMessageModel = response.ocs?.data?.asModel()
val sentMessage = chatDao.getTempMessageForConversation(
internalConversationId,
referenceId
).firstOrNull()
sentMessage?.let {
it.sendStatus = SendStatus.SENT_PENDING_ACK
chatDao.updateChatMessage(it)
}
Log.d(TAG, "sending chat message succeeded: " + message)
emit(Result.success(chatMessageModel))
}
.catch { e ->
@ -880,7 +862,7 @@ class OfflineFirstChatRepository @Inject constructor(
referenceId
).firstOrNull()
failedMessage?.let {
it.sendStatus = SendStatus.FAILED
it.sendingFailed = true
chatDao.updateChatMessage(it)
val failedMessageModel = it.asModel()
@ -900,28 +882,22 @@ class OfflineFirstChatRepository @Inject constructor(
sendWithoutNotification: Boolean,
referenceId: String
): Flow<Result<ChatMessage?>> {
val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).firstOrNull()
return if (messageToResend != null) {
messageToResend.sendStatus = SendStatus.PENDING
chatDao.updateChatMessage(messageToResend)
val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first()
messageToResend.sendingFailed = false
chatDao.updateChatMessage(messageToResend)
val messageToResendModel = messageToResend.asModel()
_updateMessageFlow.emit(messageToResendModel)
val messageToResendModel = messageToResend.asModel()
_updateMessageFlow.emit(messageToResendModel)
sendChatMessage(
credentials,
url,
message,
displayName,
replyTo,
sendWithoutNotification,
referenceId
)
} else {
flow {
emit(Result.failure(IllegalStateException("No temporary message found to resend")))
}
}
return sendChatMessage(
credentials,
url,
message,
displayName,
replyTo,
sendWithoutNotification,
referenceId
)
}
@Suppress("Detekt.TooGenericExceptionCaught")
@ -963,8 +939,8 @@ class OfflineFirstChatRepository @Inject constructor(
}
}
override suspend fun sendUnsentChatMessages(credentials: String, url: String) {
val tempMessages = chatDao.getTempUnsentMessagesForConversation(internalConversationId).first()
override suspend fun sendTempChatMessages(credentials: String, url: String) {
val tempMessages = chatDao.getTempMessagesForConversation(internalConversationId).first()
tempMessages.sortedBy { it.internalId }.onEach {
sendChatMessage(
credentials,
@ -1058,7 +1034,7 @@ class OfflineFirstChatRepository @Inject constructor(
actorDisplayName = currentUser.displayName!!,
referenceId = referenceId,
isTemporary = true,
sendStatus = SendStatus.PENDING,
sendingFailed = false,
silent = sendWithoutNotification
)
return entity

View File

@ -285,6 +285,10 @@ class ChatViewModel @Inject constructor(
conversationRepository.getRoom(token)
}
suspend fun isMessageSaved(messageId: Long): Boolean {
return chatRepository.checkIfMessageIsSaved(messageId)
}
fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) {
Log.d(TAG, "Remote server ${conversationModel.remoteServer}")
if (conversationModel.remoteServer.isNullOrEmpty()) {

View File

@ -169,9 +169,9 @@ class MessageInputViewModel @Inject constructor(
}
}
fun sendUnsentMessages(credentials: String, url: String) {
fun sendTempMessages(credentials: String, url: String) {
viewModelScope.launch {
chatRepository.sendUnsentChatMessages(
chatRepository.sendTempChatMessages(
credentials,
url
)

View File

@ -1,6 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@ -8,41 +9,16 @@
package com.nextcloud.talk.components
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
@Composable
fun ColoredStatusBar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
Box(modifier = Modifier.fillMaxSize()) {
Box(
Modifier
.windowInsetsTopHeight(WindowInsets.statusBars)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
)
}
} else {
ColorLegacyStatusBar()
}
}
@Composable
private fun ColorLegacyStatusBar() {
fun SetupSystemBars() {
val view = LocalView.current
val isDarkMode = isSystemInDarkTheme()
val statusBarColor = MaterialTheme.colorScheme.surface.toArgb()

View File

@ -1,24 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun VerticallyCenteredRow(content: @Composable RowScope.() -> Unit) {
Row(
modifier = Modifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically,
content = content
)
}

View File

@ -18,9 +18,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import autodagger.AutoInjector
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import com.nextcloud.talk.utils.bundle.BundleKeys
import javax.inject.Inject
@ -64,11 +64,11 @@ class ContactsActivity : BaseActivity() {
MaterialTheme(
colorScheme = colorScheme
) {
ColoredStatusBar()
ContactsScreen(
contactsViewModel = contactsViewModel,
uiState = uiState.value
)
SetupSystemBars()
}
}
}

View File

@ -11,18 +11,16 @@ package com.nextcloud.talk.contacts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nextcloud.talk.R
import com.nextcloud.talk.contacts.components.ContactsAppBar
import com.nextcloud.talk.contacts.components.AppBar
import com.nextcloud.talk.contacts.components.ContactsList
import com.nextcloud.talk.contacts.components.ContactsSearchAppBar
import com.nextcloud.talk.contacts.components.ConversationCreationOptions
@Composable
@ -31,40 +29,33 @@ fun ContactsScreen(contactsViewModel: ContactsViewModel, uiState: ContactsUiStat
val isSearchActive by contactsViewModel.isSearchActive.collectAsStateWithLifecycle()
val isAddParticipants by contactsViewModel.isAddParticipantsView.collectAsStateWithLifecycle()
val autocompleteUsers by contactsViewModel.selectedParticipantsList.collectAsStateWithLifecycle()
val enableAddButton by contactsViewModel.enableAddButton.collectAsStateWithLifecycle()
Scaffold(
modifier = Modifier
.statusBarsPadding(),
topBar = {
if (isSearchActive) {
ContactsSearchAppBar(
searchQuery = searchQuery,
onTextChange = {
contactsViewModel.updateSearchQuery(it)
contactsViewModel.getContactsFromSearchParams()
},
onCloseSearch = {
contactsViewModel.updateSearchQuery("")
contactsViewModel.setSearchActive(false)
contactsViewModel.getContactsFromSearchParams()
},
enableAddButton = enableAddButton,
isAddParticipants = isAddParticipants,
clickAddButton = { contactsViewModel.modifyClickAddButton(true) }
)
} else {
ContactsAppBar(
isAddParticipants = isAddParticipants,
autocompleteUsers = autocompleteUsers,
onStartSearch = { contactsViewModel.setSearchActive(true) }
)
}
AppBar(
title = stringResource(R.string.nc_app_product_name),
searchQuery = searchQuery,
isSearchActive = isSearchActive,
isAddParticipants = isAddParticipants,
autocompleteUsers = autocompleteUsers,
onEnableSearch = {
contactsViewModel.setSearchActive(true)
},
onDisableSearch = {
contactsViewModel.setSearchActive(false)
},
onUpdateSearchQuery = {
contactsViewModel.updateSearchQuery(query = it)
},
onUpdateAutocompleteUsers = {
contactsViewModel.getContactsFromSearchParams()
}
)
},
content = { paddingValues ->
content = {
Column(
Modifier
.padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp)
.padding(it)
.background(colorResource(id = R.color.bg_default))
) {
if (!isAddParticipants) {

View File

@ -36,15 +36,6 @@ class ContactsViewModel @Inject constructor(
private val _isAddParticipantsView = MutableStateFlow(false)
val isAddParticipantsView: StateFlow<Boolean> = _isAddParticipantsView
private val _enableAddButton = MutableStateFlow(false)
val enableAddButton: StateFlow<Boolean> = _enableAddButton
@Suppress("PropertyName")
private val _selectedContacts = MutableStateFlow<List<AutocompleteUser>>(emptyList())
@Suppress("PropertyName")
private val _clickAddButton = MutableStateFlow(false)
private var hideAlreadyAddedParticipants: Boolean = false
init {
@ -55,28 +46,14 @@ class ContactsViewModel @Inject constructor(
_searchQuery.value = query
}
fun modifyClickAddButton(value: Boolean) {
_clickAddButton.value = value
}
fun selectContact(contact: AutocompleteUser) {
val updatedParticipants = selectedParticipants.value + contact
selectedParticipants.value = updatedParticipants
_selectedContacts.value = _selectedContacts.value + contact
}
fun updateAddButtonState() {
if (_selectedContacts.value.isEmpty()) {
_enableAddButton.value = false
} else {
_enableAddButton.value = true
}
}
fun deselectContact(contact: AutocompleteUser) {
val updatedParticipants = selectedParticipants.value - contact
selectedParticipants.value = updatedParticipants
_selectedContacts.value = _selectedContacts.value - contact
}
fun updateSelectedParticipants(participants: List<AutocompleteUser>) {
@ -99,23 +76,20 @@ class ContactsViewModel @Inject constructor(
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun getContactsFromSearchParams(query: String = "") {
fun getContactsFromSearchParams() {
_contactsViewState.value = ContactsUiState.Loading
viewModelScope.launch {
try {
val contacts = repository.getContacts(
if (query != "") query else searchQuery.value,
searchQuery.value,
shareTypeList
)
val contactsList: MutableList<AutocompleteUser>? = contacts.ocs!!.data?.toMutableList()
if (hideAlreadyAddedParticipants && !_clickAddButton.value) {
if (hideAlreadyAddedParticipants) {
contactsList?.removeAll(selectedParticipants.value)
}
if (_clickAddButton.value) {
contactsList?.removeAll(selectedParticipants.value)
contactsList?.addAll(_selectedContacts.value)
}
_contactsViewState.value = ContactsUiState.Success(contactsList)
} catch (exception: Exception) {
_contactsViewState.value = ContactsUiState.Error(exception.message ?: "")

View File

@ -0,0 +1,89 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.components
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.nextcloud.talk.R
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@SuppressLint("UnrememberedMutableState")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppBar(
title: String,
searchQuery: String,
isSearchActive: Boolean,
isAddParticipants: Boolean,
autocompleteUsers: List<AutocompleteUser>,
onEnableSearch: () -> Unit,
onDisableSearch: () -> Unit,
onUpdateSearchQuery: (String) -> Unit,
onUpdateAutocompleteUsers: () -> Unit
) {
val context = LocalContext.current
TopAppBar(
title = { Text(text = title) },
navigationIcon = {
IconButton(onClick = {
(context as? Activity)?.finish()
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_button))
}
},
actions = {
IconButton(onClick = onEnableSearch) {
Icon(Icons.Filled.Search, contentDescription = stringResource(R.string.search_icon))
}
if (isAddParticipants) {
Text(
text = stringResource(id = R.string.nc_contacts_done),
modifier = Modifier.clickable {
val resultIntent = Intent().apply {
putParcelableArrayListExtra(
"selectedParticipants",
ArrayList(autocompleteUsers)
)
}
(context as? Activity)?.setResult(Activity.RESULT_OK, resultIntent)
(context as? Activity)?.finish()
}
)
}
}
)
if (isSearchActive) {
Row {
SearchComponent(
text = searchQuery,
onTextChange = { searchQuery ->
onUpdateSearchQuery(searchQuery)
onUpdateAutocompleteUsers()
},
onDisableSearch = onDisableSearch
)
}
}
}

View File

@ -65,10 +65,8 @@ fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewMod
isSelected = !isSelected
if (isSelected) {
contactsViewModel.selectContact(contact)
contactsViewModel.updateAddButtonState()
} else {
contactsViewModel.deselectContact(contact)
contactsViewModel.updateAddButtonState()
}
}
}

View File

@ -1,77 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.components
import android.app.Activity
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.nextcloud.talk.R
import com.nextcloud.talk.components.VerticallyCenteredRow
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ContactsAppBar(isAddParticipants: Boolean, autocompleteUsers: List<AutocompleteUser>, onStartSearch: () -> Unit) {
val context = LocalContext.current
TopAppBar(
modifier = Modifier
.height(60.dp),
title = {
VerticallyCenteredRow {
Text(
text = if (isAddParticipants) {
stringResource(R.string.nc_participants_add)
} else {
stringResource(R.string.nc_new_conversation)
}
)
}
},
navigationIcon = {
VerticallyCenteredRow {
IconButton(onClick = { (context as? Activity)?.finish() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_button))
}
}
},
actions = {
VerticallyCenteredRow {
IconButton(onClick = onStartSearch) {
Icon(Icons.Filled.Search, contentDescription = stringResource(R.string.search_icon))
}
if (isAddParticipants) {
Text(
text = stringResource(id = R.string.nc_contacts_done),
modifier = Modifier.clickable {
val resultIntent = Intent().apply {
putParcelableArrayListExtra("selectedParticipants", ArrayList(autocompleteUsers))
}
(context as? Activity)?.setResult(Activity.RESULT_OK, resultIntent)
(context as? Activity)?.finish()
}
)
}
}
}
)
}

View File

@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider
@ -43,13 +44,9 @@ fun ContactsItem(contacts: List<AutocompleteUser>, contactsViewModel: ContactsVi
}
LazyColumn(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
contentPadding = PaddingValues(
top = 10.dp,
bottom = 40.dp,
start = 10.dp,
end = 10.dp
),
contentPadding = PaddingValues(all = 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
groupedContacts.forEach { (initial, contactsForInitial) ->

View File

@ -1,117 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.components
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.nextcloud.talk.R
import com.nextcloud.talk.components.VerticallyCenteredRow
@Suppress("LongParameterList")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ContactsSearchAppBar(
searchQuery: String,
onTextChange: (String) -> Unit,
onCloseSearch: () -> Unit,
enableAddButton: Boolean,
isAddParticipants: Boolean,
clickAddButton: (Boolean) -> Unit
) {
val keyboardController = LocalSoftwareKeyboardController.current
Surface(
modifier = Modifier.height(60.dp)
) {
VerticallyCenteredRow {
IconButton(
modifier = Modifier.padding(start = 4.dp),
onClick = onCloseSearch
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_button)
)
}
TextField(
value = searchQuery,
onValueChange = onTextChange,
placeholder = { Text(text = stringResource(R.string.nc_search)) },
singleLine = true,
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = searchKeyboardActions(searchQuery, keyboardController),
colors = searchTextFieldColors(),
trailingIcon = {
if (searchQuery.isNotEmpty()) {
IconButton(onClick = { onTextChange("") }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.nc_search_clear)
)
}
}
}
)
if (isAddParticipants) {
TextButton(
onClick = {
onCloseSearch()
clickAddButton(true)
},
enabled = enableAddButton
) {
Text(text = stringResource(R.string.add_participants))
}
}
}
}
}
@Composable
fun searchTextFieldColors() =
TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent
)
fun searchKeyboardActions(text: String, keyboardController: SoftwareKeyboardController?) =
KeyboardActions(
onSearch = {
if (text.trim().isNotEmpty()) {
keyboardController?.hide()
}
}
)

View File

@ -0,0 +1,103 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nextcloud.talk.R
@Composable
fun SearchComponent(text: String, onTextChange: (String) -> Unit, onDisableSearch: () -> Unit) {
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth()
.height(60.dp),
value = text,
onValueChange = { onTextChange(it) },
placeholder = { Text(text = stringResource(R.string.nc_search)) },
textStyle = TextStyle(fontSize = 16.sp),
singleLine = true,
leadingIcon = { LeadingIcon(onTextChange, onDisableSearch) },
trailingIcon = { TrailingIcon(text, onTextChange) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = searchKeyboardActions(text, keyboardController),
colors = searchTextFieldColors(),
maxLines = 1
)
}
@Composable
fun searchTextFieldColors() =
TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent
)
@Composable
fun LeadingIcon(onTextChange: (String) -> Unit, onDisableSearch: () -> Unit) {
IconButton(
onClick = {
onTextChange("")
onDisableSearch()
}
) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource(R.string.back_button)
)
}
}
@Composable
fun TrailingIcon(text: String, onTextChange: (String) -> Unit) {
if (text.isNotEmpty()) {
IconButton(
onClick = { onTextChange("") }
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.close_icon)
)
}
}
}
fun searchKeyboardActions(text: String, keyboardController: SoftwareKeyboardController?) =
KeyboardActions(
onSearch = {
if (text.trim().isNotEmpty()) {
keyboardController?.hide()
}
}
)

View File

@ -84,8 +84,8 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.contacts.ContactsActivity
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.contacts.loadImage
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@ -117,6 +117,7 @@ class ConversationCreationActivity : BaseActivity() {
colorScheme = colorScheme
) {
ConversationCreationScreen(conversationCreationViewModel, context, pickImage)
SetupSystemBars()
}
}
}
@ -171,7 +172,6 @@ fun ConversationCreationScreen(
}
)
ColoredStatusBar()
Scaffold(
topBar = {
TopAppBar(
@ -191,7 +191,7 @@ fun ConversationCreationScreen(
content = { paddingValues ->
Column(
modifier = Modifier
.padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp)
.padding(paddingValues)
.background(colorResource(id = R.color.bg_default))
.fillMaxSize()
.verticalScroll(rememberScrollState())
@ -289,7 +289,7 @@ fun UploadAvatar(
}
) {
Icon(
painter = painterResource(id = R.drawable.ic_folder),
painter = painterResource(id = R.drawable.ic_mimetype_folder),
contentDescription = null,
modifier = Modifier.size(24.dp)
)

View File

@ -190,7 +190,7 @@ class ConversationInfoActivity :
binding = ActivityConversationInfoBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
viewModel =
ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
@ -252,8 +252,6 @@ class ConversationInfoActivity :
initClearChatHistoryObserver()
initMarkConversationAsSensitiveObserver()
initMarkConversationAsInsensitiveObserver()
initMarkConversationAsImportantObserver()
initMarkConversationAsUnimportantObserver()
}
private fun initMarkConversationAsSensitiveObserver() {
@ -383,47 +381,7 @@ class ConversationInfoActivity :
}
}
private fun initMarkConversationAsImportantObserver() {
viewModel.markAsImportantResult.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.MarkConversationAsImportantViewState.Success -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_mark_conversation_as_important),
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.MarkConversationAsImportantViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to mark conversation as important", uiState.exception)
}
else -> {
}
}
}
}
private fun initMarkConversationAsUnimportantObserver() {
viewModel.markAsUnimportantResult.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.MarkConversationAsUnimportantViewState.Success -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_mark_conversation_as_unimportant),
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.MarkConversationAsUnimportantViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to mark conversation as unimportant", uiState.exception)
}
else -> {
}
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
@SuppressLint("SetTextI18n")
private fun initViewStateObserver() {
viewModel.viewState.observe(this) { state ->
when (state) {
@ -444,10 +402,8 @@ class ConversationInfoActivity :
)
}
conversation?.let {
if (it.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
viewModel.getProfileData(conversationUser, it.name)
}
if (conversation!!.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
viewModel.getProfileData(conversationUser, conversation!!.name)
}
}
@ -462,48 +418,28 @@ class ConversationInfoActivity :
viewModel.getProfileViewState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.GetProfileSuccessState -> {
try {
// Pronouns
val profile = state.profile
val pronouns = profile.pronouns ?: ""
binding.pronouns.text = pronouns
val profile = state.profile
val pronouns = profile.pronouns ?: ""
binding.pronouns.text = pronouns
// Role @ Organization
val concat1 = if (profile.role != null && profile.company != null) " @ " else ""
val role = profile.role ?: ""
val company = profile.company ?: ""
val professionCompanyText = "$role$concat1$company"
binding.professionCompany.text = professionCompanyText
val concat1 = if (profile.role != null && profile.company != null) " @ " else ""
val role = profile.role ?: ""
val company = profile.company ?: ""
val professionCompanyText = "$role$concat1$company"
binding.professionCompany.text = professionCompanyText
// Local Time: xX:xX · Address
val profileZoneOffset = ZoneOffset.ofTotalSeconds(0)
val secondsToAdd = profile.timezoneOffset?.toLong() ?: 0
val localTime = ZonedDateTime.ofInstant(
Instant.now().plusSeconds(secondsToAdd),
profileZoneOffset
)
val localTimeString = localTime.format(
DateTimeFormatter
.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.getDefault())
)
val concat2 = if (profile.address != null) " · " else ""
val address = profile.address ?: ""
val localTimeLocation = "$localTimeString$concat2$address"
binding.locationTime.text = resources.getString(R.string.local_time, localTimeLocation)
val profileZoneOffset = ZoneOffset.ofTotalSeconds(0)
val secondsToAdd = profile.timezoneOffset?.toLong() ?: 0
val localTime = ZonedDateTime.ofInstant(Instant.now().plusSeconds(secondsToAdd), profileZoneOffset)
val localTimeString = localTime.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT))
val concat2 = if (profile.address != null) " · " else ""
val address = profile.address ?: ""
val localTimeLocation = "$localTimeString$concat2$address"
binding.locationTime.text = resources.getString(R.string.local_time, localTimeLocation)
binding.pronouns.visibility = VISIBLE
binding.professionCompany.visibility = if (professionCompanyText.isNotEmpty()) VISIBLE else GONE
binding.locationTime.visibility = VISIBLE
} catch (e: Exception) {
Log.e(TAG, "Exception getting profile information", e)
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
}
}
is ConversationInfoViewModel.GetProfileErrorState -> {
Log.e(TAG, "Network error occurred getting profile information")
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
binding.pronouns.visibility = VISIBLE
binding.professionCompany.visibility = if (professionCompanyText.isNotEmpty()) VISIBLE else GONE
binding.locationTime.visibility = VISIBLE
}
else -> {}
@ -568,8 +504,7 @@ class ConversationInfoActivity :
binding.guestAccessView.allowGuestsSwitch,
binding.guestAccessView.passwordProtectionSwitch,
binding.recordingConsentView.recordingConsentForConversationSwitch,
binding.lockConversationSwitch,
binding.notificationSettingsView.sensitiveConversationSwitch
binding.lockConversationSwitch
).forEach(viewThemeUtils.talk::colorSwitch)
}
}
@ -882,13 +817,16 @@ class ConversationInfoActivity :
private fun selectParticipantsToAdd() {
val bundle = Bundle()
val existingParticipants = ArrayList<AutocompleteUser>()
for (userItem in userItems) {
val user = AutocompleteUser(
userItem.model.calculatedActorId!!,
userItem.model.displayName,
userItem.model.calculatedActorType.name.lowercase()
)
existingParticipants.add(user)
if (userItem.model.calculatedActorType == USERS) {
val user = AutocompleteUser(
userItem.model.calculatedActorId!!,
userItem.model.displayName,
userItem.model.calculatedActorType.name.lowercase()
)
existingParticipants.add(user)
}
}
bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true)
@ -1065,13 +1003,21 @@ class ConversationInfoActivity :
) {
binding.addParticipantsAction.visibility = GONE
binding.startGroupChat.visibility = VISIBLE
showDeleteAllMessagesOption(conversationCopy)
} else if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) {
binding.addParticipantsAction.visibility = VISIBLE
showDeleteAllMessagesOption(conversationCopy)
if (hasSpreedFeatureCapability(
spreedCapabilities,
SpreedFeatures.CLEAR_HISTORY
) && conversationCopy.canDeleteConversation
) {
binding.clearConversationHistory.visibility = VISIBLE
} else {
binding.clearConversationHistory.visibility = GONE
}
showOptionsMenu()
} else {
binding.addParticipantsAction.visibility = GONE
if (ConversationUtils.isNoteToSelfConversation(conversation)) {
binding.notificationSettingsView.notificationSettings.visibility = VISIBLE
} else {
@ -1079,31 +1025,6 @@ class ConversationInfoActivity :
}
}
binding.notificationSettingsView.importantConversationSwitch.isChecked = conversation!!.hasImportant
binding.notificationSettingsView.notificationSettingsImportantConversation.setOnClickListener {
val isChecked = binding.notificationSettingsView.importantConversationSwitch.isChecked
binding.notificationSettingsView.importantConversationSwitch.isChecked = !isChecked
if (!isChecked) {
viewModel.markConversationAsImportant(
credentials,
conversationUser.baseUrl!!,
conversation?.token!!
)
} else {
viewModel.markConversationAsUnimportant(
credentials,
conversationUser.baseUrl!!,
conversation?.token!!
)
}
}
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.IMPORTANT_CONVERSATIONS)) {
binding.notificationSettingsView.notificationSettingsImportantConversation.visibility = VISIBLE
} else {
binding.notificationSettingsView.notificationSettingsImportantConversation.visibility = GONE
}
if (!hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.ARCHIVE_CONVERSATIONS)) {
binding.archiveConversationBtn.visibility = GONE
binding.archiveConversationTextHint.visibility = GONE
@ -1303,18 +1224,6 @@ class ConversationInfoActivity :
}
}
fun showDeleteAllMessagesOption(conversationCopy: ConversationModel) {
if (hasSpreedFeatureCapability(
spreedCapabilities,
SpreedFeatures.CLEAR_HISTORY
) && conversationCopy.canDeleteConversation
) {
binding.clearConversationHistory.visibility = VISIBLE
} else {
binding.clearConversationHistory.visibility = GONE
}
}
private fun submitRecordingConsentChanges() {
val state = if (binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked) {
RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION
@ -1847,6 +1756,13 @@ class ConversationInfoActivity :
}
private fun setUpNotificationSettings(module: DatabaseStorageModule) {
binding.notificationSettingsView.notificationSettingsImportantConversation.setOnClickListener {
val isChecked = binding.notificationSettingsView.importantConversationSwitch.isChecked
binding.notificationSettingsView.importantConversationSwitch.isChecked = !isChecked
lifecycleScope.launch {
module.saveBoolean("important_conversation_switch", !isChecked)
}
}
binding.notificationSettingsView.notificationSettingsCallNotifications.setOnClickListener {
val isChecked = binding.notificationSettingsView.callNotificationsSwitch.isChecked
binding.notificationSettingsView.callNotificationsSwitch.isChecked = !isChecked
@ -1864,6 +1780,9 @@ class ConversationInfoActivity :
}
}
binding.notificationSettingsView.importantConversationSwitch.isChecked = module
.getBoolean("important_conversation_switch", false)
if (conversation!!.remoteServer.isNullOrEmpty()) {
binding.notificationSettingsView.notificationSettingsCallNotifications.visibility = VISIBLE
binding.notificationSettingsView.callNotificationsSwitch.isChecked = module

View File

@ -124,18 +124,6 @@ class ConversationInfoViewModel @Inject constructor(
val getConversationReadOnlyState: LiveData<SetConversationReadOnlyViewState>
get() = _getConversationReadOnlyState
@Suppress("PropertyName")
private val _markConversationAsImportantResult =
MutableLiveData<MarkConversationAsImportantViewState>(MarkConversationAsImportantViewState.None)
val markAsImportantResult: LiveData<MarkConversationAsImportantViewState>
get() = _markConversationAsImportantResult
@Suppress("PropertyName")
private val _markConversationAsUnimportantResult =
MutableLiveData<MarkConversationAsUnimportantViewState>(MarkConversationAsUnimportantViewState.None)
val markAsUnimportantResult: LiveData<MarkConversationAsUnimportantViewState>
get() = _markConversationAsUnimportantResult
private val _createRoomViewState = MutableLiveData<CreateRoomUIState>(CreateRoomUIState.None)
val createRoomViewState: LiveData<CreateRoomUIState>
get() = _createRoomViewState
@ -319,19 +307,14 @@ class ConversationInfoViewModel @Inject constructor(
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun getProfileData(user: User, userId: String) {
val url = ApiUtils.getUrlForProfile(user.baseUrl!!, userId)
viewModelScope.launch {
try {
val profile = conversationsRepository.getProfile(user.getCredentials(), url)
if (profile != null) {
_getProfileViewState.value = GetProfileSuccessState(profile)
} else {
_getProfileViewState.value = GetProfileErrorState
}
} catch (e: Exception) {
Log.w(TAG, "Failed to get profile data (if not supported there wil be http405)", e)
val profile = conversationsRepository.getProfile(user.getCredentials(), url)
if (profile != null) {
_getProfileViewState.value = GetProfileSuccessState(profile)
} else {
_getProfileViewState.value = GetProfileErrorState
}
}
}
@ -373,34 +356,6 @@ class ConversationInfoViewModel @Inject constructor(
conversationsRepository.unarchiveConversation(user.getCredentials(), url)
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsImportant(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = conversationsRepository.markConversationAsImportant(credentials, baseUrl, roomToken)
_markConversationAsImportantResult.value =
MarkConversationAsImportantViewState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_markConversationAsImportantResult.value =
MarkConversationAsImportantViewState.Error(exception)
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsUnimportant(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = conversationsRepository.markConversationAsUnImportant(credentials, baseUrl, roomToken)
_markConversationAsUnimportantResult.value =
MarkConversationAsUnimportantViewState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_markConversationAsUnimportantResult.value =
MarkConversationAsUnimportantViewState.Error(exception)
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun clearChatHistory(apiVersion: Int, roomToken: String) {
viewModelScope.launch {
@ -525,16 +480,4 @@ class ConversationInfoViewModel @Inject constructor(
data object Success : PasswordUiState()
data class Error(val exception: Exception) : PasswordUiState()
}
sealed class MarkConversationAsImportantViewState {
data object None : MarkConversationAsImportantViewState()
data class Success(val statusCode: Int) : MarkConversationAsImportantViewState()
data class Error(val exception: Exception) : MarkConversationAsImportantViewState()
}
sealed class MarkConversationAsUnimportantViewState {
data object None : MarkConversationAsUnimportantViewState()
data class Success(val statusCode: Int) : MarkConversationAsUnimportantViewState()
data class Error(val exception: Exception) : MarkConversationAsUnimportantViewState()
}
}

View File

@ -99,7 +99,7 @@ class ConversationInfoEditActivity : BaseActivity() {
binding = ActivityConversationInfoEditBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
val extras: Bundle? = intent.extras

View File

@ -16,6 +16,7 @@ import android.animation.AnimatorInflater
import android.annotation.SuppressLint
import android.app.SearchManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
@ -40,9 +41,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.core.os.bundleOf
@ -81,7 +79,6 @@ import com.nextcloud.talk.adapters.items.GenericTextHeaderItem
import com.nextcloud.talk.adapters.items.LoadMoreResultsItem
import com.nextcloud.talk.adapters.items.MessageResultItem
import com.nextcloud.talk.adapters.items.MessagesTextHeaderItem
import com.nextcloud.talk.adapters.items.SpacerItem
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
@ -214,7 +211,7 @@ class ConversationsListActivity :
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
private var conversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var conversationItemsWithHeader: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var searchableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private val searchableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var filterableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var nearFutureEventConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var searchItem: MenuItem? = null
@ -238,9 +235,9 @@ class ConversationsListActivity :
private var searchViewDisposable: Disposable? = null
private var filterState =
mutableMapOf(
MENTION to false,
UNREAD to false,
ARCHIVE to false,
FilterConversationFragment.MENTION to false,
FilterConversationFragment.UNREAD to false,
FilterConversationFragment.ARCHIVE to false,
FilterConversationFragment.DEFAULT to true
)
val searchBehaviorSubject = BehaviorSubject.createDefault(false)
@ -267,7 +264,7 @@ class ConversationsListActivity :
binding = ActivityConversationsBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
viewThemeUtils.material.themeCardView(binding.searchToolbar)
viewThemeUtils.material.themeSearchBarText(binding.searchText)
@ -294,13 +291,14 @@ class ConversationsListActivity :
override fun onResume() {
super.onResume()
// actionBar?.show()
if (adapter == null) {
adapter = FlexibleAdapter(conversationItems, this, true)
addEmptyItemForEdgeToEdgeIfNecessary()
} else {
binding.loadingContent.visibility = View.GONE
}
adapter?.addListener(this)
adapter!!.addListener(this)
prepareViews()
showNotificationWarning()
@ -316,10 +314,8 @@ class ConversationsListActivity :
showServerEOLDialog()
return
}
currentUser?.capabilities?.spreedCapability?.let { spreedCapabilities ->
if (isUnifiedSearchAvailable(spreedCapabilities)) {
searchHelper = MessageSearchHelper(unifiedSearchRepository)
}
if (isUnifiedSearchAvailable(currentUser!!.capabilities!!.spreedCapability!!)) {
searchHelper = MessageSearchHelper(unifiedSearchRepository)
}
credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
@ -344,14 +340,6 @@ class ConversationsListActivity :
showSearchOrToolbar()
}
// if edge to edge is used, add an empty item at the bottom of the list
@Suppress("MagicNumber")
private fun addEmptyItemForEdgeToEdgeIfNecessary() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
adapter?.addScrollableFooter(SpacerItem(200))
}
}
@Suppress("LongMethod")
private fun initObservers() {
this.lifecycleScope.launch {
@ -421,10 +409,6 @@ class ConversationsListActivity :
conversationsListViewModel.getRoomsFlow
.onEach { list ->
setConversationList(list)
val noteToSelf = list
.firstOrNull { ConversationUtils.isNoteToSelfConversation(it) }
val isNoteToSelfAvailable = noteToSelf != null
handleNoteToSelfShortcut(isNoteToSelfAvailable, noteToSelf?.token ?: "")
}.collect()
}
@ -477,13 +461,7 @@ class ConversationsListActivity :
userItems.add(contactItem)
}
val list = searchableConversationItems.filter {
it !is ContactItem
}.toMutableList()
list.addAll(userItems)
searchableConversationItems = list
searchableConversationItems.addAll(userItems)
}
else -> {}
@ -541,29 +519,6 @@ class ConversationsListActivity :
}
}
private fun handleNoteToSelfShortcut(noteToSelfAvailable: Boolean, noteToSelfToken: String) {
if (noteToSelfAvailable) {
val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, noteToSelfToken)
bundle.putBoolean(BundleKeys.KEY_FOCUS_INPUT, true)
val intent = Intent(context, ChatActivity::class.java)
intent.putExtras(bundle)
intent.action = Intent.ACTION_VIEW
val openNotesString = resources.getString(R.string.open_notes)
val shortcut = ShortcutInfoCompat.Builder(context, NOTE_TO_SELF_SHORTCUT_ID)
.setShortLabel(openNotesString)
.setLongLabel(openNotesString)
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_pencil_grey600_24dp))
.setIntent(intent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
} else {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(NOTE_TO_SELF_SHORTCUT_ID))
}
}
private fun setConversationList(list: List<ConversationModel>) {
// Update Conversations
conversationItems.clear()
@ -571,7 +526,7 @@ class ConversationsListActivity :
nearFutureEventConversationItems.clear()
for (conversation in list) {
if (!isFutureEvent(conversation) && !conversation.hasArchived) {
if (!isFutureEvent(conversation)) {
addToNearFutureEventConversationItems(conversation)
}
addToConversationItems(conversation)
@ -602,6 +557,9 @@ class ConversationsListActivity :
intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)
)
fetchOpenConversations(apiVersion)
// Get users
fetchUsers()
}
fun applyFilter() {
@ -679,7 +637,7 @@ class ConversationsListActivity :
}
}
val archiveFilterOn = filterState[ARCHIVE] == true
val archiveFilterOn = filterState[FilterConversationFragment.ARCHIVE] ?: false
if (archiveFilterOn && newItems.isEmpty()) {
binding.noArchivedConversationLayout.visibility = View.VISIBLE
} else {
@ -701,7 +659,7 @@ class ConversationsListActivity :
for ((k, v) in filterState) {
if (v) {
when (k) {
MENTION -> result = (result && conversation.unreadMention) ||
FilterConversationFragment.MENTION -> result = (result && conversation.unreadMention) ||
(
result &&
(
@ -711,10 +669,10 @@ class ConversationsListActivity :
(conversation.unreadMessages > 0)
)
UNREAD -> result = result && (conversation.unreadMessages > 0)
FilterConversationFragment.UNREAD -> result = result && (conversation.unreadMessages > 0)
FilterConversationFragment.DEFAULT -> {
result = if (filterState[ARCHIVE] == true) {
result = if (filterState[FilterConversationFragment.ARCHIVE] == true) {
result && conversation.hasArchived
} else {
result && !conversation.hasArchived
@ -794,7 +752,7 @@ class ConversationsListActivity :
}
private fun initSearchView() {
val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager?
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
if (searchItem != null) {
searchView = MenuItemCompat.getActionView(searchItem) as SearchView
viewThemeUtils.talk.themeSearchView(searchView!!)
@ -973,7 +931,8 @@ class ConversationsListActivity :
} else {
showToolbar()
}
initSystemBars()
colorizeStatusBar()
colorizeNavigationBar()
}
}
@ -1209,8 +1168,8 @@ class ConversationsListActivity :
}
}
private fun fetchUsers(query: String = "") {
contactsViewModel.getContactsFromSearchParams(query)
private fun fetchUsers() {
contactsViewModel.getContactsFromSearchParams()
}
private fun handleHttpExceptions(throwable: Throwable) {
@ -1255,7 +1214,7 @@ class ConversationsListActivity :
})
binding.recyclerView.setOnTouchListener { v: View, _: MotionEvent? ->
if (!isDestroyed) {
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v.windowToken, 0)
}
false
@ -1410,15 +1369,12 @@ class ConversationsListActivity :
clearMessageSearchResults()
binding.noArchivedConversationLayout.visibility = View.GONE
fetchUsers(filter)
if (hasFilterEnabled()) {
adapter?.updateDataSet(conversationItems)
adapter?.setFilter(filter)
adapter?.filterItems()
adapter?.updateDataSet(filterableConversationItems)
} else {
adapter?.updateDataSet(searchableConversationItems)
adapter?.setFilter(filter)
adapter?.filterItems()
}
@ -1433,10 +1389,9 @@ class ConversationsListActivity :
private fun resetSearchResults() {
clearMessageSearchResults()
adapter?.updateDataSet(conversationItems)
adapter?.setFilter("")
adapter?.filterItems()
val archiveFilterOn = filterState[ARCHIVE] == true
val archiveFilterOn = filterState[FilterConversationFragment.ARCHIVE] ?: false
if (archiveFilterOn && adapter!!.isEmpty) {
binding.noArchivedConversationLayout.visibility = View.VISIBLE
} else {
@ -1847,7 +1802,7 @@ class ConversationsListActivity :
val callsChannelNotEnabled = !NotificationUtils.isCallsNotificationChannelEnabled(this)
val serverNotificationAppInstalled =
currentUser?.capabilities?.notificationsCapability?.features?.isNotEmpty() == true
currentUser?.capabilities?.notificationsCapability?.features?.isNotEmpty() ?: false
val settingsOfUserAreWrong = notificationPermissionNotGranted ||
batteryOptimizationNotIgnored ||
@ -1976,9 +1931,9 @@ class ConversationsListActivity :
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
.observeForever { workInfo: WorkInfo? ->
.observeForever { workInfo: WorkInfo ->
when (workInfo?.state) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
val text = String.format(
context.resources.getString(R.string.nc_deleted_user),
@ -2179,8 +2134,8 @@ class ConversationsListActivity :
}
fun updateFilterState(mention: Boolean, unread: Boolean) {
filterState[MENTION] = mention
filterState[UNREAD] = unread
filterState[FilterConversationFragment.MENTION] = mention
filterState[FilterConversationFragment.UNREAD] = unread
}
fun setFilterableItems(items: MutableList<AbstractFlexibleItem<*>>) {
@ -2221,6 +2176,5 @@ class ConversationsListActivity :
const val ROOM_TYPE_ONE_ONE = "1"
private const val SIXTEEN_HOURS_IN_SECONDS: Long = 57600
const val LONG_1000: Long = 1000
private const val NOTE_TO_SELF_SHORTCUT_ID = "NOTE_TO_SELF_SHORTCUT_ID"
}
}

View File

@ -50,18 +50,6 @@ interface ChatMessagesDao {
)
fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>>
@Query(
"""
SELECT *
FROM ChatMessages
WHERE internalConversationId = :internalConversationId
AND isTemporary = 1
AND sendStatus != 'SENT_PENDING_ACK'
ORDER BY timestamp DESC, id DESC
"""
)
fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>>
@Query(
"""
SELECT *
@ -72,7 +60,7 @@ interface ChatMessagesDao {
ORDER BY timestamp DESC, id DESC
"""
)
fun getTempMessageForConversation(internalConversationId: String, referenceId: String): Flow<ChatMessageEntity?>
fun getTempMessageForConversation(internalConversationId: String, referenceId: String): Flow<ChatMessageEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>)

View File

@ -68,7 +68,7 @@ fun ChatMessageEntity.asModel() =
isDeleted = deleted,
referenceId = referenceId,
isTemporary = isTemporary,
sendStatus = sendStatus,
sendingFailed = sendingFailed,
readStatus = ReadStatus.NONE,
silent = silent
)

View File

@ -62,8 +62,7 @@ fun ConversationModel.asEntity() =
remoteServer = remoteServer,
remoteToken = remoteToken,
hasArchived = hasArchived,
hasSensitive = hasSensitive,
hasImportant = hasImportant
hasSensitive = hasSensitive
)
fun ConversationEntity.asModel() =
@ -116,8 +115,7 @@ fun ConversationEntity.asModel() =
remoteServer = remoteServer,
remoteToken = remoteToken,
hasArchived = hasArchived,
hasSensitive = hasSensitive,
hasImportant = hasImportant
hasSensitive = hasSensitive
)
fun Conversation.asEntity(accountId: Long) =
@ -169,6 +167,5 @@ fun Conversation.asEntity(accountId: Long) =
remoteServer = remoteServer,
remoteToken = remoteToken,
hasArchived = hasArchived,
hasSensitive = hasSensitive,
hasImportant = hasImportant
hasSensitive = hasSensitive
)

View File

@ -64,7 +64,7 @@ data class ChatMessageEntity(
@ColumnInfo(name = "reactions") var reactions: LinkedHashMap<String, Int>? = null,
@ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList<String>? = null,
@ColumnInfo(name = "referenceId") var referenceId: String? = null,
@ColumnInfo(name = "sendStatus") var sendStatus: SendStatus? = null,
@ColumnInfo(name = "sendingFailed") var sendingFailed: Boolean = false,
@ColumnInfo(name = "silent") var silent: Boolean = false,
@ColumnInfo(name = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType,
@ColumnInfo(name = "timestamp") var timestamp: Long = 0

View File

@ -95,8 +95,7 @@ data class ConversationEntity(
@ColumnInfo(name = "unreadMentionDirect") var unreadMentionDirect: Boolean,
@ColumnInfo(name = "unreadMessages") var unreadMessages: Int = 0,
@ColumnInfo(name = "hasArchived") var hasArchived: Boolean = false,
@ColumnInfo(name = "hasSensitive") var hasSensitive: Boolean = false,
@ColumnInfo(name = "hasImportant") var hasImportant: Boolean = false
@ColumnInfo(name = "hasSensitive") var hasSensitive: Boolean = false
// missing/not needed: attendeeId
// missing/not needed: attendeePin
// missing/not needed: attendeePermissions

View File

@ -1,14 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.data.database.model
enum class SendStatus {
PENDING,
SENT_PENDING_ACK,
FAILED
}

View File

@ -1,31 +1,18 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024-2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.data.source.local
import android.util.Log
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import java.sql.SQLException
@Suppress("MagicNumber")
object Migrations {
//region Auto migrations
@DeleteColumn(tableName = "ChatMessages", columnName = "sendingFailed")
class AutoMigration16To17 : AutoMigrationSpec
//endregion
//region Manual migrations
val MIGRATION_6_8 = object : Migration(6, 8) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 6 to 8")
@ -78,19 +65,10 @@ object Migrations {
val MIGRATION_14_15 = object : Migration(14, 15) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 14 to 15")
addIsSensitive(db)
addisSensitive(db)
}
}
val MIGRATION_15_16 = object : Migration(15, 16) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 15 to 16")
addIsImportant(db)
}
}
//endregion
fun migrateToRoom(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE User_new (" +
@ -312,7 +290,7 @@ object Migrations {
}
}
fun addIsSensitive(db: SupportSQLiteDatabase) {
fun addisSensitive(db: SupportSQLiteDatabase) {
try {
db.execSQL(
"ALTER TABLE Conversations " +
@ -323,17 +301,6 @@ object Migrations {
}
}
fun addIsImportant(db: SupportSQLiteDatabase) {
try {
db.execSQL(
"ALTER TABLE Conversations " +
"ADD COLUMN hasImportant INTEGER NOT NULL DEFAULT 0;"
)
} catch (e: SQLException) {
Log.i("Migrations", "Something went wrong when adding column hasImportant to table Conversations")
}
}
fun addTempMessagesSupport(db: SupportSQLiteDatabase) {
try {
db.execSQL(

View File

@ -1,7 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023-2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2023-2024 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2020 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later
@ -23,14 +23,12 @@ import com.nextcloud.talk.data.database.dao.ConversationsDao
import com.nextcloud.talk.data.database.model.ChatBlockEntity
import com.nextcloud.talk.data.database.model.ChatMessageEntity
import com.nextcloud.talk.data.database.model.ConversationEntity
import com.nextcloud.talk.data.source.local.Migrations.AutoMigration16To17
import com.nextcloud.talk.data.source.local.converters.ArrayListConverter
import com.nextcloud.talk.data.source.local.converters.CapabilitiesConverter
import com.nextcloud.talk.data.source.local.converters.ExternalSignalingServerConverter
import com.nextcloud.talk.data.source.local.converters.HashMapHashMapConverter
import com.nextcloud.talk.data.source.local.converters.LinkedHashMapConverter
import com.nextcloud.talk.data.source.local.converters.PushConfigurationConverter
import com.nextcloud.talk.data.source.local.converters.SendStatusConverter
import com.nextcloud.talk.data.source.local.converters.ServerVersionConverter
import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter
import com.nextcloud.talk.data.storage.ArbitraryStoragesDao
@ -51,10 +49,9 @@ import java.util.Locale
ChatMessageEntity::class,
ChatBlockEntity::class
],
version = 17,
version = 15,
autoMigrations = [
AutoMigration(from = 9, to = 10),
AutoMigration(from = 16, to = 17, spec = AutoMigration16To17::class)
AutoMigration(from = 9, to = 10)
],
exportSchema = true
)
@ -66,8 +63,7 @@ import java.util.Locale
SignalingSettingsConverter::class,
HashMapHashMapConverter::class,
LinkedHashMapConverter::class,
ArrayListConverter::class,
SendStatusConverter::class
ArrayListConverter::class
)
abstract class TalkDatabase : RoomDatabase() {
@ -121,8 +117,7 @@ abstract class TalkDatabase : RoomDatabase() {
Migrations.MIGRATION_11_12,
Migrations.MIGRATION_12_13,
Migrations.MIGRATION_13_14,
Migrations.MIGRATION_14_15,
Migrations.MIGRATION_15_16
Migrations.MIGRATION_14_15
)
.allowMainThreadQueries()
.addCallback(

View File

@ -1,23 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.data.source.local.converters
import androidx.room.TypeConverter
import com.nextcloud.talk.data.database.model.SendStatus
class SendStatusConverter {
@TypeConverter
fun fromStatus(value: SendStatus): String {
return value.name
}
@TypeConverter
fun toStatus(value: String): SendStatus {
return SendStatus.valueOf(value)
}
}

View File

@ -21,7 +21,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.collectAsState
@ -29,9 +28,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.ViewModelProvider
import androidx.core.net.toUri
import autodagger.AutoInjector
import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R
@ -39,8 +37,8 @@ import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.components.StandardAppBar
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.BrandingUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl
@ -111,22 +109,18 @@ class DiagnoseActivity : BaseActivity() {
MaterialTheme(
colorScheme = colorScheme
) {
ColoredStatusBar()
Scaffold(
modifier = Modifier
.statusBarsPadding(),
topBar = {
StandardAppBar(
title = stringResource(R.string.nc_settings_diagnose_title),
menuItems
)
},
content = { paddingValues ->
content = {
val viewState = diagnoseViewModel.notificationViewState.collectAsState().value
Column(
Modifier
.padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp)
.padding(it)
.background(backgroundColor)
.fillMaxSize()
) {
@ -142,6 +136,7 @@ class DiagnoseActivity : BaseActivity() {
}
}
)
SetupSystemBars()
}
}
}

View File

@ -65,12 +65,7 @@ fun DiagnoseContentComposable(
Column(
modifier = Modifier
.fillMaxSize()
.padding(
start = 16.dp,
top = 0.dp,
end = 16.dp,
bottom = 0.dp
)
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
data.value.forEach { element ->
@ -105,7 +100,6 @@ fun DiagnoseContentComposable(
ShowTestPushButton(onTestPushClick)
}
ShowNotificationData(isLoading, showDialog, context, viewState, onDismissDialog)
Spacer(modifier = Modifier.height(40.dp))
}
}

View File

@ -190,7 +190,6 @@ class FullScreenMediaActivity : AppCompatActivity() {
supportActionBar?.show()
}
@OptIn(UnstableApi::class)
private fun applyWindowInsets() {
val playerView = binding.playerView
val exoControls = playerView.findViewById<FrameLayout>(R.id.exo_bottom_bar)

View File

@ -65,7 +65,7 @@ class InvitationsActivity : BaseActivity() {
binding = ActivityInvitationsBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
adapter = InvitationsAdapter(currentUser) { invitation, action ->
handleInvitation(invitation, action)

View File

@ -67,7 +67,7 @@ class GeocodingActivity :
binding = ActivityGeocodingBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))

View File

@ -129,7 +129,7 @@ class LocationPickerActivity :
binding = ActivityLocationBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))

View File

@ -70,7 +70,7 @@ class MessageSearchActivity : BaseActivity() {
binding = ActivityMessageSearchBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
viewModel = ViewModelProvider(this, viewModelFactory)[MessageSearchViewModel::class.java]
user = currentUserProvider.currentUser.blockingGet()

View File

@ -62,10 +62,10 @@ class ConversationModel(
var remoteToken: String? = null,
var hasArchived: Boolean = false,
var hasSensitive: Boolean = false,
var hasImportant: Boolean = false,
// attributes that don't come from API. This should be changed?!
var password: String? = null
) {
companion object {
@ -128,8 +128,7 @@ class ConversationModel(
remoteServer = conversation.remoteServer,
remoteToken = conversation.remoteToken,
hasArchived = conversation.hasArchived,
hasSensitive = conversation.hasSensitive,
hasImportant = conversation.hasImportant
hasSensitive = conversation.hasSensitive
)
}
}

View File

@ -21,7 +21,7 @@ class ChatUtils {
return message
}
@Suppress("Detekt.ComplexMethod", "Detekt.ComplexCondition")
@Suppress("Detekt.ComplexMethod")
private fun parse(messageParameters: HashMap<String?, HashMap<String?, String?>>, message: String?): String? {
var resultMessage = message
for (key in messageParameters.keys) {
@ -29,9 +29,7 @@ class ChatUtils {
if (individualHashMap != null) {
val type = individualHashMap["type"]
resultMessage = if (type == "user" || type == "guest" || type == "call" || type == "email" ||
type == "user-group" || type == "circle"
) {
resultMessage = if (type == "user" || type == "guest" || type == "call" || type == "email") {
resultMessage?.replace("{$key}", "@" + individualHashMap["name"])
} else if (type == "geo-location") {
individualHashMap["name"]

View File

@ -168,8 +168,5 @@ data class Conversation(
var hasArchived: Boolean = false,
@JsonField(name = ["isSensitive"])
var hasSensitive: Boolean = false,
@JsonField(name = ["isImportant"])
var hasImportant: Boolean = false
var hasSensitive: Boolean = false
) : Parcelable

View File

@ -57,7 +57,7 @@ class ListOpenConversationsActivity : BaseActivity() {
binding = ActivityOpenConversationsBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
viewThemeUtils.platform.colorImageView(binding.searchOpenConversations, ColorRole.ON_SURFACE)
viewThemeUtils.material.colorTextInputLayout(binding.textInputLayout)

View File

@ -119,7 +119,7 @@ class ProfileActivity : BaseActivity() {
binding = ActivityProfileBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
}
override fun onResume() {

View File

@ -6,17 +6,14 @@
*/
package com.nextcloud.talk.receivers
import android.Manifest
import android.app.Notification
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
@ -165,15 +162,9 @@ class DirectReplyReceiver : BroadcastReceiver() {
// Set the updated style
previousBuilder.setStyle(previousStyle)
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
// Check if notification still exists
if (findActiveNotification(systemNotificationId!!) != null) {
NotificationManagerCompat.from(context).notify(systemNotificationId!!, previousBuilder.build())
}
// Check if notification still exists
if (findActiveNotification(systemNotificationId!!) != null) {
NotificationManagerCompat.from(context).notify(systemNotificationId!!, previousBuilder.build())
}
}
.subscribeOn(Schedulers.io())

View File

@ -53,8 +53,4 @@ interface ConversationsRepository {
suspend fun markConversationAsSensitive(credentials: String, baseUrl: String, roomToken: String): GenericOverall
suspend fun markConversationAsInsensitive(credentials: String, baseUrl: String, roomToken: String): GenericOverall
suspend fun markConversationAsImportant(credentials: String, baseUrl: String, roomToken: String): GenericOverall
suspend fun markConversationAsUnImportant(credentials: String, baseUrl: String, roomToken: String): GenericOverall
}

View File

@ -139,24 +139,6 @@ class ConversationsRepositoryImpl(
return coroutineApi.markConversationAsInsensitive(credentials, url)
}
override suspend fun markConversationAsImportant(
credentials: String,
baseUrl: String,
roomToken: String
): GenericOverall {
val url = ApiUtils.getUrlForImportantConversation(baseUrl, roomToken)
return coroutineApi.markConversationAsImportant(credentials, url)
}
override suspend fun markConversationAsUnImportant(
credentials: String,
baseUrl: String,
roomToken: String
): GenericOverall {
val url = ApiUtils.getUrlForImportantConversation(baseUrl, roomToken)
return coroutineApi.markConversationAsUnimportant(credentials, url)
}
override suspend fun banActor(
credentials: String,
url: String,

View File

@ -145,7 +145,7 @@ class SettingsActivity :
binding = ActivitySettingsBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
binding.avatarImage.let { ViewCompat.setTransitionName(it, "userAvatar.transitionTag") }
@ -681,9 +681,9 @@ class SettingsActivity :
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
.observeForever { workInfo: WorkInfo? ->
.observeForever { workInfo: WorkInfo ->
when (workInfo?.state) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
val text = String.format(
context.resources.getString(R.string.nc_deleted_user),

View File

@ -13,6 +13,7 @@ import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.view.View
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
@ -27,6 +28,7 @@ import com.nextcloud.talk.databinding.ActivitySharedItemsBinding
import com.nextcloud.talk.shareditems.adapters.SharedItemsAdapter
import com.nextcloud.talk.shareditems.model.SharedItemType
import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import javax.inject.Inject
@ -55,11 +57,15 @@ class SharedItemsActivity : BaseActivity() {
setSupportActionBar(binding.sharedItemsToolbar)
setContentView(binding.root)
initSystemBars()
viewThemeUtils.platform.themeStatusBar(this)
viewThemeUtils.material.themeToolbar(binding.sharedItemsToolbar)
viewThemeUtils.material.themeTabLayoutOnSurface(binding.sharedItemsTabs)
DisplayUtils.applyColorToNavigationBar(
this.window,
ResourcesCompat.getColor(resources, R.color.bg_default, null)
)
supportActionBar?.title = conversationName
supportActionBar?.setDisplayHomeAsUpEnabled(true)

View File

@ -86,7 +86,7 @@ class TranslateActivity : BaseActivity() {
}
setupActionBar()
setContentView(binding.root)
initSystemBars()
setupSystemColors()
setupTextViews()
viewModel.getLanguages()
setupCopyButton()

View File

@ -118,6 +118,7 @@ class ConversationsListBottomDialog(
currentUser.capabilities?.spreedCapability!!,
SpreedFeatures.FAVORITES
)
val canModerate = ConversationUtils.canModerate(conversation, currentUser.capabilities?.spreedCapability!!)
binding.conversationRemoveFromFavorites.visibility = setVisibleIf(
hasFavoritesCapability && conversation.favorite
@ -148,11 +149,14 @@ class ConversationsListBottomDialog(
)
binding.conversationOperationDelete.visibility = setVisibleIf(
conversation.canDeleteConversation
canModerate
)
binding.conversationOperationLeave.visibility = setVisibleIf(
conversation.canLeaveConversation
conversation.canLeaveConversation &&
// leaving is by api not possible for the last user with moderator permissions.
// for now, hide this option for all moderators.
!ConversationUtils.canModerate(conversation, currentUser.capabilities!!.spreedCapability!!)
)
}

View File

@ -18,7 +18,6 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
@ -59,10 +58,9 @@ class TempMessageActionsDialog(
private fun initMenuItems() {
this.lifecycleScope.launch {
val sendingFailed = message.sendStatus == SendStatus.FAILED
initResendMessage(sendingFailed && networkMonitor.isOnline.value)
initMenuEditMessage(sendingFailed || !networkMonitor.isOnline.value)
initMenuDeleteMessage(sendingFailed || !networkMonitor.isOnline.value)
initResendMessage(message.sendingFailed && networkMonitor.isOnline.value)
initMenuEditMessage(message.sendingFailed || !networkMonitor.isOnline.value)
initMenuDeleteMessage(message.sendingFailed || !networkMonitor.isOnline.value)
initMenuItemCopy()
}
}

View File

@ -205,10 +205,6 @@ object ApiUtils {
return getUrlForParticipants(version, baseUrl, token) + "/active"
}
fun getUrlForImportantConversation(baseUrl: String, roomToken: String): String {
return "$baseUrl$OCS_API_VERSION/apps/spreed/api/v4/room/$roomToken/important"
}
@JvmStatic
fun getUrlForParticipantsSelf(version: Int, baseUrl: String?, token: String?): String {
return getUrlForParticipants(version, baseUrl, token) + "/self"

View File

@ -1,38 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.utils
import android.graphics.Color
import android.os.Build
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
/**
* This method is similar to "adjustUIForAPILevel35" in
* AppCompatActivityExtensions.kt in https://github.com/nextcloud/android-common/
* Only window.addSystemBarPaddings() had to be removed. This could be unified again at some point.
*/
@JvmOverloads
fun AppCompatActivity.adjustUIForAPILevel35(
statusBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
// It may make sense to change navigationBarStyle to "SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)"
// For now, it is set to "light" to have a fully transparent navigation bar to align with the XML screens.
// It may be wanted to have a semi transparent navigation bar in the future. Then set it to "auto" and try to
// migrate the XML screens to Compose (having semi transparent navigation bar for XML did not work out. In
// general, supporting both XML and Compose system bar handling is a pain and we will have it easier without XML)
// So in short: migrate all screens to Compose. Then it's easier to decide if navigation bar should be semi
// transparent or not for all screens.
navigationBarStyle: SystemBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT)
) {
val isApiLevel35OrHigher = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM)
if (!isApiLevel35OrHigher) {
return
}
enableEdgeToEdge(statusBarStyle, navigationBarStyle)
}

View File

@ -59,8 +59,7 @@ enum class SpreedFeatures(val value: String) {
ARCHIVE_CONVERSATIONS("archived-conversations-v2"),
CONVERSATION_CREATION_ALL("conversation-creation-all"),
UNBIND_CONVERSATION("unbind-conversation"),
SENSITIVE_CONVERSATIONS("sensitive-conversations"),
IMPORTANT_CONVERSATIONS("important-conversations")
SENSITIVE_CONVERSATIONS("sensitive-conversations")
}
@Suppress("TooManyFunctions")

View File

@ -81,5 +81,4 @@ object BundleKeys {
const val KEY_FIELD_MAP: String = "KEY_FIELD_MAP"
const val KEY_CHAT_URL: String = "KEY_CHAT_URL"
const val KEY_SCROLL_TO_NOTIFICATION_CATEGORY: String = "KEY_SCROLL_TO_NOTIFICATION_CATEGORY"
const val KEY_FOCUS_INPUT: String = "KEY_FOCUS_INPUT"
}

View File

@ -30,11 +30,6 @@ class DummyChatMessagesDaoImpl : ChatMessagesDao {
override fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> =
flowOf()
override fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> {
// nothing to return here as long this class is only used for the Search window
return flowOf()
}
override fun getTempMessageForConversation(
internalConversationId: String,
referenceId: String

View File

@ -1,7 +1,7 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
~ SPDX-FileCopyrightText: 2018-2024 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
@ -11,6 +11,5 @@
android:viewportHeight="960">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M280,920Q247,920 223.5,896.5Q200,873 200,840L200,120Q200,87 223.5,63.5Q247,40 280,40L680,40Q713,40 736.5,63.5Q760,87 760,120L760,244Q778,251 789,266Q800,281 800,300L800,380Q800,399 789,414Q778,429 760,436L760,840Q760,873 736.5,896.5Q713,920 680,920L280,920ZM280,840L680,840Q680,840 680,840Q680,840 680,840L680,120Q680,120 680,120Q680,120 680,120L280,120Q280,120 280,120Q280,120 280,120L280,840Q280,840 280,840Q280,840 280,840ZM280,840Q280,840 280,840Q280,840 280,840L280,120Q280,120 280,120Q280,120 280,120L280,120Q280,120 280,120Q280,120 280,120L280,840Q280,840 280,840Q280,840 280,840ZM480,240Q497,240 508.5,228.5Q520,217 520,200Q520,183 508.5,171.5Q497,160 480,160Q463,160 451.5,171.5Q440,183 440,200Q440,217 451.5,228.5Q463,240 480,240Z" />
android:pathData="M280,920Q247,920 223.5,896.5Q200,873 200,840L200,120Q200,87 223.5,63.5Q247,40 280,40L680,40Q713,40 736.5,63.5Q760,87 760,120L760,840Q760,873 736.5,896.5Q713,920 680,920L280,920ZM280,720L680,720L680,240L280,240L280,720Z" />
</vector>

View File

@ -1,7 +1,7 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
~ SPDX-FileCopyrightText: 2018-2024 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
@ -12,5 +12,5 @@
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M40,800L40,688Q40,654 57.5,625.5Q75,597 104,582Q166,551 230,535.5Q294,520 360,520Q426,520 490,535.5Q554,551 616,582Q645,597 662.5,625.5Q680,654 680,688L680,800L40,800ZM760,800L760,680Q760,636 735.5,595.5Q711,555 666,526Q717,532 762,546.5Q807,561 846,582Q882,602 901,626.5Q920,651 920,680L920,800L760,800ZM360,480Q294,480 247,433Q200,386 200,320Q200,254 247,207Q294,160 360,160Q426,160 473,207Q520,254 520,320Q520,386 473,433Q426,480 360,480ZM760,320Q760,386 713,433Q666,480 600,480Q589,480 572,477.5Q555,475 544,472Q571,440 585.5,401Q600,362 600,320Q600,278 585.5,239Q571,200 544,168Q558,163 572,161.5Q586,160 600,160Q666,160 713,207Q760,254 760,320ZM120,720L600,720L600,688Q600,677 594.5,668Q589,659 580,654Q526,627 471,613.5Q416,600 360,600Q304,600 249,613.5Q194,627 140,654Q131,659 125.5,668Q120,677 120,688L120,720ZM360,400Q393,400 416.5,376.5Q440,353 440,320Q440,287 416.5,263.5Q393,240 360,240Q327,240 303.5,263.5Q280,287 280,320Q280,353 303.5,376.5Q327,400 360,400ZM360,720L360,720L360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720ZM360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Z" />
android:pathData="M-19,861L-19,718Q-19,677 0.69,644.15Q20.38,611.29 53,594Q119,560 189.27,543Q259.54,526 332,526Q404.12,526 474.06,543Q544,560 610,594Q642.63,611.29 662.31,644.15Q682,677 682,718L682,861L-19,861ZM767,861L767,706Q767,664 744,614Q721,564 672,529Q735,535 790.5,551.5Q846,568 893,594Q934.84,617.77 956.92,646.88Q979,676 979,706L979,861L767,861ZM331,473Q253.45,473 198.23,417.78Q143,362.55 143,285Q143,207.45 198.23,152.22Q253.45,97 331,97Q408.55,97 463.77,152.22Q519,207.45 519,285Q519,362.55 463.77,417.78Q408.55,473 331,473ZM816,285Q816,362.55 760.78,417.78Q705.55,473 628,473Q613,473 587,469.5Q561,466 546,461Q575,423 589.5,378.15Q604,333.3 604,285Q604,236.7 589.5,191.85Q575,147 546,109Q566,102 587,99.5Q608,97 628,97Q705.55,97 760.78,152.22Q816,207.45 816,285Z" />
</vector>

View File

@ -1,16 +1,15 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
~ SPDX-License-Identifier: Apache-2.0
~ SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFFFF"
android:viewportWidth="960"
android:viewportHeight="960">
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,473 799.5,465.5Q799,458 799,453Q794,482 772,501Q750,520 720,520L640,520Q607,520 583.5,496.5Q560,473 560,440L560,400L400,400L400,320Q400,287 423.5,263.5Q447,240 480,240L520,240L520,240Q520,217 532.5,199.5Q545,182 563,171Q543,166 522.5,163Q502,160 480,160Q346,160 253,253Q160,346 160,480Q160,480 160,480Q160,480 160,480L360,480Q426,480 473,527Q520,574 520,640L520,680L400,680L400,790Q420,795 439.5,797.5Q459,800 480,800Z" />
android:fillColor="#FFFFFFFF"
android:pathData="m7.95,0.65c-4.1,0 -7.4,3.3 -7.4,7.4s3.3,7.4 7.4,7.4 7.4,-3.3 7.4,-7.4 -3.3,-7.4 -7.4,-7.4zM8.75,1.55c1.3,0 2.4,0.8 3.5,1.3l1.8,2.5 -0.3,1.1 0.6,0.3v2.4c-0.2,0.7 -0.6,1.3 -0.9,2 -0.2,0.1 0,-0.8 -0.1,-1 0,-0.6 -0.5,-0.6 -0.9,-0.2 -0.4,0.3 -1.4,0.3 -1.5,-0.4 -0.3,-0.8 0,-1.7 0.3,-2.5l-0.6,-0.7 0.2,-1.8 -0.8,-0.9 0.2,-1 -1,-0.6c-0.2,-0.2 -0.6,-0.2 -0.7,-0.4 0.1,0 0.2,-0.1 0.2,-0.1zM6.15,1.65s0.1,0 0.1,0.1c0.4,0.2 -0.1,0.4 -0.2,0.6 -0.5,0.3 0.3,0.7 0.5,1 0.4,-0.1 0.8,-0.7 1.4,-0.5 0.7,-0.2 0.6,0.6 1.1,1 0.1,0.2 0.9,0.8 0.4,0.6 -0.5,-0.4 -1,-0.4 -1.3,0.1 -0.8,0.5 -0.3,-0.9 -0.7,-1.2 -0.6,-0.7 -0.4,0.5 -0.4,0.9 -0.4,0 -1.1,-0.3 -1.5,0.2l0.4,0.6 0.5,-0.7c0,-0.3 0.1,0.2 0.3,0.3 0.1,0.2 0.8,0.7 0.3,0.9 -0.8,0.4 -1.4,1.1 -2.1,1.7 -0.2,0.5 -0.7,0.4 -1,0 -0.7,-0.4 -0.7,0.7 -0.6,1.1l0.6,-0.4v1.1c-0.4,0.4 -0.9,-0.7 -1.3,-0.9v-1.6c0,-0.4 -0.1,-0.9 0,-1.3 0.8,-0.9 1.7,-1.9 2.2,-3h0.8c0.6,0.2 0.3,-0.7 0.5,-0.6zM4.95,9.85c0.1,0 0.2,0 0.3,0.1 0.8,0.1 1.4,0.7 2,1.1 0.5,0.5 1.6,0.3 1.7,1.2 -0.2,0.9 -1.1,1.4 -1.8,1.7 -0.2,0.1 -0.4,0.2 -0.6,0.2 -0.7,0.2 -1,-0.6 -1.2,-1.1 -0.3,-0.7 -1.1,-1.2 -1,-2.1 0,-0.4 0.2,-1 0.6,-1.1z" />
</vector>

View File

@ -1,16 +1,15 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
~ SPDX-FileCopyrightText: 2018-2024 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFFFF"
android:width="16dp"
android:height="16dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,400Q160,367 183.5,343.5Q207,320 240,320L280,320L280,240Q280,157 338.5,98.5Q397,40 480,40Q563,40 621.5,98.5Q680,157 680,240L680,320L720,320Q753,320 776.5,343.5Q800,367 800,400L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,400Q720,400 720,400Q720,400 720,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800ZM480,680Q513,680 536.5,656.5Q560,633 560,600Q560,567 536.5,543.5Q513,520 480,520Q447,520 423.5,543.5Q400,567 400,600Q400,633 423.5,656.5Q447,680 480,680ZM360,320L600,320L600,240Q600,190 565,155Q530,120 480,120Q430,120 395,155Q360,190 360,240L360,320ZM240,800Q240,800 240,800Q240,800 240,800L240,400Q240,400 240,400Q240,400 240,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800Z" />
android:fillColor="#FFFFFFFF"
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,400Q160,367 183.5,343.5Q207,320 240,320L280,320L280,240Q280,157 338.5,98.5Q397,40 480,40Q563,40 621.5,98.5Q680,157 680,240L680,320L720,320Q753,320 776.5,343.5Q800,367 800,400L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM480,680Q513,680 536.5,656.5Q560,633 560,600Q560,567 536.5,543.5Q513,520 480,520Q447,520 423.5,543.5Q400,567 400,600Q400,633 423.5,656.5Q447,680 480,680ZM360,320L600,320L600,240Q600,190 565,155Q530,120 480,120Q430,120 395,155Q360,190 360,240L360,320Z" />
</vector>

View File

@ -1,16 +1,16 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
~ SPDX-FileCopyrightText: 2018-2024 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#ffffff"
android:fillColor="#ffffff"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z" />
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z" />
</vector>

View File

@ -4,13 +4,15 @@
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#969696"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#969696"
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM240,680L720,680L570,480L450,640L360,520L240,680ZM340,400Q365,400 382.5,382.5Q400,365 400,340Q400,315 382.5,297.5Q365,280 340,280Q315,280 297.5,297.5Q280,315 280,340Q280,365 297.5,382.5Q315,400 340,400Z" />
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path android:fillColor="#969696"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

View File

@ -1,7 +1,7 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2023-2025 Google LLC
~ SPDX-FileCopyrightText: 2023-2024 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
@ -12,5 +12,5 @@
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z" />
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z" />
</vector>

View File

@ -5,13 +5,14 @@
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:height="24dp"
android:tint="#969696"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#969696"
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L560,80L800,320L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM520,360L720,360L520,160L520,360Z"/>
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path android:fillColor="#969696"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
</vector>

View File

@ -4,13 +4,15 @@
~ SPDX-FileCopyrightText: 2025 Google LLC
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#969696"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#969696"
android:pathData="M480,480Q513,480 536.5,456.5Q560,433 560,400Q560,367 536.5,343.5Q513,320 480,320Q447,320 423.5,343.5Q400,367 400,400Q400,433 423.5,456.5Q447,480 480,480ZM480,880Q319,743 239.5,625.5Q160,508 160,408Q160,258 256.5,169Q353,80 480,80Q607,80 703.5,169Q800,258 800,408Q800,508 720.5,625.5Q641,743 480,880Z" />
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path android:fillColor="#969696"
android:pathData="M12,2L12,2C8.13,2 5,5.13 5,9c0,1.74 0.5,3.37 1.41,4.84c0.95,1.54 2.2,2.86 3.16,4.4c0.47,0.75 0.81,1.45 1.17,2.26C11,21.05 11.21,22 12,22h0c0.79,0 1,-0.95 1.25,-1.5c0.37,-0.81 0.7,-1.51 1.17,-2.26c0.96,-1.53 2.21,-2.85 3.16,-4.4C18.5,12.37 19,10.74 19,9C19,5.13 15.87,2 12,2zM12,11.75c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5S13.38,11.75 12,11.75z"/>
</vector>

View File

@ -1,7 +1,7 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2025 Google LLC
~ SPDX-FileCopyrightText: 2024 Google LLC
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"

View File

@ -5,12 +5,13 @@
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#969696"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,560Q430,560 395,525Q360,490 360,440L360,200Q360,150 395,115Q430,80 480,80Q530,80 565,115Q600,150 600,200L600,440Q600,490 565,525Q530,560 480,560ZM480,320Q480,320 480,320Q480,320 480,320L480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320L480,320Q480,320 480,320Q480,320 480,320ZM440,840L440,717Q336,703 268,624Q200,545 200,440L280,440Q280,523 338.5,581.5Q397,640 480,640Q563,640 621.5,581.5Q680,523 680,440L760,440Q760,545 692,624Q624,703 520,717L520,840L440,840ZM480,480Q497,480 508.5,468.5Q520,457 520,440L520,200Q520,183 508.5,171.5Q497,160 480,160Q463,160 451.5,171.5Q440,183 440,200L440,440Q440,457 451.5,468.5Q463,480 480,480Z" />
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path android:fillColor="#969696"
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector>

View File

@ -1,7 +1,7 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2021-2025 Google LLC
~ SPDX-FileCopyrightText: 2021-2024 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
@ -12,5 +12,5 @@
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M160,760L160,680L240,680L240,400Q240,317 290,252.5Q340,188 420,168L420,140Q420,115 437.5,97.5Q455,80 480,80Q505,80 522.5,97.5Q540,115 540,140L540,168Q620,188 670,252.5Q720,317 720,400L720,680L800,680L800,760L160,760ZM480,460L480,460L480,460L480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460ZM480,880Q447,880 423.5,856.5Q400,833 400,800L560,800Q560,833 536.5,856.5Q513,880 480,880ZM320,680L640,680L640,400Q640,334 593,287Q546,240 480,240Q414,240 367,287Q320,334 320,400L320,680Z" />
android:pathData="M160,760L160,680L240,680L240,400Q240,317 290,252.5Q340,188 420,168L420,140Q420,115 437.5,97.5Q455,80 480,80Q505,80 522.5,97.5Q540,115 540,140L540,168Q620,188 670,252.5Q720,317 720,400L720,680L800,680L800,760L160,760ZM480,880Q447,880 423.5,856.5Q400,833 400,800L560,800Q560,833 536.5,856.5Q513,880 480,880Z" />
</vector>

View File

@ -8,9 +8,8 @@
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M360,560L760,560L622,380L530,500L468,420L360,560ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM320,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880ZM320,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640L320,640Q320,640 320,640Q320,640 320,640L320,160Q320,160 320,160Q320,160 320,160Z" />
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="@android:color/white"
android:pathData="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z"/>
</vector>

View File

@ -1,7 +1,7 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2021-2025 Google LLC
~ SPDX-FileCopyrightText: 2021-2024 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
@ -12,5 +12,5 @@
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500Z" />
android:pathData="M40,840L480,80L920,840L40,840ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600Z" />
</vector>

Some files were not shown because too many files have changed in this diff Show More