Compare commits

..

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

255 changed files with 1319 additions and 10015 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
application
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
utils

View File

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

View File

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

View File

@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Disabled on forks - 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: | run: |
echo 'Can not analyze PRs from forks' echo 'Can not analyze PRs from forks'
exit 1 exit 1

View File

@ -34,7 +34,7 @@ jobs:
java-version: 17 java-version: 17
- name: Gradle validate - 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 }} - name: Build ${{ matrix.flavor }}
run: | run: |

View File

@ -43,7 +43,7 @@ jobs:
with: with:
swap-size-gb: 10 swap-size-gb: 10
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Set up JDK 17 - name: Set up JDK 17
@ -57,4 +57,4 @@ jobs:
echo "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties" echo "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties"
./gradlew assembleDebug ./gradlew assembleDebug
- name: Perform CodeQL Analysis - 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, -) blocklist=$(curl https://raw.githubusercontent.com/nextcloud/.github/master/non-community-usernames.txt | paste -s -d, -)
echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT" echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT"
- uses: nextcloud/pr-feedback-action@e397f3c7e655092b746e3610d121545530c6a90e # main - uses: nextcloud/pr-feedback-action@1883b38a033fb16f576875e0cf45f98b857655c4 # main
with: with:
feedback-message: | feedback-message: |
Hello there, Hello there,

View File

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

View File

@ -16,7 +16,7 @@ permissions:
jobs: jobs:
reuse-compliance-check: reuse-compliance-check:
runs-on: ubuntu-latest-low runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

View File

@ -34,7 +34,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: "Run analysis" - name: "Run analysis"
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
with: with:
results_file: results.sarif results_file: results.sarif
results_format: sarif results_format: sarif
@ -42,6 +42,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - 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: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@ -33,7 +33,7 @@ jobs:
java-version: 17 java-version: 17
- name: Setup Gradle - 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 - name: Run unit tests with coverage
run: ./gradlew testGplayDebugUnit 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). 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 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 ## [21.0.1] - 2025-04-15
### Fixed ### Fixed

View File

@ -13,9 +13,9 @@ import com.github.spotbugs.snom.Effort
import com.github.spotbugs.snom.SpotBugsTask import com.github.spotbugs.snom.SpotBugsTask
plugins { 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 "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' apply plugin: 'com.android.application'
@ -28,19 +28,19 @@ apply plugin: "org.jlleitschuh.gradle.ktlint"
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
android { android {
compileSdk 35 compileSdk 34
namespace 'com.nextcloud.talk' namespace 'com.nextcloud.talk'
defaultConfig { defaultConfig {
minSdkVersion 26 minSdkVersion 26
targetSdkVersion 35 targetSdkVersion 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable) // mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)
// xx .xxx .xx .xx // xx .xxx .xx .xx
versionCode 220000009 versionCode 220000002
versionName "22.0.0 Alpha 09" versionName "22.0.0 Alpha 02"
flavorDimensions "default" flavorDimensions "default"
renderscriptTargetApi 19 renderscriptTargetApi 19
@ -155,18 +155,18 @@ ext {
daggerVersion = "2.56.2" daggerVersion = "2.56.2"
emojiVersion = "1.5.0" emojiVersion = "1.5.0"
fidoVersion = "4.1.0-patch2" fidoVersion = "4.1.0-patch2"
lifecycleVersion = '2.9.1' lifecycleVersion = '2.8.7'
okhttpVersion = "4.12.0" okhttpVersion = "4.12.0"
markwonVersion = "4.6.2" markwonVersion = "4.6.2"
materialDialogsVersion = "3.3.0" materialDialogsVersion = "3.3.0"
parcelerVersion = "1.1.13" parcelerVersion = "1.1.13"
prismVersion = "2.0.0" prismVersion = "2.0.0"
retrofit2Version = "3.0.0" retrofit2Version = "2.11.0"
roomVersion = "2.7.2" roomVersion = "2.7.1"
workVersion = "2.10.2" workVersion = "2.9.1"
espressoVersion = "3.6.1" espressoVersion = "3.6.1"
androidxTestVersion = "1.5.0" androidxTestVersion = "1.5.0"
media3_version = "1.7.1" media3_version = "1.4.1"
coroutines_version = "1.10.2" coroutines_version = "1.10.2"
mockitoKotlinVersion = "5.4.0" mockitoKotlinVersion = "5.4.0"
} }
@ -180,20 +180,20 @@ configurations.configureEach {
dependencies { dependencies {
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0' 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") 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.preference:preference-ktx:1.2.1'
implementation 'androidx.datastore:datastore-core:1.1.7' implementation 'androidx.datastore:datastore-core:1.1.6'
implementation 'androidx.datastore:datastore-preferences:1.1.7' implementation 'androidx.datastore:datastore-preferences:1.1.6'
implementation 'androidx.test.ext:junit-ktx:1.2.1' implementation 'androidx.test.ext:junit-ktx:1.2.1'
implementation fileTree(include: ['*'], dir: 'libs') 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 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation "com.vanniktech:emoji-google:0.21.0" implementation "com.vanniktech:emoji-google:0.21.0"
@ -236,7 +236,7 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}" implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}"
implementation 'com.bluelinelabs:logansquare:1.3.7' 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' kapt 'com.bluelinelabs:logansquare-compiler:1.3.7'
implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}" implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}"
@ -291,7 +291,6 @@ dependencies {
implementation "io.noties.markwon:core:$markwonVersion" implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion" implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:ext-tasklist:$markwonVersion" implementation "io.noties.markwon:ext-tasklist:$markwonVersion"
implementation "io.noties.markwon:ext-tables:$markwonVersion"
implementation 'com.github.nextcloud-deps:ImagePicker:2.1.0.2' implementation 'com.github.nextcloud-deps:ImagePicker:2.1.0.2'
implementation 'io.github.elye:loaderviewlibrary:3.0.0' implementation 'io.github.elye:loaderviewlibrary:3.0.0'
@ -301,35 +300,35 @@ dependencies {
exclude group: 'org.apache.httpcomponents', module: 'httpclient' exclude group: 'org.apache.httpcomponents', module: 'httpclient'
}) })
implementation 'androidx.core:core-ktx:1.16.0' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.10.1' implementation 'androidx.activity:activity-ktx:1.9.3'
implementation 'com.github.nextcloud.android-common:ui:0.26.0' implementation 'com.github.nextcloud.android-common:ui:0.23.2'
implementation 'com.github.nextcloud-deps:android-talk-webrtc:132.6834.0' 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.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 //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.ui:ui")
implementation 'androidx.compose.material3:material3:1.3.2' implementation 'androidx.compose.material3:material3:1.3.2'
implementation("androidx.compose.ui:ui-tooling-preview") 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") debugImplementation("androidx.compose.ui:ui-tooling")
//tests //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") debugImplementation("androidx.compose.ui:ui-test-manifest")
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.18.0' testImplementation 'org.mockito:mockito-core:5.17.0'
testImplementation 'androidx.arch.core:core-testing:2.2.0' testImplementation 'androidx.arch.core:core-testing:2.2.0'
androidTestImplementation "androidx.test:core:1.6.1" androidTestImplementation "androidx.test:core:1.6.1"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"
androidTestImplementation 'androidx.test:core-ktx:1.6.1' androidTestImplementation 'androidx.test:core-ktx:1.6.1'
androidTestImplementation 'org.mockito:mockito-android:5.18.0' androidTestImplementation 'org.mockito:mockito-android:5.17.0'
androidTestImplementation "androidx.work:work-testing:${workVersion}" androidTestImplementation "androidx.work:work-testing:${workVersion}"
// Espresso core // Espresso core
androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", { androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", {
@ -343,11 +342,11 @@ dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.2') 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.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" 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) { tasks.register('installGitHooks', Copy) {

View File

@ -1,725 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 15,
"identityHash": "acac3fd21e35762b90f65f213be38ccd",
"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, 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
}
],
"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, 'acac3fd21e35762b90f65f213be38ccd')"
]
}
}

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

View File

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

View File

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

View File

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

View File

@ -11,13 +11,11 @@ package com.nextcloud.talk.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.webkit.SslErrorHandler 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.DisplayUtils
import com.nextcloud.talk.utils.FileViewerUtils import com.nextcloud.talk.utils.FileViewerUtils
import com.nextcloud.talk.utils.UriUtils import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.adjustUIForAPILevel35
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
@ -84,7 +81,6 @@ open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
adjustUIForAPILevel35()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
cleanTempCertPreference() cleanTempCertPreference()
@ -115,23 +111,10 @@ open class BaseActivity : AppCompatActivity() {
eventBus.unregister(this) eventBus.unregister(this)
} }
/* fun setupSystemColors() {
* 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() colorizeStatusBar()
colorizeNavigationBar() colorizeNavigationBar()
} }
insets
}
}
open fun colorizeStatusBar() { open fun colorizeStatusBar() {
if (resources != null) { if (resources != null) {

View File

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

View File

@ -19,7 +19,6 @@ import android.text.TextUtils
import android.text.format.DateUtils import android.text.format.DateUtils
import android.text.style.ImageSpan import android.text.style.ImageSpan
import android.view.View import android.view.View
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import com.nextcloud.talk.R import com.nextcloud.talk.R
@ -156,30 +155,6 @@ class ConversationItem(
} else { } else {
holder.binding.userStatusImage.visibility = View.GONE 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) setLastMessage(holder, appContext)
showAvatar(holder) showAvatar(holder)
} }
@ -431,9 +406,9 @@ class ConversationItem(
) )
return lastMessage return lastMessage
} else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == chatMessage?.getCalculateMessageType()) { } else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == chatMessage?.getCalculateMessageType()) {
var attachmentName = chatMessage.text var attachmentName = chatMessage.message
if (attachmentName == "{file}") { if (attachmentName == "{file}") {
attachmentName = chatMessage.messageParameters?.get("file")?.get("name") ?: "" attachmentName = chatMessage.messageParameters?.get("file")?.get("name")
} }
val author = authorName(chatMessage) 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

@ -18,9 +18,7 @@ import androidx.core.content.ContextCompat
import androidx.core.text.toSpanned import androidx.core.text.toSpanned
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.load import coil.load
import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.snackbar.Snackbar 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.R
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
@ -105,33 +103,10 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
true, true,
viewThemeUtils viewThemeUtils
) )
val spansFromString: Array<Any> = processedMessageText!!.getSpans(
0,
processedMessageText.length,
Any::class.java
)
if (spansFromString.isNotEmpty()) {
binding.bubble.layoutParams.apply {
width = FlexboxLayout.LayoutParams.MATCH_PARENT
}
binding.messageText.layoutParams.apply {
width = FlexboxLayout.LayoutParams.MATCH_PARENT
}
} else {
binding.bubble.layoutParams.apply {
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
}
binding.messageText.layoutParams.apply {
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
}
}
processedMessageText = messageUtils.processMessageParameters( processedMessageText = messageUtils.processMessageParameters(
binding.messageText.context, binding.messageText.context,
viewThemeUtils, viewThemeUtils,
processedMessageText, processedMessageText!!,
message, message,
itemView itemView
) )
@ -158,7 +133,7 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
binding.messageEditIndicator.visibility = View.GONE binding.messageEditIndicator.visibility = View.GONE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) 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 // parent message handling
if (!message.isDeleted && message.parentMessageId != null) { if (!message.isDeleted && message.parentMessageId != null) {
processParentMessage(message) 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.ChatActivity
import com.nextcloud.talk.chat.data.ChatMessageRepository import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.model.ChatMessage 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.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
@ -106,6 +105,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
if (!hasCheckboxes) { if (!hasCheckboxes) {
realView.isSelected = false realView.isSelected = false
layoutParams.isWrapBefore = false layoutParams.isWrapBefore = false
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
binding.messageText.visibility = View.VISIBLE binding.messageText.visibility = View.VISIBLE
binding.checkboxContainer.visibility = View.GONE binding.checkboxContainer.visibility = View.GONE
@ -116,33 +116,10 @@ class OutcomingTextMessageViewHolder(itemView: View) :
false, false,
viewThemeUtils viewThemeUtils
) )
val spansFromString: Array<Any> = processedMessageText!!.getSpans(
0,
processedMessageText.length,
Any::class.java
)
if (spansFromString.isNotEmpty()) {
binding.bubble.layoutParams.apply {
width = FlexboxLayout.LayoutParams.MATCH_PARENT
}
binding.messageText.layoutParams.apply {
width = FlexboxLayout.LayoutParams.MATCH_PARENT
}
} else {
binding.bubble.layoutParams.apply {
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
}
binding.messageText.layoutParams.apply {
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
}
}
processedMessageText = messageUtils.processMessageParameters( processedMessageText = messageUtils.processMessageParameters(
binding.messageText.context, binding.messageText.context,
viewThemeUtils, viewThemeUtils,
processedMessageText, processedMessageText!!,
message, message,
itemView itemView
) )
@ -172,7 +149,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
binding.messageEditIndicator.visibility = View.GONE binding.messageEditIndicator.visibility = View.GONE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) 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) setBubbleOnChatMessage(message)
// parent message handling // parent message handling
if (!message.isDeleted && message.parentMessageId != null) { if (!message.isDeleted && message.parentMessageId != null) {
@ -185,7 +162,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
binding.checkMark.visibility = View.INVISIBLE binding.checkMark.visibility = View.INVISIBLE
binding.sendingProgress.visibility = View.GONE 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)) updateStatus(R.drawable.baseline_error_outline_24, context.resources?.getString(R.string.nc_message_failed))
} else if (message.isTemporary) { } else if (message.isTemporary) {
updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending)) updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending))

View File

@ -17,7 +17,6 @@ import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.AddParticipantOverall import com.nextcloud.talk.models.json.participants.AddParticipantOverall
import com.nextcloud.talk.models.json.participants.TalkBan import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.participants.TalkBanOverall import com.nextcloud.talk.models.json.participants.TalkBanOverall
import com.nextcloud.talk.models.json.profile.ProfileOverall
import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import okhttp3.MultipartBody import okhttp3.MultipartBody
@ -179,36 +178,12 @@ interface NcApiCoroutines {
@Url url: String @Url url: String
): GenericOverall ): 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 @DELETE
suspend fun removeConversationFromFavorites( suspend fun removeConversationFromFavorites(
@Header("Authorization") authorization: String, @Header("Authorization") authorization: String,
@Url url: String @Url url: String
): GenericOverall ): GenericOverall
@POST
suspend fun markConversationAsSensitive(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE
suspend fun markConversationAsInsensitive(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@FormUrlEncoded @FormUrlEncoded
@POST @POST
suspend fun notificationCalls( suspend fun notificationCalls(
@ -279,10 +254,4 @@ interface NcApiCoroutines {
@GET @GET
suspend fun getNoteToSelfRoom(@Header("Authorization") authorization: String, @Url url: String): RoomOverall suspend fun getNoteToSelfRoom(@Header("Authorization") authorization: String, @Url url: String): RoomOverall
@GET
suspend fun getProfile(@Header("Authorization") authorization: String, @Url url: String): ProfileOverall
@DELETE
suspend fun unbindRoom(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
} }

View File

@ -33,6 +33,7 @@ import coil.decode.SvgDecoder
import coil.memory.MemoryCache import coil.memory.MemoryCache
import coil.util.DebugLogger import coil.util.DebugLogger
import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.filebrowser.webdav.DavUtils
import com.nextcloud.talk.dagger.modules.BusModule import com.nextcloud.talk.dagger.modules.BusModule
import com.nextcloud.talk.dagger.modules.ContextModule import com.nextcloud.talk.dagger.modules.ContextModule
import com.nextcloud.talk.dagger.modules.DaosModule import com.nextcloud.talk.dagger.modules.DaosModule
@ -42,7 +43,6 @@ import com.nextcloud.talk.dagger.modules.RepositoryModule
import com.nextcloud.talk.dagger.modules.RestModule import com.nextcloud.talk.dagger.modules.RestModule
import com.nextcloud.talk.dagger.modules.UtilsModule import com.nextcloud.talk.dagger.modules.UtilsModule
import com.nextcloud.talk.dagger.modules.ViewModelModule import com.nextcloud.talk.dagger.modules.ViewModelModule
import com.nextcloud.talk.filebrowser.webdav.DavUtils
import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.CapabilitiesWorker import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.SignalingSettingsWorker import com.nextcloud.talk.jobs.SignalingSettingsWorker

View File

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

View File

@ -9,15 +9,14 @@
package com.nextcloud.talk.call.components package com.nextcloud.talk.call.components
import android.annotation.SuppressLint
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
@ -30,7 +29,6 @@ import com.nextcloud.talk.adapters.ParticipantUiState
import org.webrtc.EglBase import org.webrtc.EglBase
import kotlin.math.ceil import kotlin.math.ceil
@SuppressLint("UnusedBoxWithConstraintsScope")
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
fun ParticipantGrid( fun ParticipantGrid(
@ -38,6 +36,7 @@ fun ParticipantGrid(
eglBase: EglBase?, eglBase: EglBase?,
participantUiStates: List<ParticipantUiState>, participantUiStates: List<ParticipantUiState>,
isVoiceOnlyCall: Boolean, isVoiceOnlyCall: Boolean,
isInPipMode: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
@ -45,9 +44,8 @@ fun ParticipantGrid(
val minItemHeight = 100.dp val minItemHeight = 100.dp
if (participantUiStates.isEmpty()) return val columns =
if (isPortrait) {
val columns = if (isPortrait) {
when (participantUiStates.size) { when (participantUiStates.size) {
1, 2, 3 -> 1 1, 2, 3 -> 1
else -> 2 else -> 2
@ -58,34 +56,38 @@ fun ParticipantGrid(
2, 4 -> 2 2, 4 -> 2
else -> 3 else -> 3
} }
}.coerceAtLeast(1) // Prevent 0 }
val rows = ceil(participantUiStates.size / columns.toFloat()).toInt().coerceAtLeast(1) 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 {
0.dp
}
val gridHeight = LocalConfiguration.current.screenHeightDp.dp - heightForNonGridComponents
val itemSpacing = 8.dp val itemSpacing = 8.dp
val edgePadding = 8.dp val edgePadding = 8.dp
val totalVerticalSpacing = itemSpacing * (rows - 1) val totalVerticalSpacing = itemSpacing * (rows - 1)
val totalVerticalPadding = edgePadding * 2 val totalVerticalPadding = edgePadding * 2
val availableHeight = gridHeight - totalVerticalSpacing - totalVerticalPadding
BoxWithConstraints( val rawItemHeight = availableHeight / rows
modifier = modifier
.fillMaxSize()
.clickable { onClick() }
) {
val availableHeight = maxHeight
val gridAvailableHeight = availableHeight - totalVerticalSpacing - totalVerticalPadding
val rawItemHeight = gridAvailableHeight / rows
val itemHeight = maxOf(rawItemHeight, minItemHeight) val itemHeight = maxOf(rawItemHeight, minItemHeight)
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(columns), columns = GridCells.Fixed(columns),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxSize()
.height(availableHeight), .padding(horizontal = edgePadding)
.clickable { onClick() },
verticalArrangement = Arrangement.spacedBy(itemSpacing), verticalArrangement = Arrangement.spacedBy(itemSpacing),
horizontalArrangement = Arrangement.spacedBy(itemSpacing), horizontalArrangement = Arrangement.spacedBy(itemSpacing),
contentPadding = PaddingValues(vertical = edgePadding, horizontal = edgePadding) contentPadding = PaddingValues(vertical = edgePadding)
) { ) {
items( items(
participantUiStates, participantUiStates,
@ -102,7 +104,6 @@ fun ParticipantGrid(
} }
} }
} }
}
@Preview @Preview
@Composable @Composable
@ -110,7 +111,8 @@ fun ParticipantGridPreview() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(1), participantUiStates = getTestParticipants(1),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -120,7 +122,8 @@ fun TwoParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(2), participantUiStates = getTestParticipants(2),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -130,7 +133,8 @@ fun ThreeParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(3), participantUiStates = getTestParticipants(3),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -140,7 +144,8 @@ fun FourParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(4), participantUiStates = getTestParticipants(4),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -150,7 +155,8 @@ fun FiveParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(5), participantUiStates = getTestParticipants(5),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -160,7 +166,8 @@ fun SevenParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(7), participantUiStates = getTestParticipants(7),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -170,7 +177,8 @@ fun FiftyParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(50), participantUiStates = getTestParticipants(50),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -184,7 +192,8 @@ fun OneParticipantLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(1), participantUiStates = getTestParticipants(1),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -198,7 +207,8 @@ fun TwoParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(2), participantUiStates = getTestParticipants(2),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -212,7 +222,8 @@ fun ThreeParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(3), participantUiStates = getTestParticipants(3),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -226,7 +237,8 @@ fun FourParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(4), participantUiStates = getTestParticipants(4),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -240,7 +252,8 @@ fun SevenParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(7), participantUiStates = getTestParticipants(7),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -254,7 +267,8 @@ fun FiftyParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(50), participantUiStates = getTestParticipants(50),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }

View File

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

View File

@ -16,6 +16,7 @@ package com.nextcloud.talk.chat
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
@ -52,7 +53,6 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
@ -66,8 +66,6 @@ import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.text.bold import androidx.core.text.bold
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.emoji2.text.EmojiCompat import androidx.emoji2.text.EmojiCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.commit import androidx.fragment.app.commit
@ -87,7 +85,6 @@ import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.target.Target import coil.target.Target
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.color.ColorUtil import com.nextcloud.android.common.ui.color.ColorUtil
@ -127,7 +124,6 @@ import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.conversationinfo.ConversationInfoActivity import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationlist.ConversationsListActivity import com.nextcloud.talk.conversationlist.ConversationsListActivity
import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
@ -146,7 +142,6 @@ import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.capabilities.SpreedCapability import com.nextcloud.talk.models.json.capabilities.SpreedCapability
import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
import com.nextcloud.talk.polls.ui.PollCreateDialogFragment import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
@ -158,7 +153,6 @@ import com.nextcloud.talk.ui.PlaybackSpeed
import com.nextcloud.talk.ui.PlaybackSpeedControl import com.nextcloud.talk.ui.PlaybackSpeedControl
import com.nextcloud.talk.ui.StatusDrawable import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
import com.nextcloud.talk.ui.dialog.ContextChatCompose
import com.nextcloud.talk.ui.dialog.DateTimeCompose import com.nextcloud.talk.ui.dialog.DateTimeCompose
import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
import com.nextcloud.talk.ui.dialog.MessageActionsDialog import com.nextcloud.talk.ui.dialog.MessageActionsDialog
@ -170,10 +164,6 @@ import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.AudioUtils import com.nextcloud.talk.utils.AudioUtils
import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfEventRooms
import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfInstantMeetingRoom
import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfSIPRoom
import com.nextcloud.talk.utils.ContactUtils import com.nextcloud.talk.utils.ContactUtils
import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.ConversationUtils
import com.nextcloud.talk.utils.DateConstants import com.nextcloud.talk.utils.DateConstants
@ -218,7 +208,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@ -235,8 +224,10 @@ import java.util.Locale
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.models.json.participants.Participant
@Suppress("TooManyFunctions")
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
class ChatActivity : class ChatActivity :
BaseActivity(), BaseActivity(),
@ -277,8 +268,6 @@ class ChatActivity :
lateinit var conversationInfoViewModel: ConversationInfoViewModel lateinit var conversationInfoViewModel: ConversationInfoViewModel
lateinit var messageInputViewModel: MessageInputViewModel lateinit var messageInputViewModel: MessageInputViewModel
private var chatMenu: Menu? = null
private val startSelectContactForResult = registerForActivityResult( private val startSelectContactForResult = registerForActivityResult(
ActivityResultContracts ActivityResultContracts
.StartActivityForResult() .StartActivityForResult()
@ -307,33 +296,9 @@ class ChatActivity :
private val startMessageSearchForResult = private val startMessageSearchForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
executeIfResultOk(it) { intent -> executeIfResultOk(it) { intent ->
runBlocking { onMessageSearchResult(intent)
val id = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
id?.let {
startContextChatWindowForMessage(id)
} }
} }
}
}
private fun startContextChatWindowForMessage(id: String?) {
binding.genericComposeView.apply {
val shouldDismiss = mutableStateOf(false)
setContent {
val bundle = bundleOf()
bundle.putString(BundleKeys.KEY_CREDENTIALS, credentials!!)
bundle.putString(BundleKeys.KEY_BASE_URL, conversationUser!!.baseUrl)
bundle.putString(KEY_ROOM_TOKEN, roomToken)
bundle.putString(BundleKeys.KEY_MESSAGE_ID, id)
bundle.putString(
KEY_CONVERSATION_NAME,
currentConversation!!.displayName
)
ContextChatCompose(bundle).GetDialogView(shouldDismiss, context)
}
}
Log.d(TAG, "Should open something else")
}
private val startPickCameraIntentForResult = registerForActivityResult( private val startPickCameraIntentForResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
@ -364,7 +329,6 @@ class ChatActivity :
var startCallFromRoomSwitch: Boolean = false var startCallFromRoomSwitch: Boolean = false
var voiceOnly: Boolean = true var voiceOnly: Boolean = true
var focusInput: Boolean = false
private lateinit var path: String private lateinit var path: String
var myFirstMessage: CharSequence? = null var myFirstMessage: CharSequence? = null
@ -460,28 +424,7 @@ class ChatActivity :
binding = ActivityChatBinding.inflate(layoutInflater) binding = ActivityChatBinding.inflate(layoutInflater)
setupActionBar() setupActionBar()
setContentView(binding.root) setContentView(binding.root)
setupSystemColors()
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()
}
conversationUser = currentUserProvider.currentUser.blockingGet() conversationUser = currentUserProvider.currentUser.blockingGet()
handleIntent(intent) handleIntent(intent)
@ -509,7 +452,7 @@ class ChatActivity :
initObservers() initObservers()
pickMultipleMedia = registerForActivityResult( pickMultipleMedia = registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(MAX_AMOUNT_MEDIA_FILE_PICKER) ActivityResultContracts.PickMultipleVisualMedia(5)
) { uris -> ) { uris ->
if (uris.isNotEmpty()) { if (uris.isNotEmpty()) {
onChooseFileResult(uris) onChooseFileResult(uris)
@ -569,8 +512,6 @@ class ChatActivity :
startCallFromRoomSwitch = extras?.getBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, false) == true startCallFromRoomSwitch = extras?.getBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, false) == true
voiceOnly = extras?.getBoolean(KEY_CALL_VOICE_ONLY, false) == true voiceOnly = extras?.getBoolean(KEY_CALL_VOICE_ONLY, false) == true
focusInput = extras?.getBoolean(BundleKeys.KEY_FOCUS_INPUT) == true
} }
override fun onStart() { override fun onStart() {
@ -662,17 +603,12 @@ class ChatActivity :
supportFragmentManager.commit { supportFragmentManager.commit {
setReorderingAllowed(true) // optimizes out redundant replace operations setReorderingAllowed(true) // optimizes out redundant replace operations
replace(R.id.fragment_container_activity_chat, messageInputFragment) replace(R.id.fragment_container_activity_chat, messageInputFragment)
runOnCommit {
if (focusInput) {
messageInputFragment.binding.fragmentMessageInputView.requestFocus()
}
}
} }
joinRoomWithPassword() joinRoomWithPassword()
if (conversationUser?.userId != "?" && if (conversationUser?.userId != "?" &&
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG) CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)
) { ) {
binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() } binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() }
} }
@ -696,59 +632,6 @@ class ChatActivity :
} }
} }
if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT &&
hasSpreedFeatureCapability(
conversationUser?.capabilities!!.spreedCapability!!,
SpreedFeatures.UNBIND_CONVERSATION
)
) {
val eventEndTimeStamp =
currentConversation?.objectId
?.split("#")
?.getOrNull(1)
?.toLongOrNull()
val currentTimeStamp = (System.currentTimeMillis() / ONE_SECOND_IN_MILLIS).toLong()
val retentionPeriod = retentionOfEventRooms(spreedCapabilities)
val isPastEvent = eventEndTimeStamp?.let { it < currentTimeStamp }
if (isPastEvent == true && retentionPeriod != 0) {
showConversationDeletionWarning(retentionPeriod)
}
}
if (currentConversation?.objectType == ConversationEnums.ObjectType.PHONE_TEMPORARY &&
hasSpreedFeatureCapability(
conversationUser?.capabilities!!.spreedCapability!!,
SpreedFeatures.UNBIND_CONVERSATION
)
) {
val retentionPeriod = retentionOfSIPRoom(spreedCapabilities)
val systemMessage = currentConversation?.lastMessage?.systemMessageType
if (retentionPeriod != 0 && (
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED ||
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE
)
) {
showConversationDeletionWarning(retentionPeriod)
}
}
if (currentConversation?.objectType == ConversationEnums.ObjectType.INSTANT_MEETING &&
hasSpreedFeatureCapability(
conversationUser?.capabilities!!.spreedCapability!!,
SpreedFeatures.UNBIND_CONVERSATION
)
) {
val retentionPeriod = retentionOfInstantMeetingRoom(spreedCapabilities)
val systemMessage = currentConversation?.lastMessage?.systemMessageType
if (retentionPeriod != 0 && (
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED ||
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE
)
) {
showConversationDeletionWarning(retentionPeriod)
}
}
updateRoomTimerHandler(MILLIS_250) updateRoomTimerHandler(MILLIS_250)
val urlForChatting = val urlForChatting =
@ -1075,10 +958,8 @@ class ChatActivity :
val newString = state.messageEdited.ocs?.data?.parentMessage?.message ?: "(null)" val newString = state.messageEdited.ocs?.data?.parentMessage?.message ?: "(null)"
val id = state.messageEdited.ocs?.data?.parentMessage?.id.toString() val id = state.messageEdited.ocs?.data?.parentMessage?.id.toString()
val index = adapter?.getMessagePositionById(id) ?: 0 val index = adapter?.getMessagePositionById(id) ?: 0
val item = adapter?.items?.get(index)?.item val message = adapter?.items?.get(index)?.item as ChatMessage
item?.let { setMessageAsEdited(message, newString)
setMessageAsEdited(item as ChatMessage, newString)
}
} }
is MessageInputViewModel.EditMessageErrorState -> { is MessageInputViewModel.EditMessageErrorState -> {
@ -1120,29 +1001,6 @@ class ChatActivity :
binding.voiceRecordingLock.y -= y binding.voiceRecordingLock.y -= y
} }
chatViewModel.unbindRoomResult.observe(this) { uiState ->
when (uiState) {
is ChatViewModel.UnbindRoomUiState.Success -> {
binding.conversationDeleteNotice.visibility = View.GONE
Snackbar.make(
binding.root,
context.getString(R.string.nc_room_retention),
Snackbar.LENGTH_LONG
).show()
chatMenu?.removeItem(R.id.conversation_event)
}
is ChatViewModel.UnbindRoomUiState.Error -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_common_error_sorry),
Snackbar.LENGTH_LONG
).show()
}
else -> { }
}
}
chatViewModel.outOfOfficeViewState.observe(this) { uiState -> chatViewModel.outOfOfficeViewState.observe(this) { uiState ->
when (uiState) { when (uiState) {
is ChatViewModel.OutOfOfficeUIState.Error -> { is ChatViewModel.OutOfOfficeUIState.Error -> {
@ -1258,69 +1116,6 @@ class ChatActivity :
} }
} }
fun showConversationDeletionWarning(retentionPeriod: Int) {
binding.conversationDeleteNotice.visibility = View.VISIBLE
binding.conversationDeleteNotice.apply {
isClickable = false
isFocusable = false
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,
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 =
View.VISIBLE
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.keep_button).visibility = View.VISIBLE
} else {
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.delete_now_button).visibility =
View.GONE
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.keep_button).visibility = View.GONE
}
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.delete_now_button).setOnClickListener {
deleteConversationDialog(it.context)
}
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.keep_button).setOnClickListener {
chatViewModel.unbindRoom(credentials!!, conversationUser?.baseUrl!!, currentConversation?.token!!)
}
}
fun deleteConversationDialog(context: Context) {
val dialogBuilder = MaterialAlertDialogBuilder(context)
.setIcon(
viewThemeUtils.dialog
.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)
)
.setTitle(R.string.nc_delete_call)
.setMessage(R.string.nc_delete_conversation_more)
.setPositiveButton(R.string.nc_delete) { _, _ ->
currentConversation?.let { conversation ->
deleteConversation(conversation)
}
}
.setNegativeButton(R.string.nc_cancel) { _, _ ->
}
viewThemeUtils.dialog
.colorMaterialAlertDialogBackground(context, dialogBuilder)
val dialog = dialogBuilder.show()
viewThemeUtils.platform.colorTextButtons(
dialog.getButton(AlertDialog.BUTTON_POSITIVE),
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
)
}
@Suppress("Detekt.TooGenericExceptionCaught") @Suppress("Detekt.TooGenericExceptionCaught")
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -1993,8 +1788,8 @@ class ChatActivity :
WorkManager.getInstance().enqueue(downloadWorker) WorkManager.getInstance().enqueue(downloadWorker)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id) WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id)
.observeForever { workInfo: WorkInfo? -> .observeForever { workInfo: WorkInfo ->
if (workInfo?.state == WorkInfo.State.SUCCEEDED) { if (workInfo.state == WorkInfo.State.SUCCEEDED) {
funToCallWhenDownloadSuccessful() funToCallWhenDownloadSuccessful()
} }
} }
@ -2077,7 +1872,7 @@ class ChatActivity :
private fun shouldShowLobby(): Boolean { private fun shouldShowLobby(): Boolean {
if (currentConversation != null) { if (currentConversation != null) {
return hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) && return CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
currentConversation?.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY && currentConversation?.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
!ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) && !ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) &&
!participantPermissions.canIgnoreLobby() !participantPermissions.canIgnoreLobby()
@ -2332,8 +2127,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) { private fun executeIfResultOk(result: ActivityResult, onResult: (intent: Intent?) -> Unit) {
if (result.resultCode == RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
onResult(result.data) onResult(result.data)
} else { } else {
Log.e(TAG, "resultCode for received intent was != ok") Log.e(TAG, "resultCode for received intent was != ok")
@ -2362,8 +2164,8 @@ class ChatActivity :
} else { } else {
Log.d( Log.d(
TAG, TAG,
"message $messageId that should be scrolled " + "message $messageId that should be scrolled to was not found " +
"to was not found (scrollToAndCenterMessageWithId)" "(scrollToAndCenterMessageWithId)"
) )
} }
} }
@ -2827,7 +2629,7 @@ class ChatActivity :
} }
if (this::spreedCapabilities.isInitialized) { if (this::spreedCapabilities.isInitialized) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) { if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
deleteExpiredMessages() deleteExpiredMessages()
} }
} else { } else {
@ -3074,7 +2876,6 @@ class ChatActivity :
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.menu_conversation, menu) menuInflater.inflate(R.menu.menu_conversation, menu)
chatMenu = menu
if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) { if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) {
eventConversationMenuItem = menu.findItem(R.id.conversation_event) eventConversationMenuItem = menu.findItem(R.id.conversation_event)
@ -3088,6 +2889,7 @@ class ChatActivity :
loadAvatarForStatusBar() loadAvatarForStatusBar()
setActionBarTitle() setActionBarTitle()
} }
return true return true
} }
@ -3095,7 +2897,7 @@ class ChatActivity :
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
if (this::spreedCapabilities.isInitialized) { if (this::spreedCapabilities.isInitialized) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS)) { if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS)) {
checkShowCallButtons() checkShowCallButtons()
} }
@ -3116,7 +2918,7 @@ class ChatActivity :
}.collect() }.collect()
} }
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) { if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) {
Handler().post { Handler().post {
findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener { findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {
showCallButtonMenu(true) showCallButtonMenu(true)
@ -3175,10 +2977,10 @@ class ChatActivity :
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@SuppressLint("InflateParams")
private fun showPopupWindow(anchorView: View) { private fun showPopupWindow(anchorView: View) {
val popupView = layoutInflater.inflate(R.layout.item_event_schedule, null) 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 subtitleTextView = popupView.findViewById<TextView>(R.id.meetingTime)
val popupWindow = PopupWindow( val popupWindow = PopupWindow(
@ -3207,7 +3009,28 @@ class ChatActivity :
deleteConversation.visibility = View.VISIBLE deleteConversation.visibility = View.VISIBLE
deleteConversation.setOnClickListener { deleteConversation.setOnClickListener {
deleteConversationDialog(it.context) val dialogBuilder = MaterialAlertDialogBuilder(it.context)
.setIcon(
viewThemeUtils.dialog
.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)
)
.setTitle(R.string.nc_delete_call)
.setMessage(R.string.nc_delete_conversation_more)
.setPositiveButton(R.string.nc_delete) { _, _ ->
currentConversation?.let { conversation ->
deleteConversation(conversation)
}
}
.setNegativeButton(R.string.nc_cancel) { _, _ ->
}
viewThemeUtils.dialog
.colorMaterialAlertDialogBackground(it.context, dialogBuilder)
val dialog = dialogBuilder.show()
viewThemeUtils.platform.colorTextButtons(
dialog.getButton(AlertDialog.BUTTON_POSITIVE),
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
)
popupWindow.dismiss() popupWindow.dismiss()
} }
} else { } else {
@ -3336,11 +3159,9 @@ class ChatActivity :
context.resources.getString(R.string.nc_tomorrow_meeting), context.resources.getString(R.string.nc_tomorrow_meeting),
startDateTime.format(DateTimeFormatter.ofPattern("HH:mm")) startDateTime.format(DateTimeFormatter.ofPattern("HH:mm"))
) )
else -> startDateTime.format(DateTimeFormatter.ofPattern("MMM d, yyyy, HH:mm")) else -> startDateTime.format(DateTimeFormatter.ofPattern("MMM d, yyyy, HH:mm"))
} }
} }
currentTime.isAfter(endDateTime) -> context.resources.getString(R.string.nc_meeting_ended) currentTime.isAfter(endDateTime) -> context.resources.getString(R.string.nc_meeting_ended)
else -> context.resources.getString(R.string.nc_ongoing_meeting) else -> context.resources.getString(R.string.nc_ongoing_meeting)
} }
@ -3627,7 +3448,7 @@ class ChatActivity :
fun copyMessage(message: IMessage?) { fun copyMessage(message: IMessage?) {
val clipboardManager = val clipboardManager =
getSystemService(CLIPBOARD_SERVICE) as ClipboardManager getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText( val clipData = ClipData.newPlainText(
resources?.getString(R.string.nc_app_product_name), resources?.getString(R.string.nc_app_product_name),
message?.text message?.text
@ -3803,7 +3624,6 @@ class ChatActivity :
) )
showSnackBar(roomToken) showSnackBar(roomToken)
} }
else -> {} else -> {}
} }
} }
@ -3824,7 +3644,6 @@ class ChatActivity :
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent) startActivity(chatIntent)
} }
fun openInFilesApp(message: ChatMessage) { fun openInFilesApp(message: ChatMessage) {
val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID] val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]
val link = message.selectedIndividualHashMap!!["link"] val link = message.selectedIndividualHashMap!!["link"]
@ -3941,7 +3760,7 @@ class ChatActivity :
val isOlderThanSixHours = message val isOlderThanSixHours = message
.createdAt .createdAt
.before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE)) .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE))
val hasDeleteMessagesUnlimitedCapability = hasSpreedFeatureCapability( val hasDeleteMessagesUnlimitedCapability = CapabilitiesUtil.hasSpreedFeatureCapability(
spreedCapabilities, spreedCapabilities,
SpreedFeatures.DELETE_MESSAGES_UNLIMITED SpreedFeatures.DELETE_MESSAGES_UNLIMITED
) )
@ -3951,7 +3770,7 @@ class ChatActivity :
!hasDeleteMessagesUnlimitedCapability && isOlderThanSixHours -> false !hasDeleteMessagesUnlimitedCapability && isOlderThanSixHours -> false
message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false
message.isDeleted -> false message.isDeleted -> false
!hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false !CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false
!participantPermissions.hasChatPermission() -> false !participantPermissions.hasChatPermission() -> false
hasDeleteMessagesUnlimitedCapability -> true hasDeleteMessagesUnlimitedCapability -> true
else -> true else -> true
@ -4108,7 +3927,9 @@ class ChatActivity :
} }
if (!foundMessage) { if (!foundMessage) {
Log.d(TAG, "quoted message with id " + parentMessage.id + " was not found in adapter") Log.d(TAG, "quoted message with id " + parentMessage.id + " was not found in adapter")
startContextChatWindowForMessage(parentMessage.id) // TODO: show better info
// TODO: improve handling how this can be avoided. E.g. loading chat until message is reached...
Snackbar.make(binding.root, "Message was not found", Snackbar.LENGTH_LONG).show()
} }
} }
@ -4216,6 +4037,5 @@ class ChatActivity :
const val OUT_OF_OFFICE_ALPHA = 76 const val OUT_OF_OFFICE_ALPHA = 76
const val ZERO_INDEX = 0 const val ZERO_INDEX = 0
const val ONE_INDEX = 1 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) val connectionGained = (!wasOnline && isOnline)
Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained") Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained")
if (connectionGained) { if (connectionGained) {
chatActivity.messageInputViewModel.sendUnsentMessages( chatActivity.messageInputViewModel.sendTempMessages(
chatActivity.conversationUser!!.getCredentials(), chatActivity.conversationUser!!.getCredentials(),
ApiUtils.getUrlForChat( ApiUtils.getUrlForChat(
chatActivity.chatApiVersion, chatActivity.chatApiVersion,

View File

@ -110,7 +110,7 @@ interface ChatMessageRepository : LifecycleAwareManager {
suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow<Boolean> 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) suspend fun deleteTempMessage(chatMessage: ChatMessage)
} }

View File

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

View File

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

View File

@ -76,5 +76,4 @@ interface ChatNetworkDataSource {
limit: Int limit: Int
): List<ChatMessageJson> ): List<ChatMessageJson>
suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference? suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference?
suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall
} }

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.mappers.asModel
import com.nextcloud.talk.data.database.model.ChatBlockEntity import com.nextcloud.talk.data.database.model.ChatBlockEntity
import com.nextcloud.talk.data.database.model.ChatMessageEntity 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.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.extensions.toIntOrZero 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.. sendTempChatMessages(credentials, urlForChatting)
sendUnsentChatMessages(credentials, urlForChatting)
// delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing // 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). // with them (otherwise there is a race condition).
@ -367,18 +365,11 @@ class OfflineFirstChatRepository @Inject constructor(
lookIntoFuture: Boolean, lookIntoFuture: Boolean,
showUnreadMessagesMarker: Boolean showUnreadMessagesMarker: Boolean
) { ) {
receivedChatMessages.forEach {
Log.d(TAG, "receivedChatMessage: " + it.message)
}
// remove all temp messages from UI // remove all temp messages from UI
val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId) val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId)
.first() .first()
.map(ChatMessageEntity::asModel) .map(ChatMessageEntity::asModel)
oldTempMessages.forEach { oldTempMessages.forEach { _removeMessageFlow.emit(it) }
Log.d(TAG, "oldTempMessage to be removed from UI: " + it.message)
_removeMessageFlow.emit(it)
}
// add new messages to UI // add new messages to UI
val tripleChatMessages = Triple(lookIntoFuture, showUnreadMessagesMarker, receivedChatMessages) 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 // remove temp messages from DB that are now found in the new messages
val chatMessagesReferenceIds = receivedChatMessages.mapTo(HashSet(receivedChatMessages.size)) { it.referenceId } val chatMessagesReferenceIds = receivedChatMessages.mapTo(HashSet(receivedChatMessages.size)) { it.referenceId }
val tempChatMessagesThatCanBeReplaced = oldTempMessages.filter { it.referenceId in chatMessagesReferenceIds } val tempChatMessagesThatCanBeReplaced = oldTempMessages.filter { it.referenceId in chatMessagesReferenceIds }
tempChatMessagesThatCanBeReplaced.forEach {
Log.d(TAG, "oldTempMessage that was identified in newMessages: " + it.message)
}
chatDao.deleteTempChatMessages( chatDao.deleteTempChatMessages(
internalConversationId, internalConversationId,
tempChatMessagesThatCanBeReplaced.map { it.referenceId!! } tempChatMessagesThatCanBeReplaced.map { it.referenceId!! }
@ -401,10 +389,6 @@ class OfflineFirstChatRepository @Inject constructor(
.sortedBy { it.internalId } .sortedBy { it.internalId }
.map(ChatMessageEntity::asModel) .map(ChatMessageEntity::asModel)
remainingTempMessages.forEach {
Log.d(TAG, "remainingTempMessage: " + it.message)
}
val triple = Triple(true, false, remainingTempMessages) val triple = Triple(true, false, remainingTempMessages)
_messageFlow.emit(triple) _messageFlow.emit(triple)
} }
@ -859,17 +843,6 @@ class OfflineFirstChatRepository @Inject constructor(
val chatMessageModel = response.ocs?.data?.asModel() 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)) emit(Result.success(chatMessageModel))
} }
.catch { e -> .catch { e ->
@ -880,7 +853,7 @@ class OfflineFirstChatRepository @Inject constructor(
referenceId referenceId
).firstOrNull() ).firstOrNull()
failedMessage?.let { failedMessage?.let {
it.sendStatus = SendStatus.FAILED it.sendingFailed = true
chatDao.updateChatMessage(it) chatDao.updateChatMessage(it)
val failedMessageModel = it.asModel() val failedMessageModel = it.asModel()
@ -900,15 +873,14 @@ class OfflineFirstChatRepository @Inject constructor(
sendWithoutNotification: Boolean, sendWithoutNotification: Boolean,
referenceId: String referenceId: String
): Flow<Result<ChatMessage?>> { ): Flow<Result<ChatMessage?>> {
val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).firstOrNull() val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first()
return if (messageToResend != null) { messageToResend.sendingFailed = false
messageToResend.sendStatus = SendStatus.PENDING
chatDao.updateChatMessage(messageToResend) chatDao.updateChatMessage(messageToResend)
val messageToResendModel = messageToResend.asModel() val messageToResendModel = messageToResend.asModel()
_updateMessageFlow.emit(messageToResendModel) _updateMessageFlow.emit(messageToResendModel)
sendChatMessage( return sendChatMessage(
credentials, credentials,
url, url,
message, message,
@ -917,11 +889,6 @@ class OfflineFirstChatRepository @Inject constructor(
sendWithoutNotification, sendWithoutNotification,
referenceId referenceId
) )
} else {
flow {
emit(Result.failure(IllegalStateException("No temporary message found to resend")))
}
}
} }
@Suppress("Detekt.TooGenericExceptionCaught") @Suppress("Detekt.TooGenericExceptionCaught")
@ -963,8 +930,8 @@ class OfflineFirstChatRepository @Inject constructor(
} }
} }
override suspend fun sendUnsentChatMessages(credentials: String, url: String) { override suspend fun sendTempChatMessages(credentials: String, url: String) {
val tempMessages = chatDao.getTempUnsentMessagesForConversation(internalConversationId).first() val tempMessages = chatDao.getTempMessagesForConversation(internalConversationId).first()
tempMessages.sortedBy { it.internalId }.onEach { tempMessages.sortedBy { it.internalId }.onEach {
sendChatMessage( sendChatMessage(
credentials, credentials,
@ -1058,7 +1025,7 @@ class OfflineFirstChatRepository @Inject constructor(
actorDisplayName = currentUser.displayName!!, actorDisplayName = currentUser.displayName!!,
referenceId = referenceId, referenceId = referenceId,
isTemporary = true, isTemporary = true,
sendStatus = SendStatus.PENDING, sendingFailed = false,
silent = sendWithoutNotification silent = sendWithoutNotification
) )
return entity return entity

View File

@ -217,9 +217,4 @@ class RetrofitChatNetwork(
extractedLinkToPreview extractedLinkToPreview
).blockingFirst().ocs?.data?.references?.entries?.iterator()?.next()?.value ).blockingFirst().ocs?.data?.references?.entries?.iterator()?.next()?.value
} }
override suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall {
val url = ApiUtils.getUrlForUnbindingRoom(baseUrl, roomToken)
return ncApiCoroutines.unbindRoom(credentials, url)
}
} }

View File

@ -146,10 +146,6 @@ class ChatViewModel @Inject constructor(
val outOfOfficeViewState: LiveData<OutOfOfficeUIState> val outOfOfficeViewState: LiveData<OutOfOfficeUIState>
get() = _outOfOfficeViewState get() = _outOfOfficeViewState
private val _unbindRoomResult = MutableLiveData<UnbindRoomUiState>(UnbindRoomUiState.None)
val unbindRoomResult: LiveData<UnbindRoomUiState>
get() = _unbindRoomResult
private val _voiceMessagePlaybackSpeedPreferences: MutableLiveData<Map<String, PlaybackSpeed>> = MutableLiveData() private val _voiceMessagePlaybackSpeedPreferences: MutableLiveData<Map<String, PlaybackSpeed>> = MutableLiveData()
val voiceMessagePlaybackSpeedPreferences: LiveData<Map<String, PlaybackSpeed>> val voiceMessagePlaybackSpeedPreferences: LiveData<Map<String, PlaybackSpeed>>
get() = _voiceMessagePlaybackSpeedPreferences get() = _voiceMessagePlaybackSpeedPreferences
@ -804,18 +800,6 @@ class ChatViewModel @Inject constructor(
} }
} }
@Suppress("Detekt.TooGenericExceptionCaught")
fun unbindRoom(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = chatNetworkDataSource.unbindRoom(credentials, baseUrl, roomToken)
_unbindRoomResult.value = UnbindRoomUiState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_unbindRoomResult.value = UnbindRoomUiState.Error(exception.message.toString())
}
}
}
fun resendMessage(credentials: String, urlForChat: String, message: ChatMessage) { fun resendMessage(credentials: String, urlForChat: String, message: ChatMessage) {
viewModelScope.launch { viewModelScope.launch {
chatRepository.resendChatMessage( chatRepository.resendChatMessage(
@ -867,10 +851,4 @@ class ChatViewModel @Inject constructor(
data class Success(val userAbsence: UserAbsenceData) : OutOfOfficeUIState() data class Success(val userAbsence: UserAbsenceData) : OutOfOfficeUIState()
data class Error(val exception: Exception) : OutOfOfficeUIState() data class Error(val exception: Exception) : OutOfOfficeUIState()
} }
sealed class UnbindRoomUiState {
data object None : UnbindRoomUiState()
data class Success(val statusCode: Int) : UnbindRoomUiState()
data class Error(val message: String) : UnbindRoomUiState()
}
} }

View File

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

View File

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

View File

@ -1,7 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de> * SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */

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 autodagger.AutoInjector
import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication 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.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import javax.inject.Inject import javax.inject.Inject
@ -64,11 +64,11 @@ class ContactsActivity : BaseActivity() {
MaterialTheme( MaterialTheme(
colorScheme = colorScheme colorScheme = colorScheme
) { ) {
ColoredStatusBar()
ContactsScreen( ContactsScreen(
contactsViewModel = contactsViewModel, contactsViewModel = contactsViewModel,
uiState = uiState.value uiState = uiState.value
) )
SetupSystemBars()
} }
} }
} }

View File

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

View File

@ -36,15 +36,6 @@ class ContactsViewModel @Inject constructor(
private val _isAddParticipantsView = MutableStateFlow(false) private val _isAddParticipantsView = MutableStateFlow(false)
val isAddParticipantsView: StateFlow<Boolean> = _isAddParticipantsView 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 private var hideAlreadyAddedParticipants: Boolean = false
init { init {
@ -55,28 +46,14 @@ class ContactsViewModel @Inject constructor(
_searchQuery.value = query _searchQuery.value = query
} }
fun modifyClickAddButton(value: Boolean) {
_clickAddButton.value = value
}
fun selectContact(contact: AutocompleteUser) { fun selectContact(contact: AutocompleteUser) {
val updatedParticipants = selectedParticipants.value + contact val updatedParticipants = selectedParticipants.value + contact
selectedParticipants.value = updatedParticipants 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) { fun deselectContact(contact: AutocompleteUser) {
val updatedParticipants = selectedParticipants.value - contact val updatedParticipants = selectedParticipants.value - contact
selectedParticipants.value = updatedParticipants selectedParticipants.value = updatedParticipants
_selectedContacts.value = _selectedContacts.value - contact
} }
fun updateSelectedParticipants(participants: List<AutocompleteUser>) { fun updateSelectedParticipants(participants: List<AutocompleteUser>) {
@ -99,23 +76,20 @@ class ContactsViewModel @Inject constructor(
} }
@Suppress("Detekt.TooGenericExceptionCaught") @Suppress("Detekt.TooGenericExceptionCaught")
fun getContactsFromSearchParams(query: String = "") { fun getContactsFromSearchParams() {
_contactsViewState.value = ContactsUiState.Loading _contactsViewState.value = ContactsUiState.Loading
viewModelScope.launch { viewModelScope.launch {
try { try {
val contacts = repository.getContacts( val contacts = repository.getContacts(
if (query != "") query else searchQuery.value, searchQuery.value,
shareTypeList shareTypeList
) )
val contactsList: MutableList<AutocompleteUser>? = contacts.ocs!!.data?.toMutableList() val contactsList: MutableList<AutocompleteUser>? = contacts.ocs!!.data?.toMutableList()
if (hideAlreadyAddedParticipants && !_clickAddButton.value) { if (hideAlreadyAddedParticipants) {
contactsList?.removeAll(selectedParticipants.value) contactsList?.removeAll(selectedParticipants.value)
} }
if (_clickAddButton.value) {
contactsList?.removeAll(selectedParticipants.value)
contactsList?.addAll(_selectedContacts.value)
}
_contactsViewState.value = ContactsUiState.Success(contactsList) _contactsViewState.value = ContactsUiState.Success(contactsList)
} catch (exception: Exception) { } catch (exception: Exception) {
_contactsViewState.value = ContactsUiState.Error(exception.message ?: "") _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 isSelected = !isSelected
if (isSelected) { if (isSelected) {
contactsViewModel.selectContact(contact) contactsViewModel.selectContact(contact)
contactsViewModel.updateAddButtonState()
} else { } else {
contactsViewModel.deselectContact(contact) 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.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@ -43,13 +44,9 @@ fun ContactsItem(contacts: List<AutocompleteUser>, contactsViewModel: ContactsVi
} }
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.padding(8.dp)
.fillMaxWidth(), .fillMaxWidth(),
contentPadding = PaddingValues( contentPadding = PaddingValues(all = 10.dp),
top = 10.dp,
bottom = 40.dp,
start = 10.dp,
end = 10.dp
),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
groupedContacts.forEach { (initial, contactsForInitial) -> 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 Your Name <your@email.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.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.contacts.ContactsActivity import com.nextcloud.talk.contacts.ContactsActivity
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.contacts.loadImage import com.nextcloud.talk.contacts.loadImage
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@ -117,6 +117,7 @@ class ConversationCreationActivity : BaseActivity() {
colorScheme = colorScheme colorScheme = colorScheme
) { ) {
ConversationCreationScreen(conversationCreationViewModel, context, pickImage) ConversationCreationScreen(conversationCreationViewModel, context, pickImage)
SetupSystemBars()
} }
} }
} }
@ -171,7 +172,6 @@ fun ConversationCreationScreen(
} }
) )
ColoredStatusBar()
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -191,7 +191,7 @@ fun ConversationCreationScreen(
content = { paddingValues -> content = { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
.padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp) .padding(paddingValues)
.background(colorResource(id = R.color.bg_default)) .background(colorResource(id = R.color.bg_default))
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
@ -289,7 +289,7 @@ fun UploadAvatar(
} }
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_folder), painter = painterResource(id = R.drawable.ic_mimetype_folder),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )

View File

@ -103,11 +103,6 @@ import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Calendar import java.util.Calendar
import java.util.Collections import java.util.Collections
import java.util.Locale import java.util.Locale
@ -155,7 +150,7 @@ class ConversationInfoActivity :
get() { get() {
if (!TextUtils.isEmpty(conversationToken)) { if (!TextUtils.isEmpty(conversationToken)) {
val data = Data.Builder() val data = Data.Builder()
data.putString(KEY_ROOM_TOKEN, conversationToken) data.putString(BundleKeys.KEY_ROOM_TOKEN, conversationToken)
data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!) data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!)
return data.build() return data.build()
} }
@ -190,14 +185,14 @@ class ConversationInfoActivity :
binding = ActivityConversationInfoBinding.inflate(layoutInflater) binding = ActivityConversationInfoBinding.inflate(layoutInflater)
setupActionBar() setupActionBar()
setContentView(binding.root) setContentView(binding.root)
initSystemBars() setupSystemColors()
viewModel = viewModel =
ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java] ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
conversationUser = currentUserProvider.currentUser.blockingGet() conversationUser = currentUserProvider.currentUser.blockingGet()
conversationToken = intent.getStringExtra(KEY_ROOM_TOKEN)!! conversationToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!!
hasAvatarSpacing = intent.getBooleanExtra(BundleKeys.KEY_ROOM_ONE_TO_ONE, false) hasAvatarSpacing = intent.getBooleanExtra(BundleKeys.KEY_ROOM_ONE_TO_ONE, false)
credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)!! credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)!!
} }
@ -250,51 +245,8 @@ class ConversationInfoActivity :
initBanActorObserver() initBanActorObserver()
initConversationReadOnlyObserver() initConversationReadOnlyObserver()
initClearChatHistoryObserver() initClearChatHistoryObserver()
initMarkConversationAsSensitiveObserver()
initMarkConversationAsInsensitiveObserver()
initMarkConversationAsImportantObserver()
initMarkConversationAsUnimportantObserver()
} }
private fun initMarkConversationAsSensitiveObserver() {
viewModel.markAsSensitiveResult.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.MarkConversationAsSensitiveViewState.Success -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_mark_conversation_as_sensitive),
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.MarkConversationAsSensitiveViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to mark conversation as insensitive", uiState.exception)
}
else -> {
}
}
}
}
private fun initMarkConversationAsInsensitiveObserver() {
viewModel.markAsInsensitiveResult.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.MarkConversationAsInsensitiveViewState.Success -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_mark_conversation_as_insensitive),
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.MarkConversationAsInsensitiveViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to mark conversation as sensitive", uiState.exception)
}
else -> {
}
}
}
}
private fun initClearChatHistoryObserver() { private fun initClearChatHistoryObserver() {
viewModel.clearChatHistoryViewState.observe(this) { uiState -> viewModel.clearChatHistoryViewState.observe(this) { uiState ->
when (uiState) { when (uiState) {
@ -383,47 +335,6 @@ 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")
private fun initViewStateObserver() { private fun initViewStateObserver() {
viewModel.viewState.observe(this) { state -> viewModel.viewState.observe(this) { state ->
when (state) { when (state) {
@ -443,12 +354,6 @@ class ConversationInfoActivity :
canGeneratePrettyURL canGeneratePrettyURL
) )
} }
conversation?.let {
if (it.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
viewModel.getProfileData(conversationUser, it.name)
}
}
} }
is ConversationInfoViewModel.GetRoomErrorState -> { is ConversationInfoViewModel.GetRoomErrorState -> {
@ -458,57 +363,6 @@ class ConversationInfoActivity :
else -> {} else -> {}
} }
} }
viewModel.getProfileViewState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.GetProfileSuccessState -> {
try {
// 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
// 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)
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()
}
else -> {}
}
}
} }
private fun setupActionBar() { private fun setupActionBar() {
@ -550,7 +404,7 @@ class ConversationInfoActivity :
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.edit) { if (item.itemId == R.id.edit) {
val bundle = Bundle() val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, conversationToken) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversationToken)
val intent = Intent(this, ConversationInfoEditActivity::class.java) val intent = Intent(this, ConversationInfoEditActivity::class.java)
intent.putExtras(bundle) intent.putExtras(bundle)
@ -567,9 +421,7 @@ class ConversationInfoActivity :
binding.notificationSettingsView.importantConversationSwitch, binding.notificationSettingsView.importantConversationSwitch,
binding.guestAccessView.allowGuestsSwitch, binding.guestAccessView.allowGuestsSwitch,
binding.guestAccessView.passwordProtectionSwitch, binding.guestAccessView.passwordProtectionSwitch,
binding.recordingConsentView.recordingConsentForConversationSwitch, binding.recordingConsentView.recordingConsentForConversationSwitch
binding.lockConversationSwitch,
binding.notificationSettingsView.sensitiveConversationSwitch
).forEach(viewThemeUtils.talk::colorSwitch) ).forEach(viewThemeUtils.talk::colorSwitch)
} }
} }
@ -593,7 +445,7 @@ class ConversationInfoActivity :
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra(BundleKeys.KEY_CONVERSATION_NAME, conversation?.displayName) intent.putExtra(BundleKeys.KEY_CONVERSATION_NAME, conversation?.displayName)
intent.putExtra(KEY_ROOM_TOKEN, conversationToken) intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, conversationToken)
intent.putExtra( intent.putExtra(
SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR, SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR,
ConversationUtils.isParticipantOwnerOrModerator(conversation!!) ConversationUtils.isParticipantOwnerOrModerator(conversation!!)
@ -686,7 +538,7 @@ class ConversationInfoActivity :
) { ) {
binding.webinarInfoView.startTimeButtonSummary.text = ( binding.webinarInfoView.startTimeButtonSummary.text = (
dateUtils.getLocalDateTimeStringFromTimestamp( dateUtils.getLocalDateTimeStringFromTimestamp(
conversation!!.lobbyTimer * DateConstants.SECOND_DIVIDER conversation!!.lobbyTimer!! * DateConstants.SECOND_DIVIDER
) )
) )
} else { } else {
@ -882,7 +734,9 @@ class ConversationInfoActivity :
private fun selectParticipantsToAdd() { private fun selectParticipantsToAdd() {
val bundle = Bundle() val bundle = Bundle()
val existingParticipants = ArrayList<AutocompleteUser>() val existingParticipants = ArrayList<AutocompleteUser>()
for (userItem in userItems) { for (userItem in userItems) {
if (userItem.model.calculatedActorType == USERS) {
val user = AutocompleteUser( val user = AutocompleteUser(
userItem.model.calculatedActorId!!, userItem.model.calculatedActorId!!,
userItem.model.displayName, userItem.model.displayName,
@ -890,6 +744,7 @@ class ConversationInfoActivity :
) )
existingParticipants.add(user) existingParticipants.add(user)
} }
}
bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true) bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true)
bundle.putParcelableArrayList("selectedParticipants", existingParticipants) bundle.putParcelableArrayList("selectedParticipants", existingParticipants)
@ -1065,13 +920,21 @@ class ConversationInfoActivity :
) { ) {
binding.addParticipantsAction.visibility = GONE binding.addParticipantsAction.visibility = GONE
binding.startGroupChat.visibility = VISIBLE binding.startGroupChat.visibility = VISIBLE
showDeleteAllMessagesOption(conversationCopy)
} else if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) { } else if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) {
binding.addParticipantsAction.visibility = VISIBLE binding.addParticipantsAction.visibility = VISIBLE
showDeleteAllMessagesOption(conversationCopy) if (hasSpreedFeatureCapability(
spreedCapabilities,
SpreedFeatures.CLEAR_HISTORY
) && conversationCopy.canDeleteConversation
) {
binding.clearConversationHistory.visibility = VISIBLE
} else {
binding.clearConversationHistory.visibility = GONE
}
showOptionsMenu() showOptionsMenu()
} else { } else {
binding.addParticipantsAction.visibility = GONE binding.addParticipantsAction.visibility = GONE
if (ConversationUtils.isNoteToSelfConversation(conversation)) { if (ConversationUtils.isNoteToSelfConversation(conversation)) {
binding.notificationSettingsView.notificationSettings.visibility = VISIBLE binding.notificationSettingsView.notificationSettings.visibility = VISIBLE
} else { } else {
@ -1079,31 +942,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)) { if (!hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.ARCHIVE_CONVERSATIONS)) {
binding.archiveConversationBtn.visibility = GONE binding.archiveConversationBtn.visibility = GONE
binding.archiveConversationTextHint.visibility = GONE binding.archiveConversationTextHint.visibility = GONE
@ -1139,31 +977,6 @@ class ConversationInfoActivity :
binding.archiveConversationText.text = resources.getString(R.string.archive_conversation) binding.archiveConversationText.text = resources.getString(R.string.archive_conversation)
binding.archiveConversationTextHint.text = resources.getString(R.string.archive_hint) binding.archiveConversationTextHint.text = resources.getString(R.string.archive_hint)
} }
binding.notificationSettingsView.sensitiveConversationSwitch.isChecked = conversation!!.hasSensitive
binding.notificationSettingsView.notificationSettingsSensitiveConversation.setOnClickListener {
val isChecked = !binding.notificationSettingsView.sensitiveConversationSwitch.isChecked
binding.notificationSettingsView.sensitiveConversationSwitch.isChecked = isChecked
if (isChecked) {
viewModel.markConversationAsSensitive(
credentials,
conversationUser.baseUrl!!,
conversation?.token!!
)
} else {
viewModel.markConversationAsInsensitive(
credentials,
conversationUser.baseUrl!!,
conversation?.token!!
)
}
}
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SENSITIVE_CONVERSATIONS)) {
binding.notificationSettingsView.notificationSettingsSensitiveConversation.visibility = VISIBLE
} else {
binding.notificationSettingsView.notificationSettingsSensitiveConversation.visibility = GONE
}
if (ConversationUtils.isConversationReadOnlyAvailable(conversationCopy, spreedCapabilities)) { if (ConversationUtils.isConversationReadOnlyAvailable(conversationCopy, spreedCapabilities)) {
binding.lockConversation.visibility = VISIBLE binding.lockConversation.visibility = VISIBLE
binding.lockConversationSwitch.isChecked = databaseStorageModule!!.getBoolean("lock_switch", false) binding.lockConversationSwitch.isChecked = databaseStorageModule!!.getBoolean("lock_switch", false)
@ -1226,7 +1039,7 @@ class ConversationInfoActivity :
binding.displayNameText.text = conversation!!.displayName binding.displayNameText.text = conversation!!.displayName
if (conversation!!.description != null && conversation!!.description.isNotEmpty()) { if (conversation!!.description != null && conversation!!.description!!.isNotEmpty()) {
binding.descriptionText.text = conversation!!.description binding.descriptionText.text = conversation!!.description
binding.conversationDescription.visibility = VISIBLE binding.conversationDescription.visibility = VISIBLE
} }
@ -1303,18 +1116,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() { private fun submitRecordingConsentChanges() {
val state = if (binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked) { val state = if (binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked) {
RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION
@ -1384,7 +1185,7 @@ class ConversationInfoActivity :
val stringValue: String = val stringValue: String =
when ( when (
DomainEnumNotificationLevelConverter() DomainEnumNotificationLevelConverter()
.convertToInt(conversation!!.notificationLevel) .convertToInt(conversation!!.notificationLevel!!)
) { ) {
NOTIFICATION_LEVEL_ALWAYS -> resources.getString(R.string.nc_notify_me_always) NOTIFICATION_LEVEL_ALWAYS -> resources.getString(R.string.nc_notify_me_always)
NOTIFICATION_LEVEL_MENTION -> resources.getString(R.string.nc_notify_me_mention) NOTIFICATION_LEVEL_MENTION -> resources.getString(R.string.nc_notify_me_mention)
@ -1433,7 +1234,7 @@ class ConversationInfoActivity :
conversation!!.name conversation!!.name
) )
) { ) {
conversation!!.name.let { conversation!!.name?.let {
binding.avatarImage.loadUserAvatar( binding.avatarImage.loadUserAvatar(
conversationUser, conversationUser,
it, it,
@ -1847,6 +1648,13 @@ class ConversationInfoActivity :
} }
private fun setUpNotificationSettings(module: DatabaseStorageModule) { 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 { binding.notificationSettingsView.notificationSettingsCallNotifications.setOnClickListener {
val isChecked = binding.notificationSettingsView.callNotificationsSwitch.isChecked val isChecked = binding.notificationSettingsView.callNotificationsSwitch.isChecked
binding.notificationSettingsView.callNotificationsSwitch.isChecked = !isChecked binding.notificationSettingsView.callNotificationsSwitch.isChecked = !isChecked
@ -1854,7 +1662,6 @@ class ConversationInfoActivity :
module.saveBoolean("call_notifications_switch", !isChecked) module.saveBoolean("call_notifications_switch", !isChecked)
} }
} }
binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown
.setOnItemClickListener { _, _, position, _ -> .setOnItemClickListener { _, _, position, _ ->
val value = resources.getStringArray(R.array.message_notification_levels_entry_values)[position] val value = resources.getStringArray(R.array.message_notification_levels_entry_values)[position]
@ -1864,6 +1671,9 @@ class ConversationInfoActivity :
} }
} }
binding.notificationSettingsView.importantConversationSwitch.isChecked = module
.getBoolean("important_conversation_switch", false)
if (conversation!!.remoteServer.isNullOrEmpty()) { if (conversation!!.remoteServer.isNullOrEmpty()) {
binding.notificationSettingsView.notificationSettingsCallNotifications.visibility = VISIBLE binding.notificationSettingsView.notificationSettingsCallNotifications.visibility = VISIBLE
binding.notificationSettingsView.callNotificationsSwitch.isChecked = module binding.notificationSettingsView.callNotificationsSwitch.isChecked = module

View File

@ -28,7 +28,6 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.EMAILS
import com.nextcloud.talk.models.json.participants.Participant.ActorType.FEDERATED import com.nextcloud.talk.models.json.participants.Participant.ActorType.FEDERATED
import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS
import com.nextcloud.talk.models.json.participants.TalkBan import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.profile.Profile
import com.nextcloud.talk.repositories.conversations.ConversationsRepository import com.nextcloud.talk.repositories.conversations.ConversationsRepository
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ApiUtils.getUrlForRooms import com.nextcloud.talk.utils.ApiUtils.getUrlForRooms
@ -124,40 +123,10 @@ class ConversationInfoViewModel @Inject constructor(
val getConversationReadOnlyState: LiveData<SetConversationReadOnlyViewState> val getConversationReadOnlyState: LiveData<SetConversationReadOnlyViewState>
get() = _getConversationReadOnlyState 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) private val _createRoomViewState = MutableLiveData<CreateRoomUIState>(CreateRoomUIState.None)
val createRoomViewState: LiveData<CreateRoomUIState> val createRoomViewState: LiveData<CreateRoomUIState>
get() = _createRoomViewState get() = _createRoomViewState
object GetProfileErrorState : ViewState
class GetProfileSuccessState(val profile: Profile) : ViewState
private val _getProfileViewState = MutableLiveData<ViewState>()
val getProfileViewState: LiveData<ViewState>
get() = _getProfileViewState
@Suppress("PropertyName")
private val _markConversationAsSensitiveResult =
MutableLiveData<MarkConversationAsSensitiveViewState>(MarkConversationAsSensitiveViewState.None)
val markAsSensitiveResult: LiveData<MarkConversationAsSensitiveViewState>
get() = _markConversationAsSensitiveResult
@Suppress("PropertyName")
private val _markConversationAsInsensitiveResult =
MutableLiveData<MarkConversationAsInsensitiveViewState>(MarkConversationAsInsensitiveViewState.None)
val markAsInsensitiveResult: LiveData<MarkConversationAsInsensitiveViewState>
get() = _markConversationAsInsensitiveResult
fun getRoom(user: User, token: String) { fun getRoom(user: User, token: String) {
_viewState.value = GetRoomStartState _viewState.value = GetRoomStartState
chatNetworkDataSource.getRoom(user, token) chatNetworkDataSource.getRoom(user, token)
@ -319,23 +288,6 @@ 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)
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught") @Suppress("Detekt.TooGenericExceptionCaught")
fun allowGuests(token: String, allow: Boolean) { fun allowGuests(token: String, allow: Boolean) {
viewModelScope.launch { viewModelScope.launch {
@ -373,34 +325,6 @@ class ConversationInfoViewModel @Inject constructor(
conversationsRepository.unarchiveConversation(user.getCredentials(), url) 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") @Suppress("Detekt.TooGenericExceptionCaught")
fun clearChatHistory(apiVersion: Int, roomToken: String) { fun clearChatHistory(apiVersion: Int, roomToken: String) {
viewModelScope.launch { viewModelScope.launch {
@ -413,34 +337,6 @@ class ConversationInfoViewModel @Inject constructor(
} }
} }
@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsSensitive(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = conversationsRepository.markConversationAsSensitive(credentials, baseUrl, roomToken)
_markConversationAsSensitiveResult.value =
MarkConversationAsSensitiveViewState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_markConversationAsSensitiveResult.value =
MarkConversationAsSensitiveViewState.Error(exception)
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsInsensitive(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = conversationsRepository.markConversationAsInsensitive(credentials, baseUrl, roomToken)
_markConversationAsInsensitiveResult.value =
MarkConversationAsInsensitiveViewState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_markConversationAsInsensitiveResult.value =
MarkConversationAsInsensitiveViewState.Error(exception)
}
}
}
inner class GetRoomObserver : Observer<ConversationModel> { inner class GetRoomObserver : Observer<ConversationModel> {
override fun onSubscribe(d: Disposable) { override fun onSubscribe(d: Disposable) {
// unused atm // unused atm
@ -490,18 +386,6 @@ class ConversationInfoViewModel @Inject constructor(
data class Error(val exception: Exception) : ClearChatHistoryViewState() data class Error(val exception: Exception) : ClearChatHistoryViewState()
} }
sealed class MarkConversationAsSensitiveViewState {
data object None : MarkConversationAsSensitiveViewState()
data class Success(val statusCode: Int) : MarkConversationAsSensitiveViewState()
data class Error(val exception: Exception) : MarkConversationAsSensitiveViewState()
}
sealed class MarkConversationAsInsensitiveViewState {
data object None : MarkConversationAsInsensitiveViewState()
data class Success(val statusCode: Int) : MarkConversationAsInsensitiveViewState()
data class Error(val exception: Exception) : MarkConversationAsInsensitiveViewState()
}
sealed class SetConversationReadOnlyViewState { sealed class SetConversationReadOnlyViewState {
data object None : SetConversationReadOnlyViewState() data object None : SetConversationReadOnlyViewState()
data object Success : SetConversationReadOnlyViewState() data object Success : SetConversationReadOnlyViewState()
@ -525,16 +409,4 @@ class ConversationInfoViewModel @Inject constructor(
data object Success : PasswordUiState() data object Success : PasswordUiState()
data class Error(val exception: Exception) : 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) binding = ActivityConversationInfoEditBinding.inflate(layoutInflater)
setupActionBar() setupActionBar()
setContentView(binding.root) setContentView(binding.root)
initSystemBars() setupSystemColors()
val extras: Bundle? = intent.extras val extras: Bundle? = intent.extras

View File

@ -16,6 +16,7 @@ import android.animation.AnimatorInflater
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.SearchManager import android.app.SearchManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
@ -40,12 +41,9 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.os.bundleOf
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.core.view.MenuItemCompat import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
@ -81,7 +79,6 @@ import com.nextcloud.talk.adapters.items.GenericTextHeaderItem
import com.nextcloud.talk.adapters.items.LoadMoreResultsItem import com.nextcloud.talk.adapters.items.LoadMoreResultsItem
import com.nextcloud.talk.adapters.items.MessageResultItem import com.nextcloud.talk.adapters.items.MessageResultItem
import com.nextcloud.talk.adapters.items.MessagesTextHeaderItem import com.nextcloud.talk.adapters.items.MessagesTextHeaderItem
import com.nextcloud.talk.adapters.items.SpacerItem
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
@ -117,9 +114,6 @@ import com.nextcloud.talk.ui.dialog.ChooseAccountShareToDialogFragment
import com.nextcloud.talk.ui.dialog.ContextChatCompose import com.nextcloud.talk.ui.dialog.ContextChatCompose
import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog
import com.nextcloud.talk.ui.dialog.FilterConversationFragment import com.nextcloud.talk.ui.dialog.FilterConversationFragment
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.ARCHIVE
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.MENTION
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.UNREAD
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.BrandingUtils import com.nextcloud.talk.utils.BrandingUtils
@ -214,7 +208,7 @@ class ConversationsListActivity :
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
private var conversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList() private var conversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var conversationItemsWithHeader: 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 filterableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var nearFutureEventConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList() private var nearFutureEventConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var searchItem: MenuItem? = null private var searchItem: MenuItem? = null
@ -238,9 +232,9 @@ class ConversationsListActivity :
private var searchViewDisposable: Disposable? = null private var searchViewDisposable: Disposable? = null
private var filterState = private var filterState =
mutableMapOf( mutableMapOf(
MENTION to false, FilterConversationFragment.MENTION to false,
UNREAD to false, FilterConversationFragment.UNREAD to false,
ARCHIVE to false, FilterConversationFragment.ARCHIVE to false,
FilterConversationFragment.DEFAULT to true FilterConversationFragment.DEFAULT to true
) )
val searchBehaviorSubject = BehaviorSubject.createDefault(false) val searchBehaviorSubject = BehaviorSubject.createDefault(false)
@ -267,7 +261,7 @@ class ConversationsListActivity :
binding = ActivityConversationsBinding.inflate(layoutInflater) binding = ActivityConversationsBinding.inflate(layoutInflater)
setupActionBar() setupActionBar()
setContentView(binding.root) setContentView(binding.root)
initSystemBars() setupSystemColors()
viewThemeUtils.material.themeCardView(binding.searchToolbar) viewThemeUtils.material.themeCardView(binding.searchToolbar)
viewThemeUtils.material.themeSearchBarText(binding.searchText) viewThemeUtils.material.themeSearchBarText(binding.searchText)
@ -294,9 +288,10 @@ class ConversationsListActivity :
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// actionBar?.show()
if (adapter == null) { if (adapter == null) {
adapter = FlexibleAdapter(conversationItems, this, true) adapter = FlexibleAdapter(conversationItems, this, true)
addEmptyItemForEdgeToEdgeIfNecessary()
} else { } else {
binding.loadingContent.visibility = View.GONE binding.loadingContent.visibility = View.GONE
} }
@ -342,14 +337,6 @@ class ConversationsListActivity :
showSearchOrToolbar() 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") @Suppress("LongMethod")
private fun initObservers() { private fun initObservers() {
this.lifecycleScope.launch { this.lifecycleScope.launch {
@ -419,10 +406,6 @@ class ConversationsListActivity :
conversationsListViewModel.getRoomsFlow conversationsListViewModel.getRoomsFlow
.onEach { list -> .onEach { list ->
setConversationList(list) setConversationList(list)
val noteToSelf = list
.firstOrNull { ConversationUtils.isNoteToSelfConversation(it) }
val isNoteToSelfAvailable = noteToSelf != null
handleNoteToSelfShortcut(isNoteToSelfAvailable, noteToSelf?.token ?: "")
}.collect() }.collect()
} }
@ -475,13 +458,7 @@ class ConversationsListActivity :
userItems.add(contactItem) userItems.add(contactItem)
} }
val list = searchableConversationItems.filter { searchableConversationItems.addAll(userItems)
it !is ContactItem
}.toMutableList()
list.addAll(userItems)
searchableConversationItems = list
} }
else -> {} else -> {}
@ -539,29 +516,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>) { private fun setConversationList(list: List<ConversationModel>) {
// Update Conversations // Update Conversations
conversationItems.clear() conversationItems.clear()
@ -569,29 +523,26 @@ class ConversationsListActivity :
nearFutureEventConversationItems.clear() nearFutureEventConversationItems.clear()
for (conversation in list) { for (conversation in list) {
if (!isFutureEvent(conversation) && !conversation.hasArchived) { if (!futureEvent(conversation)) {
addToNearFutureEventConversationItems(conversation) addToNearFutureEventConversationItems(conversation)
} }
addToConversationItems(conversation) addToConversationItems(conversation)
} }
getFilterStates()
val noFiltersActive = !(
filterState[MENTION] == true ||
filterState[UNREAD] == true ||
filterState[ARCHIVE] == true
)
sortConversations(conversationItems) sortConversations(conversationItems)
sortConversations(conversationItemsWithHeader) sortConversations(conversationItemsWithHeader)
sortConversations(nearFutureEventConversationItems) sortConversations(nearFutureEventConversationItems)
if (noFiltersActive && searchBehaviorSubject.value == false) { if (!hasFilterEnabled() && searchBehaviorSubject.value == false) {
adapter?.updateDataSet(nearFutureEventConversationItems, false) adapter?.updateDataSet(nearFutureEventConversationItems, false)
} else { } else {
applyFilter() // Filter Conversations
if (!hasFilterEnabled()) {
filterableConversationItems = conversationItems
}
filterConversation()
adapter?.updateDataSet(filterableConversationItems, false)
} }
Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong()) Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong())
// Fetch Open Conversations // Fetch Open Conversations
@ -600,14 +551,9 @@ class ConversationsListActivity :
intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1) intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)
) )
fetchOpenConversations(apiVersion) fetchOpenConversations(apiVersion)
}
fun applyFilter() { // Get users
if (!hasFilterEnabled()) { fetchUsers()
filterableConversationItems = conversationItems
}
filterConversation()
adapter?.updateDataSet(filterableConversationItems, false)
} }
private fun hasFilterEnabled(): Boolean { private fun hasFilterEnabled(): Boolean {
@ -618,14 +564,13 @@ class ConversationsListActivity :
return false return false
} }
private fun isFutureEvent(conversation: ConversationModel): Boolean { private fun futureEvent(conversation: ConversationModel): Boolean {
if (!conversation.objectId.contains("#")) { if (!conversation.objectId.contains("#")) {
return false return false
} }
val eventTimeStart = conversation.objectId.split("#")[0].toLong() return conversation.objectType == ConversationEnums.ObjectType.EVENT &&
val currentTimeStampInSeconds = System.currentTimeMillis() / LONG_1000 (conversation.objectId.split("#")[0].toLong() - (System.currentTimeMillis() / LONG_1000)) >
val sixteenHoursAfterTimeStamp = (eventTimeStart - currentTimeStampInSeconds) > SIXTEEN_HOURS_IN_SECONDS AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS
return conversation.objectType == ConversationEnums.ObjectType.EVENT && sixteenHoursAfterTimeStamp
} }
fun showOnlyNearFutureEvents() { fun showOnlyNearFutureEvents() {
@ -639,35 +584,32 @@ class ConversationsListActivity :
nearFutureEventConversationItems.add(conversationItem) nearFutureEventConversationItems.add(conversationItem)
} }
fun getFilterStates() {
val accountId = UserIdUtils.getIdForUser(currentUser)
filterState[UNREAD] = (
arbitraryStorageManager.getStorageSetting(
accountId,
UNREAD,
""
).blockingGet()?.value ?: ""
) == "true"
filterState[MENTION] = (
arbitraryStorageManager.getStorageSetting(
accountId,
MENTION,
""
).blockingGet()?.value ?: ""
) == "true"
filterState[ARCHIVE] = (
arbitraryStorageManager.getStorageSetting(
accountId,
ARCHIVE,
""
).blockingGet()?.value ?: ""
) == "true"
}
fun filterConversation() { fun filterConversation() {
getFilterStates() val accountId = UserIdUtils.getIdForUser(currentUser)
filterState[FilterConversationFragment.UNREAD] = (
arbitraryStorageManager.getStorageSetting(
accountId,
FilterConversationFragment.UNREAD,
""
).blockingGet()?.value ?: ""
) == "true"
filterState[FilterConversationFragment.MENTION] = (
arbitraryStorageManager.getStorageSetting(
accountId,
FilterConversationFragment.MENTION,
""
).blockingGet()?.value ?: ""
) == "true"
filterState[FilterConversationFragment.ARCHIVE] = (
arbitraryStorageManager.getStorageSetting(
accountId,
FilterConversationFragment.ARCHIVE,
""
).blockingGet()?.value ?: ""
) == "true"
val newItems: MutableList<AbstractFlexibleItem<*>> = ArrayList() val newItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
val items = conversationItems val items = conversationItems
for (i in items) { for (i in items) {
@ -677,7 +619,7 @@ class ConversationsListActivity :
} }
} }
val archiveFilterOn = filterState[ARCHIVE] == true val archiveFilterOn = filterState[FilterConversationFragment.ARCHIVE] ?: false
if (archiveFilterOn && newItems.isEmpty()) { if (archiveFilterOn && newItems.isEmpty()) {
binding.noArchivedConversationLayout.visibility = View.VISIBLE binding.noArchivedConversationLayout.visibility = View.VISIBLE
} else { } else {
@ -699,7 +641,7 @@ class ConversationsListActivity :
for ((k, v) in filterState) { for ((k, v) in filterState) {
if (v) { if (v) {
when (k) { when (k) {
MENTION -> result = (result && conversation.unreadMention) || FilterConversationFragment.MENTION -> result = (result && conversation.unreadMention) ||
( (
result && result &&
( (
@ -709,10 +651,10 @@ class ConversationsListActivity :
(conversation.unreadMessages > 0) (conversation.unreadMessages > 0)
) )
UNREAD -> result = result && (conversation.unreadMessages > 0) FilterConversationFragment.UNREAD -> result = result && (conversation.unreadMessages > 0)
FilterConversationFragment.DEFAULT -> { FilterConversationFragment.DEFAULT -> {
result = if (filterState[ARCHIVE] == true) { result = if (filterState[FilterConversationFragment.ARCHIVE] == true) {
result && conversation.hasArchived result && conversation.hasArchived
} else { } else {
result && !conversation.hasArchived result && !conversation.hasArchived
@ -792,7 +734,7 @@ class ConversationsListActivity :
} }
private fun initSearchView() { private fun initSearchView() {
val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager? val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
if (searchItem != null) { if (searchItem != null) {
searchView = MenuItemCompat.getActionView(searchItem) as SearchView searchView = MenuItemCompat.getActionView(searchItem) as SearchView
viewThemeUtils.talk.themeSearchView(searchView!!) viewThemeUtils.talk.themeSearchView(searchView!!)
@ -971,7 +913,8 @@ class ConversationsListActivity :
} else { } else {
showToolbar() showToolbar()
} }
initSystemBars() colorizeStatusBar()
colorizeNavigationBar()
} }
} }
@ -1207,8 +1150,8 @@ class ConversationsListActivity :
} }
} }
private fun fetchUsers(query: String = "") { private fun fetchUsers() {
contactsViewModel.getContactsFromSearchParams(query) contactsViewModel.getContactsFromSearchParams()
} }
private fun handleHttpExceptions(throwable: Throwable) { private fun handleHttpExceptions(throwable: Throwable) {
@ -1253,7 +1196,7 @@ class ConversationsListActivity :
}) })
binding.recyclerView.setOnTouchListener { v: View, _: MotionEvent? -> binding.recyclerView.setOnTouchListener { v: View, _: MotionEvent? ->
if (!isDestroyed) { if (!isDestroyed) {
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v.windowToken, 0) imm.hideSoftInputFromWindow(v.windowToken, 0)
} }
false false
@ -1406,9 +1349,6 @@ class ConversationsListActivity :
private fun performFilterAndSearch(filter: String?) { private fun performFilterAndSearch(filter: String?) {
if (filter!!.length >= SEARCH_MIN_CHARS) { if (filter!!.length >= SEARCH_MIN_CHARS) {
clearMessageSearchResults() clearMessageSearchResults()
binding.noArchivedConversationLayout.visibility = View.GONE
fetchUsers(filter)
if (hasFilterEnabled()) { if (hasFilterEnabled()) {
adapter?.updateDataSet(conversationItems) adapter?.updateDataSet(conversationItems)
@ -1416,7 +1356,6 @@ class ConversationsListActivity :
adapter?.filterItems() adapter?.filterItems()
adapter?.updateDataSet(filterableConversationItems) adapter?.updateDataSet(filterableConversationItems)
} else { } else {
adapter?.updateDataSet(searchableConversationItems)
adapter?.setFilter(filter) adapter?.setFilter(filter)
adapter?.filterItems() adapter?.filterItems()
} }
@ -1431,15 +1370,8 @@ class ConversationsListActivity :
private fun resetSearchResults() { private fun resetSearchResults() {
clearMessageSearchResults() clearMessageSearchResults()
adapter?.updateDataSet(conversationItems)
adapter?.setFilter("") adapter?.setFilter("")
adapter?.filterItems() adapter?.filterItems()
val archiveFilterOn = filterState[ARCHIVE] == true
if (archiveFilterOn && adapter!!.isEmpty) {
binding.noArchivedConversationLayout.visibility = View.VISIBLE
} else {
binding.noArchivedConversationLayout.visibility = View.GONE
}
} }
private fun clearMessageSearchResults() { private fun clearMessageSearchResults() {
@ -1448,7 +1380,6 @@ class ConversationsListActivity :
adapter?.removeSection(firstHeader) adapter?.removeSection(firstHeader)
} else { } else {
adapter?.removeItemsOfType(MessageResultItem.VIEW_TYPE) adapter?.removeItemsOfType(MessageResultItem.VIEW_TYPE)
adapter?.removeItemsOfType(MessagesTextHeaderItem.VIEW_TYPE)
} }
adapter?.removeItemsOfType(LoadMoreResultsItem.VIEW_TYPE) adapter?.removeItemsOfType(LoadMoreResultsItem.VIEW_TYPE)
} }
@ -1845,7 +1776,7 @@ class ConversationsListActivity :
val callsChannelNotEnabled = !NotificationUtils.isCallsNotificationChannelEnabled(this) val callsChannelNotEnabled = !NotificationUtils.isCallsNotificationChannelEnabled(this)
val serverNotificationAppInstalled = val serverNotificationAppInstalled =
currentUser?.capabilities?.notificationsCapability?.features?.isNotEmpty() == true currentUser?.capabilities?.notificationsCapability?.features?.isNotEmpty() ?: false
val settingsOfUserAreWrong = notificationPermissionNotGranted || val settingsOfUserAreWrong = notificationPermissionNotGranted ||
batteryOptimizationNotIgnored || batteryOptimizationNotIgnored ||
@ -1974,9 +1905,9 @@ class ConversationsListActivity :
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
.observeForever { workInfo: WorkInfo? -> .observeForever { workInfo: WorkInfo ->
when (workInfo?.state) { when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> { WorkInfo.State.SUCCEEDED -> {
val text = String.format( val text = String.format(
context.resources.getString(R.string.nc_deleted_user), context.resources.getString(R.string.nc_deleted_user),
@ -2146,25 +2077,29 @@ class ConversationsListActivity :
val entries = results.messages val entries = results.messages
if (entries.isNotEmpty()) { if (entries.isNotEmpty()) {
val adapterItems: MutableList<AbstractFlexibleItem<*>> = ArrayList(entries.size + 1) val adapterItems: MutableList<AbstractFlexibleItem<*>> = ArrayList(entries.size + 1)
for (i in entries.indices) { for (i in entries.indices) {
val showHeader = i == 0
adapterItems.add( adapterItems.add(
MessageResultItem( MessageResultItem(
context, context,
currentUser!!, currentUser!!,
entries[i], entries[i],
showHeader,
viewThemeUtils = viewThemeUtils viewThemeUtils = viewThemeUtils
) )
) )
} }
if (results.hasMore) { if (results.hasMore) {
adapterItems.add(LoadMoreResultsItem) adapterItems.add(LoadMoreResultsItem)
} }
adapter?.addItems(Int.MAX_VALUE, adapterItems) adapter?.addItems(Int.MAX_VALUE, adapterItems)
val pos = adapter?.currentItems?.indexOfFirst {
it is MessageResultItem
}
val item = (adapter?.currentItems?.get(pos!!) as MessageResultItem).apply { showHeader = true }
adapter?.addItem(pos!!, item)
adapter?.notifyItemInserted(pos!!)
adapter?.removeItem(pos!! - 1)
adapter?.notifyItemRemoved(pos!! - 1)
binding.recyclerView.scrollToPosition(0) binding.recyclerView.scrollToPosition(0)
} }
} }
@ -2177,8 +2112,8 @@ class ConversationsListActivity :
} }
fun updateFilterState(mention: Boolean, unread: Boolean) { fun updateFilterState(mention: Boolean, unread: Boolean) {
filterState[MENTION] = mention filterState[FilterConversationFragment.MENTION] = mention
filterState[UNREAD] = unread filterState[FilterConversationFragment.UNREAD] = unread
} }
fun setFilterableItems(items: MutableList<AbstractFlexibleItem<*>>) { fun setFilterableItems(items: MutableList<AbstractFlexibleItem<*>>) {
@ -2217,8 +2152,7 @@ class ConversationsListActivity :
const val NOTIFICATION_WARNING_DATE_NOT_SET = 0L const val NOTIFICATION_WARNING_DATE_NOT_SET = 0L
const val OFFSET_HEIGHT_DIVIDER: Int = 3 const val OFFSET_HEIGHT_DIVIDER: Int = 3
const val ROOM_TYPE_ONE_ONE = "1" const val ROOM_TYPE_ONE_ONE = "1"
private const val SIXTEEN_HOURS_IN_SECONDS: Long = 57600 private const val AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS: Long = 57600
const val LONG_1000: Long = 1000 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>> 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( @Query(
""" """
SELECT * SELECT *
@ -72,7 +60,7 @@ interface ChatMessagesDao {
ORDER BY timestamp DESC, id DESC 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) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>) suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>)

View File

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

View File

@ -61,9 +61,7 @@ fun ConversationModel.asEntity() =
recordingConsentRequired = recordingConsentRequired, recordingConsentRequired = recordingConsentRequired,
remoteServer = remoteServer, remoteServer = remoteServer,
remoteToken = remoteToken, remoteToken = remoteToken,
hasArchived = hasArchived, hasArchived = hasArchived
hasSensitive = hasSensitive,
hasImportant = hasImportant
) )
fun ConversationEntity.asModel() = fun ConversationEntity.asModel() =
@ -115,9 +113,7 @@ fun ConversationEntity.asModel() =
recordingConsentRequired = recordingConsentRequired, recordingConsentRequired = recordingConsentRequired,
remoteServer = remoteServer, remoteServer = remoteServer,
remoteToken = remoteToken, remoteToken = remoteToken,
hasArchived = hasArchived, hasArchived = hasArchived
hasSensitive = hasSensitive,
hasImportant = hasImportant
) )
fun Conversation.asEntity(accountId: Long) = fun Conversation.asEntity(accountId: Long) =
@ -168,7 +164,5 @@ fun Conversation.asEntity(accountId: Long) =
recordingConsentRequired = recordingConsentRequired, recordingConsentRequired = recordingConsentRequired,
remoteServer = remoteServer, remoteServer = remoteServer,
remoteToken = remoteToken, remoteToken = remoteToken,
hasArchived = hasArchived, hasArchived = hasArchived
hasSensitive = hasSensitive,
hasImportant = hasImportant
) )

View File

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

View File

@ -94,9 +94,7 @@ data class ConversationEntity(
@ColumnInfo(name = "unreadMention") var unreadMention: Boolean = false, @ColumnInfo(name = "unreadMention") var unreadMention: Boolean = false,
@ColumnInfo(name = "unreadMentionDirect") var unreadMentionDirect: Boolean, @ColumnInfo(name = "unreadMentionDirect") var unreadMentionDirect: Boolean,
@ColumnInfo(name = "unreadMessages") var unreadMessages: Int = 0, @ColumnInfo(name = "unreadMessages") var unreadMessages: Int = 0,
@ColumnInfo(name = "hasArchived") var hasArchived: Boolean = false, @ColumnInfo(name = "hasArchived") var hasArchived: Boolean = false
@ColumnInfo(name = "hasSensitive") var hasSensitive: Boolean = false,
@ColumnInfo(name = "hasImportant") var hasImportant: Boolean = false
// missing/not needed: attendeeId // missing/not needed: attendeeId
// missing/not needed: attendeePin // missing/not needed: attendeePin
// missing/not needed: attendeePermissions // 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 * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024-2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de> * SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
package com.nextcloud.talk.data.source.local package com.nextcloud.talk.data.source.local
import android.util.Log import android.util.Log
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import java.sql.SQLException import java.sql.SQLException
@Suppress("MagicNumber") @Suppress("MagicNumber")
object Migrations { 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) { val MIGRATION_6_8 = object : Migration(6, 8) {
override fun migrate(db: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 6 to 8") Log.i("Migrations", "Migrating 6 to 8")
@ -75,22 +62,6 @@ object Migrations {
} }
} }
val MIGRATION_14_15 = object : Migration(14, 15) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 14 to 15")
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) { fun migrateToRoom(db: SupportSQLiteDatabase) {
db.execSQL( db.execSQL(
"CREATE TABLE User_new (" + "CREATE TABLE User_new (" +
@ -312,28 +283,6 @@ object Migrations {
} }
} }
fun addIsSensitive(db: SupportSQLiteDatabase) {
try {
db.execSQL(
"ALTER TABLE Conversations " +
"ADD COLUMN hasSensitive INTEGER NOT NULL DEFAULT 0;"
)
} catch (e: SQLException) {
Log.i("Migrations", "Something went wrong when adding column hasSensitive to table Conversations")
}
}
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) { fun addTempMessagesSupport(db: SupportSQLiteDatabase) {
try { try {
db.execSQL( db.execSQL(

View File

@ -1,7 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * 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: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2020 Mario Danic <mario@lovelyhq.com> * SPDX-FileCopyrightText: 2017-2020 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later * 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.ChatBlockEntity
import com.nextcloud.talk.data.database.model.ChatMessageEntity import com.nextcloud.talk.data.database.model.ChatMessageEntity
import com.nextcloud.talk.data.database.model.ConversationEntity 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.ArrayListConverter
import com.nextcloud.talk.data.source.local.converters.CapabilitiesConverter 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.ExternalSignalingServerConverter
import com.nextcloud.talk.data.source.local.converters.HashMapHashMapConverter 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.LinkedHashMapConverter
import com.nextcloud.talk.data.source.local.converters.PushConfigurationConverter 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.ServerVersionConverter
import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter
import com.nextcloud.talk.data.storage.ArbitraryStoragesDao import com.nextcloud.talk.data.storage.ArbitraryStoragesDao
@ -51,10 +49,9 @@ import java.util.Locale
ChatMessageEntity::class, ChatMessageEntity::class,
ChatBlockEntity::class ChatBlockEntity::class
], ],
version = 17, version = 14,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 9, to = 10), AutoMigration(from = 9, to = 10)
AutoMigration(from = 16, to = 17, spec = AutoMigration16To17::class)
], ],
exportSchema = true exportSchema = true
) )
@ -66,8 +63,7 @@ import java.util.Locale
SignalingSettingsConverter::class, SignalingSettingsConverter::class,
HashMapHashMapConverter::class, HashMapHashMapConverter::class,
LinkedHashMapConverter::class, LinkedHashMapConverter::class,
ArrayListConverter::class, ArrayListConverter::class
SendStatusConverter::class
) )
abstract class TalkDatabase : RoomDatabase() { abstract class TalkDatabase : RoomDatabase() {
@ -120,9 +116,7 @@ abstract class TalkDatabase : RoomDatabase() {
Migrations.MIGRATION_10_11, Migrations.MIGRATION_10_11,
Migrations.MIGRATION_11_12, Migrations.MIGRATION_11_12,
Migrations.MIGRATION_12_13, Migrations.MIGRATION_12_13,
Migrations.MIGRATION_13_14, Migrations.MIGRATION_13_14
Migrations.MIGRATION_14_15,
Migrations.MIGRATION_15_16
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback( .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.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -29,9 +28,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.core.net.toUri
import autodagger.AutoInjector import autodagger.AutoInjector
import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R 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.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.components.StandardAppBar import com.nextcloud.talk.components.StandardAppBar
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.BrandingUtils import com.nextcloud.talk.utils.BrandingUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl import com.nextcloud.talk.utils.ClosedInterfaceImpl
@ -111,22 +109,18 @@ class DiagnoseActivity : BaseActivity() {
MaterialTheme( MaterialTheme(
colorScheme = colorScheme colorScheme = colorScheme
) { ) {
ColoredStatusBar()
Scaffold( Scaffold(
modifier = Modifier
.statusBarsPadding(),
topBar = { topBar = {
StandardAppBar( StandardAppBar(
title = stringResource(R.string.nc_settings_diagnose_title), title = stringResource(R.string.nc_settings_diagnose_title),
menuItems menuItems
) )
}, },
content = { paddingValues -> content = {
val viewState = diagnoseViewModel.notificationViewState.collectAsState().value val viewState = diagnoseViewModel.notificationViewState.collectAsState().value
Column( Column(
Modifier Modifier
.padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp) .padding(it)
.background(backgroundColor) .background(backgroundColor)
.fillMaxSize() .fillMaxSize()
) { ) {
@ -142,6 +136,7 @@ class DiagnoseActivity : BaseActivity() {
} }
} }
) )
SetupSystemBars()
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,8 +61,6 @@ class ConversationModel(
var remoteServer: String? = null, var remoteServer: String? = null,
var remoteToken: String? = null, var remoteToken: String? = null,
var hasArchived: Boolean = false, var hasArchived: Boolean = false,
var hasSensitive: Boolean = false,
var hasImportant: Boolean = false,
// attributes that don't come from API. This should be changed?! // attributes that don't come from API. This should be changed?!
var password: String? = null var password: String? = null
@ -127,9 +125,7 @@ class ConversationModel(
recordingConsentRequired = conversation.recordingConsentRequired, recordingConsentRequired = conversation.recordingConsentRequired,
remoteServer = conversation.remoteServer, remoteServer = conversation.remoteServer,
remoteToken = conversation.remoteToken, remoteToken = conversation.remoteToken,
hasArchived = conversation.hasArchived, hasArchived = conversation.hasArchived
hasSensitive = conversation.hasSensitive,
hasImportant = conversation.hasImportant
) )
} }
} }

View File

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

View File

@ -165,11 +165,5 @@ data class Conversation(
var remoteToken: String? = "", var remoteToken: String? = "",
@JsonField(name = ["isArchived"]) @JsonField(name = ["isArchived"])
var hasArchived: Boolean = false, var hasArchived: Boolean = false
@JsonField(name = ["isSensitive"])
var hasSensitive: Boolean = false,
@JsonField(name = ["isImportant"])
var hasImportant: Boolean = false
) : Parcelable ) : Parcelable

View File

@ -44,9 +44,6 @@ class ConversationEnums {
SHARE_PASSWORD, SHARE_PASSWORD,
FILE, FILE,
ROOM, ROOM,
EVENT, EVENT
PHONE_TEMPORARY,
PHONE_PERSIST,
INSTANT_MEETING
} }
} }

View File

@ -16,9 +16,6 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter<ConversationEnu
"room" -> ConversationEnums.ObjectType.ROOM "room" -> ConversationEnums.ObjectType.ROOM
"file" -> ConversationEnums.ObjectType.FILE "file" -> ConversationEnums.ObjectType.FILE
"event" -> ConversationEnums.ObjectType.EVENT "event" -> ConversationEnums.ObjectType.EVENT
"phone_persist" -> ConversationEnums.ObjectType.PHONE_PERSIST
"phone_temporary" -> ConversationEnums.ObjectType.PHONE_TEMPORARY
"instant_meeting" -> ConversationEnums.ObjectType.INSTANT_MEETING
else -> ConversationEnums.ObjectType.DEFAULT else -> ConversationEnums.ObjectType.DEFAULT
} }
} }
@ -33,9 +30,6 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter<ConversationEnu
ConversationEnums.ObjectType.ROOM -> "room" ConversationEnums.ObjectType.ROOM -> "room"
ConversationEnums.ObjectType.FILE -> "file" ConversationEnums.ObjectType.FILE -> "file"
ConversationEnums.ObjectType.EVENT -> "event" ConversationEnums.ObjectType.EVENT -> "event"
ConversationEnums.ObjectType.PHONE_PERSIST -> "phone_persist"
ConversationEnums.ObjectType.PHONE_TEMPORARY -> "phone_temporary"
ConversationEnums.ObjectType.INSTANT_MEETING -> "instant_meeting"
else -> "" else -> ""
} }
} }

View File

@ -1,29 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.profile
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class CoreProfileAction(
@JsonField(name = ["id"])
var id: String? = null,
@JsonField(name = ["icon"])
var icon: String? = null,
@JsonField(name = ["title"])
var title: String? = null,
@JsonField(name = ["target"])
var target: String? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null, null, null)
}

View File

@ -1,30 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.profile
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class Profile(
@JsonField(name = ["userId"]) var userId: String? = null,
@JsonField(name = ["address"]) var address: String? = null,
@JsonField(name = ["biography"]) var biography: Int? = null,
@JsonField(name = ["displayname"]) var displayName: Int? = null,
@JsonField(name = ["headline"]) var headline: String? = null,
// @JsonField(name = ["isUserAvatarVisible"]) var isUserAvatarVisible: Boolean = false,
@JsonField(name = ["organisation"]) var company: String? = null,
@JsonField(name = ["pronouns"]) var pronouns: String? = null,
@JsonField(name = ["role"]) var role: String? = null,
@JsonField(name = ["actions"]) var actions: List<CoreProfileAction>? = null,
@JsonField(name = ["timezone"]) var timezone: String? = null,
@JsonField(name = ["timezoneOffset"]) var timezoneOffset: Int? = null
) : Parcelable

View File

@ -1,26 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.profile
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.models.json.generic.GenericMeta
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class ProfileOCS(
@JsonField(name = ["meta"])
var meta: GenericMeta? = null,
@JsonField(name = ["data"])
var data: Profile? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null)
}

View File

@ -1,23 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.profile
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class ProfileOverall(
@JsonField(name = ["ocs"])
var ocs: ProfileOCS? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null)
}

View File

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

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import com.nextcloud.talk.conversationinfo.CreateRoomRequest
import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.TalkBan import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.profile.Profile
import io.reactivex.Observable import io.reactivex.Observable
interface ConversationsRepository { interface ConversationsRepository {
@ -47,14 +46,4 @@ interface ConversationsRepository {
suspend fun clearChatHistory(apiVersion: Int, roomToken: String): GenericOverall suspend fun clearChatHistory(apiVersion: Int, roomToken: String): GenericOverall
suspend fun createRoom(credentials: String, url: String, body: CreateRoomRequest): RoomOverall suspend fun createRoom(credentials: String, url: String, body: CreateRoomRequest): RoomOverall
suspend fun getProfile(credentials: String, url: String): Profile?
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

@ -14,7 +14,6 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.TalkBan import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.profile.Profile
import com.nextcloud.talk.repositories.conversations.ConversationsRepository.ResendInvitationsResult import com.nextcloud.talk.repositories.conversations.ConversationsRepository.ResendInvitationsResult
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
@ -117,46 +116,6 @@ class ConversationsRepositoryImpl(
return response return response
} }
override suspend fun getProfile(credentials: String, url: String): Profile? {
return coroutineApi.getProfile(credentials, url).ocs?.data
}
override suspend fun markConversationAsSensitive(
credentials: String,
baseUrl: String,
roomToken: String
): GenericOverall {
val url = ApiUtils.getUrlForSensitiveConversation(baseUrl, roomToken)
return coroutineApi.markConversationAsSensitive(credentials, url)
}
override suspend fun markConversationAsInsensitive(
credentials: String,
baseUrl: String,
roomToken: String
): GenericOverall {
val url = ApiUtils.getUrlForSensitiveConversation(baseUrl, roomToken)
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( override suspend fun banActor(
credentials: String, credentials: String,
url: String, url: String,

View File

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@
package com.nextcloud.talk.ui package com.nextcloud.talk.ui
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.util.Log import android.util.Log
import android.view.View.TEXT_ALIGNMENT_VIEW_START import android.view.View.TEXT_ALIGNMENT_VIEW_START
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@ -24,7 +23,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -48,7 +46,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -71,18 +68,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.emoji2.widget.EmojiTextView
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import autodagger.AutoInjector import autodagger.AutoInjector
@ -90,8 +84,6 @@ import coil.compose.AsyncImage
import com.elyeproj.loaderviewlibrary.LoaderImageView import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.elyeproj.loaderviewlibrary.LoaderTextView import com.elyeproj.loaderviewlibrary.LoaderTextView
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder.Companion.KEY_MIMETYPE
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
@ -107,9 +99,7 @@ import com.nextcloud.talk.models.json.opengraph.Reference
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType
import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preview.ComposePreviewUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
@ -126,53 +116,40 @@ import kotlin.random.Random
@Suppress("FunctionNaming", "TooManyFunctions", "LongMethod", "StaticFieldLeak", "LargeClass") @Suppress("FunctionNaming", "TooManyFunctions", "LongMethod", "StaticFieldLeak", "LargeClass")
class ComposeChatAdapter( class ComposeChatAdapter(
private var messagesJson: List<ChatMessageJson>? = null, private var messagesJson: List<ChatMessageJson>? = null,
private var messageId: String? = null, private var messageId: String? = null
private val utils: ComposePreviewUtils? = null
) { ) {
interface PreviewAble {
val viewThemeUtils: ViewThemeUtils
val messageUtils: MessageUtils
val contactsViewModel: ContactsViewModel
val chatViewModel: ChatViewModel
val context: Context
val userManager: UserManager
}
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
inner class ComposeChatAdapterViewModel : ViewModel(), PreviewAble { inner class ComposeChatAdapterViewModel : ViewModel() {
@Inject @Inject
override lateinit var viewThemeUtils: ViewThemeUtils lateinit var viewThemeUtils: ViewThemeUtils
@Inject @Inject
override lateinit var messageUtils: MessageUtils lateinit var messageUtils: MessageUtils
@Inject @Inject
override lateinit var contactsViewModel: ContactsViewModel lateinit var contactsViewModel: ContactsViewModel
@Inject @Inject
override lateinit var chatViewModel: ChatViewModel lateinit var chatViewModel: ChatViewModel
@Inject @Inject
override lateinit var context: Context lateinit var context: Context
@Inject @Inject
override lateinit var userManager: UserManager lateinit var userManager: UserManager
val items = mutableStateListOf<ChatMessage>()
init { init {
sharedApplication?.componentApplication?.inject(this) sharedApplication!!.componentApplication.inject(this)
}
} }
inner class ComposeChatAdapterPreviewViewModel( val currentUser: User = userManager.currentUser.blockingGet()
override val viewThemeUtils: ViewThemeUtils, val colorScheme = viewThemeUtils.getColorScheme(context)
override val messageUtils: MessageUtils, val highEmphasisColorInt = context.resources.getColor(R.color.high_emphasis_text, null)
override val contactsViewModel: ContactsViewModel, }
override val chatViewModel: ChatViewModel,
override val context: Context,
override val userManager: UserManager
) : ViewModel(), PreviewAble
companion object { companion object {
val TAG: String = ComposeChatAdapter::class.java.simpleName val TAG: String = ComposeChatAdapter::class.java.simpleName
@ -196,48 +173,21 @@ class ComposeChatAdapter(
private var incomingShape: RoundedCornerShape = RoundedCornerShape(2.dp, 20.dp, 20.dp, 20.dp) private var incomingShape: RoundedCornerShape = RoundedCornerShape(2.dp, 20.dp, 20.dp, 20.dp)
private var outgoingShape: RoundedCornerShape = RoundedCornerShape(20.dp, 2.dp, 20.dp, 20.dp) private var outgoingShape: RoundedCornerShape = RoundedCornerShape(20.dp, 2.dp, 20.dp, 20.dp)
private val viewModel = ComposeChatAdapterViewModel()
val viewModel: PreviewAble =
if (utils != null) {
ComposeChatAdapterPreviewViewModel(
utils.viewThemeUtils,
utils.messageUtils,
utils.contactsViewModel,
utils.chatViewModel,
utils.context,
utils.userManager
)
} else {
ComposeChatAdapterViewModel()
}
val items = mutableStateListOf<ChatMessage>()
val currentUser: User = viewModel.userManager.currentUser.blockingGet()
val colorScheme = viewModel.viewThemeUtils.getColorScheme(viewModel.context)
val highEmphasisColorInt = viewModel.context.resources.getColor(R.color.high_emphasis_text, null)
fun Context.findMainActivityOrNull(): MainActivity? {
var context = this
while (context is ContextWrapper) {
if (context is MainActivity) return context
context = context.baseContext
}
return null
}
fun addMessages(messages: MutableList<ChatMessage>, append: Boolean) { fun addMessages(messages: MutableList<ChatMessage>, append: Boolean) {
if (messages.isEmpty()) return if (messages.isEmpty()) return
val processedMessages = messages.toMutableList() val processedMessages = messages.toMutableList()
if (items.isNotEmpty()) { if (viewModel.items.isNotEmpty()) {
if (append) { if (append) {
processedMessages.add(items.first()) processedMessages.add(viewModel.items.first())
} else { } else {
processedMessages.add(items.last()) processedMessages.add(viewModel.items.last())
} }
} }
if (append) items.addAll(processedMessages) else items.addAll(0, processedMessages) if (append) viewModel.items.addAll(processedMessages) else viewModel.items.addAll(0, processedMessages)
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -252,7 +202,7 @@ class ComposeChatAdapter(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) { ) {
stickyHeader { stickyHeader {
if (items.size == 0) { if (viewModel.items.size == 0) {
Column( Column(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -261,11 +211,11 @@ class ComposeChatAdapter(
ShimmerGroup() ShimmerGroup()
} }
} else { } else {
val timestamp = items[listState.firstVisibleItemIndex].timestamp val timestamp = viewModel.items[listState.firstVisibleItemIndex].timestamp
val dateString = formatTime(timestamp * LONG_1000) val dateString = formatTime(timestamp * LONG_1000)
val color = Color(highEmphasisColorInt) val color = Color(viewModel.highEmphasisColorInt)
val backgroundColor = val backgroundColor =
LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble, null) viewModel.context.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
Row( Row(
horizontalArrangement = Arrangement.Absolute.Center, horizontalArrangement = Arrangement.Absolute.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -279,8 +229,8 @@ class ComposeChatAdapter(
.padding(8.dp) .padding(8.dp)
.shadow( .shadow(
16.dp, 16.dp,
spotColor = colorScheme.primary, spotColor = viewModel.colorScheme.primary,
ambientColor = colorScheme.primary ambientColor = viewModel.colorScheme.primary
) )
.background(color = Color(backgroundColor), shape = RoundedCornerShape(8.dp)) .background(color = Color(backgroundColor), shape = RoundedCornerShape(8.dp))
.padding(8.dp) .padding(8.dp)
@ -290,8 +240,8 @@ class ComposeChatAdapter(
} }
} }
items(items) { message -> items(viewModel.items) { message ->
message.activeUser = currentUser message.activeUser = viewModel.currentUser
when (val type = message.getCalculateMessageType()) { when (val type = message.getCalculateMessageType()) {
ChatMessage.MessageType.SYSTEM_MESSAGE -> { ChatMessage.MessageType.SYSTEM_MESSAGE -> {
if (!message.shouldFilter()) { if (!message.shouldFilter()) {
@ -334,7 +284,7 @@ class ComposeChatAdapter(
} }
} }
if (messageId != null && items.size > 0) { if (messageId != null && viewModel.items.size > 0) {
LaunchedEffect(Dispatchers.Main) { LaunchedEffect(Dispatchers.Main) {
delay(SCROLL_DELAY) delay(SCROLL_DELAY)
val pos = searchMessages(messageId!!) val pos = searchMessages(messageId!!)
@ -376,7 +326,7 @@ class ComposeChatAdapter(
} }
private fun searchMessages(searchId: String): Int { private fun searchMessages(searchId: String): Int {
items.forEachIndexed { index, message -> viewModel.items.forEachIndexed { index, message ->
if (message.id == searchId) return index if (message.id == searchId) return index
} }
return -1 return -1
@ -430,18 +380,18 @@ class ComposeChatAdapter(
@Composable @Composable
(RowScope.() -> Unit) (RowScope.() -> Unit)
) { ) {
val incoming = message.actorId != currentUser.userId val incoming = message.actorId != viewModel.currentUser.userId
val color = if (incoming) { val color = if (incoming) {
if (message.isDeleted) { if (message.isDeleted) {
LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble_deleted, null) viewModel.context.resources.getColor(R.color.bg_message_list_incoming_bubble_deleted, null)
} else { } else {
LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble, null) viewModel.context.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
} }
} else { } else {
if (message.isDeleted) { if (message.isDeleted) {
ColorUtils.setAlphaComponent(colorScheme.surfaceVariant.toArgb(), HALF_OPACITY) ColorUtils.setAlphaComponent(viewModel.colorScheme.surfaceVariant.toArgb(), HALF_OPACITY)
} else { } else {
colorScheme.surfaceVariant.toArgb() viewModel.colorScheme.surfaceVariant.toArgb()
} }
} }
val shape = if (incoming) incomingShape else outgoingShape val shape = if (incoming) incomingShape else outgoingShape
@ -455,7 +405,7 @@ class ComposeChatAdapter(
if (incoming) { if (incoming) {
val imageUri = message.actorId?.let { viewModel.contactsViewModel.getImageUri(it, true) } val imageUri = message.actorId?.let { viewModel.contactsViewModel.getImageUri(it, true) }
val errorPlaceholderImage: Int = R.drawable.account_circle_96dp val errorPlaceholderImage: Int = R.drawable.account_circle_96dp
val loadedImage = loadImage(imageUri, LocalContext.current, errorPlaceholderImage) val loadedImage = loadImage(imageUri, viewModel.context, errorPlaceholderImage)
AsyncImage( AsyncImage(
model = loadedImage, model = loadedImage,
contentDescription = stringResource(R.string.user_avatar), contentDescription = stringResource(R.string.user_avatar),
@ -477,13 +427,13 @@ class ComposeChatAdapter(
color = Color(color), color = Color(color),
shape = shape shape = shape
) { ) {
val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp) val timeString = DateUtils(viewModel.context).getLocalTimeStringFromTimestamp(message.timestamp)
val modifier = if (includePadding) Modifier.padding(8.dp, 4.dp, 8.dp, 4.dp) else Modifier val modifier = if (includePadding) Modifier.padding(8.dp, 4.dp, 8.dp, 4.dp) else Modifier
Column(modifier = modifier) { Column(modifier = modifier) {
if (message.parentMessageId != null && !message.isDeleted && messagesJson != null) { if (message.parentMessageId != null && !message.isDeleted && messagesJson != null) {
messagesJson!! messagesJson!!
.find { it.parentMessage?.id == message.parentMessageId } .find { it.parentMessage?.id == message.parentMessageId }
?.parentMessage!!.asModel().let { CommonMessageQuote(LocalContext.current, it) } ?.parentMessage!!.asModel().let { CommonMessageQuote(viewModel.context, it) }
} }
if (incoming) { if (incoming) {
@ -520,8 +470,8 @@ class ComposeChatAdapter(
private fun Modifier.withCustomAnimation(incoming: Boolean): Modifier { private fun Modifier.withCustomAnimation(incoming: Boolean): Modifier {
val infiniteTransition = rememberInfiniteTransition() val infiniteTransition = rememberInfiniteTransition()
val borderColor by infiniteTransition.animateColor( val borderColor by infiniteTransition.animateColor(
initialValue = colorScheme.primary, initialValue = viewModel.colorScheme.primary,
targetValue = colorScheme.background, targetValue = viewModel.colorScheme.background,
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(ANIMATED_BLINK, easing = LinearEasing), animation = tween(ANIMATED_BLINK, easing = LinearEasing),
repeatMode = RepeatMode.Reverse repeatMode = RepeatMode.Reverse
@ -592,7 +542,7 @@ class ComposeChatAdapter(
LoaderTextView(ctx).apply { LoaderTextView(ctx).apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
val color = if (outgoing) { val color = if (outgoing) {
colorScheme.primary.toArgb() viewModel.colorScheme.primary.toArgb()
} else { } else {
resources.getColor(R.color.nc_shimmer_default_color, null) resources.getColor(R.color.nc_shimmer_default_color, null)
} }
@ -612,7 +562,7 @@ class ComposeChatAdapter(
@Composable @Composable
private fun EnrichedText(message: ChatMessage) { private fun EnrichedText(message: ChatMessage) {
AndroidView(factory = { ctx -> AndroidView(factory = { ctx ->
val incoming = message.actorId != currentUser.userId val incoming = message.actorId != viewModel.currentUser.userId
var processedMessageText = viewModel.messageUtils.enrichChatMessageText( var processedMessageText = viewModel.messageUtils.enrichChatMessageText(
ctx, ctx,
message, message,
@ -624,7 +574,7 @@ class ComposeChatAdapter(
ctx, viewModel.viewThemeUtils, processedMessageText!!, message, null ctx, viewModel.viewThemeUtils, processedMessageText!!, message, null
) )
EmojiTextView(ctx).apply { androidx.emoji2.widget.EmojiTextView(ctx).apply {
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
setLineSpacing(0F, LINE_SPACING) setLineSpacing(0F, LINE_SPACING)
textAlignment = TEXT_ALIGNMENT_VIEW_START textAlignment = TEXT_ALIGNMENT_VIEW_START
@ -642,14 +592,14 @@ class ComposeChatAdapter(
} }
@Composable @Composable
fun SystemMessage(message: ChatMessage) { private fun SystemMessage(message: ChatMessage) {
val similarMessages = sharedApplication!!.resources.getQuantityString( val similarMessages = sharedApplication!!.resources.getQuantityString(
R.plurals.see_similar_system_messages, R.plurals.see_similar_system_messages,
message.expandableChildrenAmount, message.expandableChildrenAmount,
message.expandableChildrenAmount message.expandableChildrenAmount
) )
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp) val timeString = DateUtils(viewModel.context).getLocalTimeStringFromTimestamp(message.timestamp)
Row(horizontalArrangement = Arrangement.Absolute.Center, verticalAlignment = Alignment.CenterVertically) { Row(horizontalArrangement = Arrangement.Absolute.Center, verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(
@ -682,7 +632,7 @@ class ComposeChatAdapter(
Text( Text(
text, text,
fontSize = AUTHOR_TEXT_SIZE, fontSize = AUTHOR_TEXT_SIZE,
color = Color(highEmphasisColorInt) color = Color(viewModel.highEmphasisColorInt)
) )
} }
} }
@ -690,15 +640,14 @@ class ComposeChatAdapter(
@Composable @Composable
private fun ImageMessage(message: ChatMessage, state: MutableState<Boolean>) { private fun ImageMessage(message: ChatMessage, state: MutableState<Boolean>) {
val hasCaption = (message.message != "{file}") val hasCaption = (message.message != "{file}")
val incoming = message.actorId != currentUser.userId val incoming = message.actorId != viewModel.currentUser.userId
val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp) val timeString = DateUtils(viewModel.context).getLocalTimeStringFromTimestamp(message.timestamp)
CommonMessageBody(message, includePadding = false, playAnimation = state.value) { CommonMessageBody(message, includePadding = false, playAnimation = state.value) {
Column { Column {
message.activeUser = currentUser message.activeUser = viewModel.currentUser
val imageUri = message.imageUrl val imageUri = message.imageUrl
val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE] val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image
val drawableResourceId = getDrawableResourceIdForMimeType(mimetype) val loadedImage = load(imageUri, viewModel.context, errorPlaceholderImage)
val loadedImage = load(imageUri, LocalContext.current, drawableResourceId)
AsyncImage( AsyncImage(
model = loadedImage, model = loadedImage,
@ -768,8 +717,8 @@ class ComposeChatAdapter(
WaveformSeekBar(ctx).apply { WaveformSeekBar(ctx).apply {
setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now
setColors( setColors(
colorScheme.inversePrimary.toArgb(), viewModel.colorScheme.inversePrimary.toArgb(),
colorScheme.onPrimaryContainer.toArgb() viewModel.colorScheme.onPrimaryContainer.toArgb()
) )
} }
}, },
@ -844,8 +793,8 @@ class ComposeChatAdapter(
private fun LinkMessage(message: ChatMessage, state: MutableState<Boolean>) { private fun LinkMessage(message: ChatMessage, state: MutableState<Boolean>) {
val color = colorResource(R.color.high_emphasis_text) val color = colorResource(R.color.high_emphasis_text)
viewModel.chatViewModel.getOpenGraph( viewModel.chatViewModel.getOpenGraph(
currentUser.getCredentials(), viewModel.currentUser.getCredentials(),
currentUser.baseUrl!!, viewModel.currentUser.baseUrl!!,
message.extractedUrlToPreview!! message.extractedUrlToPreview!!
) )
CommonMessageBody(message, playAnimation = state.value) { CommonMessageBody(message, playAnimation = state.value) {
@ -879,7 +828,7 @@ class ComposeChatAdapter(
it.link?.let { Text(it, fontSize = TIME_TEXT_SIZE) } it.link?.let { Text(it, fontSize = TIME_TEXT_SIZE) }
it.thumb?.let { it.thumb?.let {
val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image
val loadedImage = loadImage(it, LocalContext.current, errorPlaceholderImage) val loadedImage = loadImage(it, viewModel.context, errorPlaceholderImage)
AsyncImage( AsyncImage(
model = loadedImage, model = loadedImage,
contentDescription = stringResource(R.string.nc_sent_an_image), contentDescription = stringResource(R.string.nc_sent_an_image),
@ -933,7 +882,7 @@ class ComposeChatAdapter(
if (cardName?.isNotEmpty() == true) { if (cardName?.isNotEmpty() == true) {
val cardDescription = String.format( val cardDescription = String.format(
LocalContext.current.resources.getString(R.string.deck_card_description), viewModel.context.resources.getString(R.string.deck_card_description),
stackName, stackName,
boardName boardName
) )
@ -950,44 +899,3 @@ class ComposeChatAdapter(
} }
} }
} }
@Preview(showBackground = true, widthDp = 380, heightDp = 800)
@Composable
fun AllMessageTypesPreview() {
val previewUtils = ComposePreviewUtils.getInstance(LocalContext.current)
val adapter = remember { ComposeChatAdapter(messagesJson = null, messageId = null, previewUtils) }
val sampleMessages = remember {
listOf(
// Text Messages
ChatMessage().apply {
jsonMessageId = 1
actorId = "user1"
message = "I love Nextcloud"
timestamp = System.currentTimeMillis()
actorDisplayName = "User1"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
},
ChatMessage().apply {
jsonMessageId = 2
actorId = "user1_id"
message = "I love Nextcloud"
timestamp = System.currentTimeMillis()
actorDisplayName = "User2"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
}
)
}
LaunchedEffect(sampleMessages) { // Use LaunchedEffect or similar to update state once
if (adapter.items.isEmpty()) { // Prevent adding multiple times on recomposition
adapter.addMessages(sampleMessages.toMutableList(), append = false) // Add messages
}
}
MaterialTheme(colorScheme = adapter.colorScheme) { // Use the (potentially faked) color scheme
Box(modifier = Modifier.fillMaxSize()) { // Provide a container
adapter.GetView() // Call the main Composable
}
}
}

View File

@ -7,16 +7,14 @@
package com.nextcloud.talk.ui.dialog package com.nextcloud.talk.ui.dialog
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ActivityInfo
import android.os.Bundle import android.os.Bundle
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -44,7 +42,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@ -94,15 +94,6 @@ class ContextChatCompose(val bundle: Bundle) {
} }
} }
private fun Context.requireActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
throw IllegalStateException("No activity was present but it is required.")
}
@Composable @Composable
fun GetDialogView( fun GetDialogView(
shouldDismiss: MutableState<Boolean>, shouldDismiss: MutableState<Boolean>,
@ -110,11 +101,9 @@ class ContextChatCompose(val bundle: Bundle) {
contextViewModel: ContextChatComposeViewModel = ContextChatComposeViewModel() contextViewModel: ContextChatComposeViewModel = ContextChatComposeViewModel()
) { ) {
if (shouldDismiss.value) { if (shouldDismiss.value) {
context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
return return
} }
context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
val colorScheme = contextViewModel.viewThemeUtils.getColorScheme(context) val colorScheme = contextViewModel.viewThemeUtils.getColorScheme(context)
MaterialTheme(colorScheme) { MaterialTheme(colorScheme) {
Dialog( Dialog(
@ -155,28 +144,28 @@ class ContextChatCompose(val bundle: Bundle) {
val name = bundle.getString(BundleKeys.KEY_CONVERSATION_NAME)!! val name = bundle.getString(BundleKeys.KEY_CONVERSATION_NAME)!!
Text(name, fontSize = 24.sp) Text(name, fontSize = 24.sp)
} }
// Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// val cInt = context.resources.getColor(R.color.high_emphasis_text, null) val cInt = context.resources.getColor(R.color.high_emphasis_text, null)
// Icon( Icon(
// painterResource(R.drawable.ic_call_black_24dp), painterResource(R.drawable.ic_call_black_24dp),
// "", "",
// tint = Color(cInt), tint = Color(cInt),
// modifier = Modifier modifier = Modifier
// .padding() .padding()
// .padding(end = 16.dp) .padding(end = 16.dp)
// .alpha(HALF_ALPHA) .alpha(HALF_ALPHA)
// ) )
//
// Icon( Icon(
// painterResource(R.drawable.ic_baseline_videocam_24), painterResource(R.drawable.ic_baseline_videocam_24),
// "", "",
// tint = Color(cInt), tint = Color(cInt),
// modifier = Modifier modifier = Modifier
// .padding() .padding()
// .alpha(HALF_ALPHA) .alpha(HALF_ALPHA)
// ) )
//
// ComposeChatMenu(colorScheme.background, false) ComposeChatMenu(colorScheme.background, false)
} }
if (shouldShow) { if (shouldShow) {
Icon( Icon(

View File

@ -118,6 +118,7 @@ class ConversationsListBottomDialog(
currentUser.capabilities?.spreedCapability!!, currentUser.capabilities?.spreedCapability!!,
SpreedFeatures.FAVORITES SpreedFeatures.FAVORITES
) )
val canModerate = ConversationUtils.canModerate(conversation, currentUser.capabilities?.spreedCapability!!)
binding.conversationRemoveFromFavorites.visibility = setVisibleIf( binding.conversationRemoveFromFavorites.visibility = setVisibleIf(
hasFavoritesCapability && conversation.favorite hasFavoritesCapability && conversation.favorite
@ -148,11 +149,14 @@ class ConversationsListBottomDialog(
) )
binding.conversationOperationDelete.visibility = setVisibleIf( binding.conversationOperationDelete.visibility = setVisibleIf(
conversation.canDeleteConversation canModerate
) )
binding.conversationOperationLeave.visibility = setVisibleIf( 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.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage 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.network.NetworkMonitor
import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
@ -59,10 +58,9 @@ class TempMessageActionsDialog(
private fun initMenuItems() { private fun initMenuItems() {
this.lifecycleScope.launch { this.lifecycleScope.launch {
val sendingFailed = message.sendStatus == SendStatus.FAILED initResendMessage(message.sendingFailed && networkMonitor.isOnline.value)
initResendMessage(sendingFailed && networkMonitor.isOnline.value) initMenuEditMessage(message.sendingFailed || !networkMonitor.isOnline.value)
initMenuEditMessage(sendingFailed || !networkMonitor.isOnline.value) initMenuDeleteMessage(message.sendingFailed || !networkMonitor.isOnline.value)
initMenuDeleteMessage(sendingFailed || !networkMonitor.isOnline.value)
initMenuItemCopy() initMenuItemCopy()
} }
} }

View File

@ -205,10 +205,6 @@ object ApiUtils {
return getUrlForParticipants(version, baseUrl, token) + "/active" 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 @JvmStatic
fun getUrlForParticipantsSelf(version: Int, baseUrl: String?, token: String?): String { fun getUrlForParticipantsSelf(version: Int, baseUrl: String?, token: String?): String {
return getUrlForParticipants(version, baseUrl, token) + "/self" return getUrlForParticipants(version, baseUrl, token) + "/self"
@ -453,10 +449,6 @@ object ApiUtils {
return "$baseUrl$OCS_API_VERSION/cloud/users/search/by-phone" return "$baseUrl$OCS_API_VERSION/cloud/users/search/by-phone"
} }
fun getUrlForUnbindingRoom(baseUrl: String, roomToken: String): String {
return "$baseUrl/ocs/v2.php/apps/spreed/api/v4/room/$roomToken/object"
}
fun getUrlForFileUpload(baseUrl: String, user: String, remotePath: String): String { fun getUrlForFileUpload(baseUrl: String, user: String, remotePath: String): String {
return "$baseUrl/remote.php/dav/files/$user$remotePath" return "$baseUrl/remote.php/dav/files/$user$remotePath"
} }
@ -481,10 +473,6 @@ object ApiUtils {
return "$baseUrl$OCS_API_VERSION/apps/spreed/temp-user-avatar" return "$baseUrl$OCS_API_VERSION/apps/spreed/temp-user-avatar"
} }
fun getUrlForSensitiveConversation(baseUrl: String, roomToken: String): String {
return "$baseUrl$OCS_API_VERSION/apps/spreed/api/v4/room/$roomToken/sensitive"
}
fun getUrlForUserFields(baseUrl: String): String { fun getUrlForUserFields(baseUrl: String): String {
return "$baseUrl$OCS_API_VERSION/cloud/user/fields" return "$baseUrl$OCS_API_VERSION/cloud/user/fields"
} }
@ -637,8 +625,4 @@ object ApiUtils {
fun getUrlForChatMessageContext(baseUrl: String, token: String, messageId: String): String { fun getUrlForChatMessageContext(baseUrl: String, token: String, messageId: String): String {
return "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/chat/$token/$messageId/context" return "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/chat/$token/$messageId/context"
} }
fun getUrlForProfile(baseUrl: String, userId: String): String {
return "$baseUrl$OCS_API_VERSION/profile/$userId"
}
} }

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

@ -57,10 +57,7 @@ enum class SpreedFeatures(val value: String) {
BAN_V1("ban-v1"), BAN_V1("ban-v1"),
EDIT_MESSAGES_NOTE_TO_SELF("edit-messages-note-to-self"), EDIT_MESSAGES_NOTE_TO_SELF("edit-messages-note-to-self"),
ARCHIVE_CONVERSATIONS("archived-conversations-v2"), ARCHIVE_CONVERSATIONS("archived-conversations-v2"),
CONVERSATION_CREATION_ALL("conversation-creation-all"), CONVERSATION_CREATION_ALL("conversation-creation-all")
UNBIND_CONVERSATION("unbind-conversation"),
SENSITIVE_CONVERSATIONS("sensitive-conversations"),
IMPORTANT_CONVERSATIONS("important-conversations")
} }
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
@ -143,36 +140,6 @@ object CapabilitiesUtil {
return false return false
} }
fun retentionOfEventRooms(spreedCapabilities: SpreedCapability): Int {
if (spreedCapabilities.config?.containsKey("conversations") == true) {
val map = spreedCapabilities.config!!["conversations"]
if (map?.containsKey("retention-event") == true) {
return map["retention-event"].toString().toInt()
}
}
return 0
}
fun retentionOfSIPRoom(spreedCapabilities: SpreedCapability): Int {
if (spreedCapabilities.config?.containsKey("conversations") == true) {
val map = spreedCapabilities.config!!["conversations"]
if (map?.containsKey("retention-phone") == true) {
return map["retention-phone"].toString().toInt()
}
}
return 0
}
fun retentionOfInstantMeetingRoom(spreedCapabilities: SpreedCapability): Int {
if (spreedCapabilities.config?.containsKey("conversations") == true) {
val map = spreedCapabilities.config!!["conversations"]
if (map?.containsKey("retention-instant-meetings") == true) {
return map["retention-instant-meetings"].toString().toInt()
}
}
return 0
}
@JvmStatic @JvmStatic
fun isCallRecordingAvailable(spreedCapabilities: SpreedCapability): Boolean { fun isCallRecordingAvailable(spreedCapabilities: SpreedCapability): Boolean {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RECORDING_V1) && if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RECORDING_V1) &&

View File

@ -36,7 +36,6 @@ import com.nextcloud.talk.utils.Mimetype.AUDIO_MPEG
import com.nextcloud.talk.utils.Mimetype.AUDIO_OGG import com.nextcloud.talk.utils.Mimetype.AUDIO_OGG
import com.nextcloud.talk.utils.Mimetype.AUDIO_WAV import com.nextcloud.talk.utils.Mimetype.AUDIO_WAV
import com.nextcloud.talk.utils.Mimetype.IMAGE_GIF import com.nextcloud.talk.utils.Mimetype.IMAGE_GIF
import com.nextcloud.talk.utils.Mimetype.IMAGE_HEIC
import com.nextcloud.talk.utils.Mimetype.IMAGE_JPEG import com.nextcloud.talk.utils.Mimetype.IMAGE_JPEG
import com.nextcloud.talk.utils.Mimetype.IMAGE_PNG import com.nextcloud.talk.utils.Mimetype.IMAGE_PNG
import com.nextcloud.talk.utils.Mimetype.TEXT_MARKDOWN import com.nextcloud.talk.utils.Mimetype.TEXT_MARKDOWN
@ -156,8 +155,7 @@ class FileViewerUtils(private val context: Context, private val user: User) {
-> openMediaView(filename, mimetype) -> openMediaView(filename, mimetype)
IMAGE_PNG, IMAGE_PNG,
IMAGE_JPEG, IMAGE_JPEG,
IMAGE_GIF, IMAGE_GIF
IMAGE_HEIC
-> openImageView(filename, mimetype) -> openImageView(filename, mimetype)
TEXT_MARKDOWN, TEXT_MARKDOWN,
TEXT_PLAIN TEXT_PLAIN
@ -250,7 +248,6 @@ class FileViewerUtils(private val context: Context, private val user: User) {
return when (mimetype) { return when (mimetype) {
IMAGE_PNG, IMAGE_PNG,
IMAGE_JPEG, IMAGE_JPEG,
IMAGE_HEIC,
IMAGE_GIF, IMAGE_GIF,
AUDIO_MPEG, AUDIO_MPEG,
AUDIO_WAV, AUDIO_WAV,

View File

@ -22,7 +22,6 @@ object Mimetype {
const val IMAGE_JPEG = "image/jpeg" const val IMAGE_JPEG = "image/jpeg"
const val IMAGE_JPG = "image/jpg" const val IMAGE_JPG = "image/jpg"
const val IMAGE_GIF = "image/gif" const val IMAGE_GIF = "image/gif"
const val IMAGE_HEIC = "image/heic"
const val VIDEO_MP4 = "video/mp4" const val VIDEO_MP4 = "video/mp4"
const val VIDEO_QUICKTIME = "video/quicktime" const val VIDEO_QUICKTIME = "video/quicktime"

View File

@ -81,5 +81,4 @@ object BundleKeys {
const val KEY_FIELD_MAP: String = "KEY_FIELD_MAP" const val KEY_FIELD_MAP: String = "KEY_FIELD_MAP"
const val KEY_CHAT_URL: String = "KEY_CHAT_URL" 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_SCROLL_TO_NOTIFICATION_CATEGORY: String = "KEY_SCROLL_TO_NOTIFICATION_CATEGORY"
const val KEY_FOCUS_INPUT: String = "KEY_FOCUS_INPUT"
} }

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