repo created
This commit is contained in:
commit
93184d21d1
1403 changed files with 189511 additions and 0 deletions
418
app/build.gradle
Normal file
418
app/build.gradle
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2021-2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
import com.github.spotbugs.snom.Confidence
|
||||
import com.github.spotbugs.snom.Effort
|
||||
import com.github.spotbugs.snom.SpotBugsTask
|
||||
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.plugin.compose" version "2.2.20"
|
||||
id "org.jetbrains.kotlin.kapt"
|
||||
id 'com.google.devtools.ksp' version '2.2.20-2.0.3'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.github.spotbugs'
|
||||
apply plugin: 'io.gitlab.arturbosch.detekt'
|
||||
apply plugin: "org.jlleitschuh.gradle.ktlint"
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
android {
|
||||
compileSdkVersion 35
|
||||
|
||||
namespace = 'com.nextcloud.talk'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 35
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)
|
||||
// xx .xxx .xx .xx
|
||||
versionCode 230000005
|
||||
versionName "23.0.0 Alpha 05"
|
||||
|
||||
flavorDimensions "default"
|
||||
renderscriptTargetApi = 19
|
||||
renderscriptSupportModeEnabled true
|
||||
|
||||
productFlavors {
|
||||
// used for f-droid
|
||||
generic {
|
||||
applicationId 'com.nextcloud.talk2'
|
||||
dimension "default"
|
||||
}
|
||||
gplay {
|
||||
applicationId 'com.nextcloud.talk2'
|
||||
dimension "default"
|
||||
}
|
||||
qa {
|
||||
applicationId "com.nextcloud.talk2.qa"
|
||||
dimension "default"
|
||||
versionCode 1
|
||||
versionName "1"
|
||||
}
|
||||
}
|
||||
|
||||
// Enabling multidex support.
|
||||
multiDexEnabled = true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
disable 'MissingTranslation'
|
||||
disable 'VectorPath'
|
||||
disable 'UnusedQuantity'
|
||||
}
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
// Adds exported schema location as test app assets.
|
||||
getByName("androidTest").assets.srcDir("$projectDir/schemas")
|
||||
}
|
||||
|
||||
testInstrumentationRunnerArgument "TEST_SERVER_URL", "${NC_TEST_SERVER_BASEURL}"
|
||||
testInstrumentationRunnerArgument "TEST_SERVER_USERNAME", "${NC_TEST_SERVER_USERNAME}"
|
||||
testInstrumentationRunnerArgument "TEST_SERVER_PASSWORD", "${NC_TEST_SERVER_PASSWORD}"
|
||||
|
||||
def localBroadcastPermission = "PRIVATE_BROADCAST"
|
||||
manifestPlaceholders.broadcastPermission = localBroadcastPermission
|
||||
buildConfigField "String", "PERMISSION_LOCAL_BROADCAST", "\"${localBroadcastPermission}\""
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
unitTests.returnDefaultValues = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += [
|
||||
'META-INF/LICENSE.txt',
|
||||
'META-INF/LICENSE',
|
||||
'META-INF/NOTICE.txt',
|
||||
'META-INF/NOTICE',
|
||||
'META-INF/DEPENDENCIES',
|
||||
'META-INF/rxjava.properties'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
check.dependsOn 'spotbugsGplayDebug', 'lint', 'ktlintCheck', 'detekt'
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.15"
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = false
|
||||
disable 'MissingTranslation','PrivateResource'
|
||||
htmlOutput = layout.buildDirectory.file("reports/lint/lint.html").get().asFile
|
||||
htmlReport = true
|
||||
}
|
||||
}
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
||||
ext {
|
||||
androidxCameraVersion = "1.5.0"
|
||||
coilKtVersion = "2.7.0"
|
||||
daggerVersion = "2.57.1"
|
||||
emojiVersion = "1.6.0"
|
||||
fidoVersion = "4.1.0-patch2"
|
||||
lifecycleVersion = '2.9.4'
|
||||
okhttpVersion = "4.12.0"
|
||||
markwonVersion = "4.6.2"
|
||||
materialDialogsVersion = "3.3.0"
|
||||
parcelerVersion = "1.1.13"
|
||||
prismVersion = "2.0.0"
|
||||
retrofit2Version = "3.0.0"
|
||||
roomVersion = "2.8.0"
|
||||
workVersion = "2.10.4"
|
||||
espressoVersion = "3.7.0"
|
||||
androidxTestVersion = "1.5.0"
|
||||
media3_version = "1.8.0"
|
||||
coroutines_version = "1.10.2"
|
||||
mockitoKotlinVersion = "6.0.0"
|
||||
}
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
exclude group: 'org.jetbrains', module: 'annotations-java5' // via prism4j, already using annotations explicitly
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.room:room-testing-android:${roomVersion}"
|
||||
implementation 'androidx.compose.foundation:foundation-layout:1.9.1'
|
||||
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0'
|
||||
spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.14'
|
||||
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.8")
|
||||
|
||||
implementation("androidx.compose.runtime:runtime:1.9.1")
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.datastore:datastore-core:1.1.7'
|
||||
implementation 'androidx.datastore:datastore-preferences:1.1.7'
|
||||
implementation 'androidx.test.ext:junit-ktx:1.3.0'
|
||||
|
||||
implementation fileTree(include: ['*'], dir: 'libs')
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.7.1'
|
||||
implementation 'com.google.android.material:material:1.13.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation "com.vanniktech:emoji-google:0.21.0"
|
||||
implementation "androidx.emoji2:emoji2:${emojiVersion}"
|
||||
implementation "androidx.emoji2:emoji2-bundled:${emojiVersion}"
|
||||
implementation "androidx.emoji2:emoji2-views:${emojiVersion}"
|
||||
implementation "androidx.emoji2:emoji2-views-helper:${emojiVersion}"
|
||||
implementation 'org.michaelevans.colorart:library:0.0.3'
|
||||
implementation "androidx.work:work-runtime:${workVersion}"
|
||||
implementation "androidx.work:work-rxjava2:${workVersion}"
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
implementation ('com.github.bitfireAT:dav4jvm:2.1.3', {
|
||||
exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser
|
||||
})
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.3'
|
||||
implementation "com.github.nextcloud-deps:qrcodescanner:0.1.2.4" // "com.github.blikoon:QRCodeScanner:0.1.2"
|
||||
|
||||
implementation "androidx.camera:camera-core:${androidxCameraVersion}"
|
||||
implementation "androidx.camera:camera-camera2:${androidxCameraVersion}"
|
||||
implementation "androidx.camera:camera-lifecycle:${androidxCameraVersion}"
|
||||
implementation "androidx.camera:camera-view:${androidxCameraVersion}"
|
||||
implementation "androidx.exifinterface:exifinterface:1.4.1"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:${lifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${lifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${lifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-process:${lifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-common:${lifecycleVersion}"
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${lifecycleVersion}")
|
||||
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}"
|
||||
implementation "com.squareup.okhttp3:okhttp-urlconnection:${okhttpVersion}"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}"
|
||||
|
||||
implementation 'com.bluelinelabs:logansquare:1.3.7'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.20.0'
|
||||
kapt 'com.bluelinelabs:logansquare-compiler:1.3.7'
|
||||
|
||||
implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${retrofit2Version}"
|
||||
implementation "com.squareup.retrofit2:converter-gson:${retrofit2Version}"
|
||||
implementation 'de.mannodermaus.retrofit2:converter-logansquare:1.4.1'
|
||||
|
||||
implementation "com.google.dagger:dagger:${daggerVersion}"
|
||||
kapt "com.google.dagger:dagger-compiler:${daggerVersion}"
|
||||
implementation 'com.github.lukaspili.autodagger2:autodagger2:1.1'
|
||||
kapt 'com.github.lukaspili.autodagger2:autodagger2-compiler:1.1'
|
||||
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
|
||||
|
||||
implementation 'org.greenrobot:eventbus:3.3.1'
|
||||
implementation 'net.zetetic:sqlcipher-android:4.10.0'
|
||||
|
||||
implementation "androidx.room:room-runtime:${roomVersion}"
|
||||
implementation "androidx.room:room-rxjava2:${roomVersion}"
|
||||
ksp "androidx.room:room-compiler:${roomVersion}"
|
||||
implementation "androidx.room:room-ktx:${roomVersion}"
|
||||
|
||||
implementation "org.parceler:parceler-api:$parcelerVersion"
|
||||
implementation 'com.github.ddB0515.FlexibleAdapter:flexible-adapter:5.1.1'
|
||||
implementation 'com.github.ddB0515.FlexibleAdapter:flexible-adapter-ui:5.1.1'
|
||||
implementation 'org.apache.commons:commons-lang3:3.18.0'
|
||||
implementation 'com.github.wooplr:Spotlight:1.3'
|
||||
implementation 'com.google.code.findbugs:jsr305:3.0.2'
|
||||
implementation 'com.github.nextcloud-deps:ChatKit:0.4.2'
|
||||
implementation 'joda-time:joda-time:2.14.0'
|
||||
implementation "io.coil-kt:coil:${coilKtVersion}"
|
||||
implementation "io.coil-kt:coil-gif:${coilKtVersion}"
|
||||
implementation "io.coil-kt:coil-svg:${coilKtVersion}"
|
||||
implementation "io.coil-kt:coil-compose:${coilKtVersion}"
|
||||
implementation 'com.github.natario1:Autocomplete:1.1.0'
|
||||
|
||||
implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido:${fidoVersion}"
|
||||
implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido2:${fidoVersion}"
|
||||
|
||||
implementation "com.afollestad.material-dialogs:core:${materialDialogsVersion}"
|
||||
implementation "com.afollestad.material-dialogs:datetime:${materialDialogsVersion}"
|
||||
implementation "com.afollestad.material-dialogs:bottomsheets:${materialDialogsVersion}"
|
||||
implementation "com.afollestad.material-dialogs:lifecycle:${materialDialogsVersion}"
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.13.2'
|
||||
|
||||
implementation "androidx.media3:media3-exoplayer:$media3_version"
|
||||
implementation "androidx.media3:media3-ui:$media3_version"
|
||||
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.29'
|
||||
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$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 'io.github.elye:loaderviewlibrary:3.0.0'
|
||||
implementation 'org.osmdroid:osmdroid-android:6.1.20'
|
||||
implementation ('fr.dudie:nominatim-api:3.4', {
|
||||
//noinspection DuplicatePlatformClasses
|
||||
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
|
||||
})
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.16.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.10.1'
|
||||
implementation 'com.github.nextcloud.android-common:ui:0.28.0'
|
||||
implementation 'com.github.nextcloud-deps:android-talk-webrtc:132.6834.0'
|
||||
|
||||
gplayImplementation 'com.google.android.gms:play-services-base:18.8.0'
|
||||
gplayImplementation "com.google.firebase:firebase-messaging:25.0.0"
|
||||
|
||||
//compose
|
||||
implementation(platform("androidx.compose:compose-bom:2025.09.00"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation 'androidx.compose.material3:material3:1.3.2'
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation 'androidx.activity:activity-compose:1.10.1'
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
|
||||
//tests
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.13.4'
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.9.1")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:5.19.0'
|
||||
testImplementation 'androidx.arch.core:core-testing:2.2.0'
|
||||
|
||||
androidTestImplementation "androidx.test:core:1.7.0"
|
||||
|
||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.7.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:5.19.0'
|
||||
androidTestImplementation "androidx.work:work-testing:${workVersion}"
|
||||
// Espresso core
|
||||
androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
})
|
||||
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-web:$espressoVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espressoVersion"
|
||||
|
||||
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
|
||||
|
||||
androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.2')
|
||||
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2025.09.00"))
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
|
||||
|
||||
testImplementation 'org.junit.vintage:junit-vintage-engine:5.13.4' // DO NOT REMOVE
|
||||
testImplementation "androidx.room:room-testing:${roomVersion}"
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:$okhttpVersion")
|
||||
testImplementation("com.google.dagger:hilt-android-testing:2.57.1")
|
||||
testImplementation("org.robolectric:robolectric:4.16")
|
||||
}
|
||||
|
||||
tasks.register('installGitHooks', Copy) {
|
||||
description = "Install git hooks"
|
||||
from("../scripts/hooks") {
|
||||
include '*'
|
||||
}
|
||||
into '../.git/hooks'
|
||||
}
|
||||
|
||||
spotbugs {
|
||||
ignoreFailures = true // should continue checking
|
||||
effort = Effort.MAX
|
||||
reportLevel = Confidence.valueOf('MEDIUM')
|
||||
}
|
||||
|
||||
tasks.withType(SpotBugsTask).configureEach { task ->
|
||||
String variantNameCap = task.name.replace("spotbugs", "")
|
||||
String variantName = variantNameCap.substring(0, 1).toLowerCase() + variantNameCap.substring(1)
|
||||
|
||||
dependsOn "compile${variantNameCap}Sources"
|
||||
|
||||
excludeFilter = file("${project.rootDir}/spotbugs-filter.xml")
|
||||
classes = fileTree(layout.buildDirectory.get().asFile.toString()+"/intermediates/javac/${variantName}/compile${variantNameCap}JavaWithJavac/classes/")
|
||||
reports {
|
||||
xml {
|
||||
required = true
|
||||
}
|
||||
html {
|
||||
required = true
|
||||
outputLocation = layout.buildDirectory.file("reports/spotbugs/spotbugs.html")
|
||||
stylesheet = 'fancy.xsl'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("detekt").configure {
|
||||
reports {
|
||||
html.required.set(true)
|
||||
txt.required.set(true)
|
||||
xml.required.set(false)
|
||||
sarif.required.set(false)
|
||||
md.required.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
detekt {
|
||||
config.setFrom("../detekt.yml")
|
||||
source.setFrom("src/")
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg('room.schemaLocation', "$projectDir/schemas")
|
||||
}
|
||||
56
app/lint.xml
Normal file
56
app/lint.xml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Nextcloud Talk - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
~ SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
~ SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
~ SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
~ SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<lint>
|
||||
<issue id="UnusedAttribute">
|
||||
<ignore regexp="importantForAutofill"/>
|
||||
<ignore regexp="autofillHints"/>
|
||||
</issue>
|
||||
|
||||
<issue id="ExtraTranslation">
|
||||
<ignore path="**/strings.xml"/>
|
||||
<ignore path="**/values-b+en+001/strings.xml"/>
|
||||
</issue>
|
||||
|
||||
<issue id="UnusedResources">
|
||||
<ignore path="**/values-**/strings.xml" />
|
||||
</issue>
|
||||
|
||||
<issue id="MissingQuantity">
|
||||
<ignore path="**/values-ka-rGE/strings.xml"/>
|
||||
<ignore path="**/values-ar/strings.xml" />
|
||||
<ignore path="**/values-ca/strings.xml" />
|
||||
</issue>
|
||||
|
||||
<issue id="TypographyEllipsis">
|
||||
<ignore path="**/values-**/strings.xml" />
|
||||
</issue>
|
||||
|
||||
<issue id="Typos">
|
||||
<ignore path="**/values-**/strings.xml" />
|
||||
</issue>
|
||||
|
||||
<issue id="RestrictedApi" severity="error">
|
||||
<ignore path="build" />
|
||||
</issue>
|
||||
|
||||
<issue id="NewApi" severity="error">
|
||||
<ignore path="build" />
|
||||
</issue>
|
||||
|
||||
<issue id="ObsoleteLintCustomCheck" severity="warning">
|
||||
<ignore path="**/jetified-annotation-experimental-1.**/**/lint.jar" />
|
||||
</issue>
|
||||
|
||||
<issue id="Aligned16KB" severity="error">
|
||||
<ignore path="**/jetified-android-database-sqlcipher-4.5.4/**/libsqlcipher.so" />
|
||||
</issue>
|
||||
</lint>
|
||||
27
app/proguard-rules.pro
vendored
Normal file
27
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#
|
||||
# Nextcloud Talk - Android Client
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 10,
|
||||
"identityHash": "1b2dab0ea495c45c9c9ee6e64ba74039",
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pushConfigurationState",
|
||||
"columnName": "pushConfigurationState",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "capabilities",
|
||||
"columnName": "capabilities",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverVersion",
|
||||
"columnName": "serverVersion",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientCertificate",
|
||||
"columnName": "clientCertificate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalSignalingServer",
|
||||
"columnName": "externalSignalingServer",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "current",
|
||||
"columnName": "current",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scheduledForDeletion",
|
||||
"columnName": "scheduledForDeletion",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"accountIdentifier",
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"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, '1b2dab0ea495c45c9c9ee6e64ba74039')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,719 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 11,
|
||||
"identityHash": "bc802cadfdef41d3eb94ffbb0729eb89",
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pushConfigurationState",
|
||||
"columnName": "pushConfigurationState",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "capabilities",
|
||||
"columnName": "capabilities",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverVersion",
|
||||
"columnName": "serverVersion",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientCertificate",
|
||||
"columnName": "clientCertificate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalSignalingServer",
|
||||
"columnName": "externalSignalingServer",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "current",
|
||||
"columnName": "current",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scheduledForDeletion",
|
||||
"columnName": "scheduledForDeletion",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"accountIdentifier",
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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, `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, 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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteToken",
|
||||
"columnName": "remoteToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sessionId",
|
||||
"columnName": "sessionId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusClearAt",
|
||||
"columnName": "statusClearAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusIcon",
|
||||
"columnName": "statusIcon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusMessage",
|
||||
"columnName": "statusMessage",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `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": "lastEditActorDisplayName",
|
||||
"columnName": "lastEditActorDisplayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditActorId",
|
||||
"columnName": "lastEditActorId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditActorType",
|
||||
"columnName": "lastEditActorType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditTimestamp",
|
||||
"columnName": "lastEditTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "renderMarkdown",
|
||||
"columnName": "markdown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageParameters",
|
||||
"columnName": "messageParameters",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageType",
|
||||
"columnName": "messageType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentMessageId",
|
||||
"columnName": "parent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reactions",
|
||||
"columnName": "reactions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reactionsSelf",
|
||||
"columnName": "reactionsSelf",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"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, 'bc802cadfdef41d3eb94ffbb0729eb89')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,725 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 12,
|
||||
"identityHash": "7edb537b6987d0de6586a6760c970958",
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pushConfigurationState",
|
||||
"columnName": "pushConfigurationState",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "capabilities",
|
||||
"columnName": "capabilities",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverVersion",
|
||||
"columnName": "serverVersion",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientCertificate",
|
||||
"columnName": "clientCertificate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalSignalingServer",
|
||||
"columnName": "externalSignalingServer",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "current",
|
||||
"columnName": "current",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scheduledForDeletion",
|
||||
"columnName": "scheduledForDeletion",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"accountIdentifier",
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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, `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, 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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteToken",
|
||||
"columnName": "remoteToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sessionId",
|
||||
"columnName": "sessionId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusClearAt",
|
||||
"columnName": "statusClearAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusIcon",
|
||||
"columnName": "statusIcon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusMessage",
|
||||
"columnName": "statusMessage",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `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": "lastEditActorDisplayName",
|
||||
"columnName": "lastEditActorDisplayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditActorId",
|
||||
"columnName": "lastEditActorId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditActorType",
|
||||
"columnName": "lastEditActorType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditTimestamp",
|
||||
"columnName": "lastEditTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "renderMarkdown",
|
||||
"columnName": "markdown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageParameters",
|
||||
"columnName": "messageParameters",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageType",
|
||||
"columnName": "messageType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentMessageId",
|
||||
"columnName": "parent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reactions",
|
||||
"columnName": "reactions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reactionsSelf",
|
||||
"columnName": "reactionsSelf",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"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, '7edb537b6987d0de6586a6760c970958')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,749 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 13,
|
||||
"identityHash": "a521f027909f69f4c7d1855f84a2e67f",
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pushConfigurationState",
|
||||
"columnName": "pushConfigurationState",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "capabilities",
|
||||
"columnName": "capabilities",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverVersion",
|
||||
"columnName": "serverVersion",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientCertificate",
|
||||
"columnName": "clientCertificate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalSignalingServer",
|
||||
"columnName": "externalSignalingServer",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "current",
|
||||
"columnName": "current",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scheduledForDeletion",
|
||||
"columnName": "scheduledForDeletion",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"accountIdentifier",
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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, `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, 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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteToken",
|
||||
"columnName": "remoteToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sessionId",
|
||||
"columnName": "sessionId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusClearAt",
|
||||
"columnName": "statusClearAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusIcon",
|
||||
"columnName": "statusIcon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusMessage",
|
||||
"columnName": "statusMessage",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditActorId",
|
||||
"columnName": "lastEditActorId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditActorType",
|
||||
"columnName": "lastEditActorType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditTimestamp",
|
||||
"columnName": "lastEditTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "renderMarkdown",
|
||||
"columnName": "markdown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageParameters",
|
||||
"columnName": "messageParameters",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageType",
|
||||
"columnName": "messageType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentMessageId",
|
||||
"columnName": "parent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reactions",
|
||||
"columnName": "reactions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reactionsSelf",
|
||||
"columnName": "reactionsSelf",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "referenceId",
|
||||
"columnName": "referenceId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"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, 'a521f027909f69f4c7d1855f84a2e67f')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,719 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 14,
|
||||
"identityHash": "506abc931eb3b657cafe6ad1b25f635d",
|
||||
"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, 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
|
||||
}
|
||||
],
|
||||
"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, '506abc931eb3b657cafe6ad1b25f635d')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,725 @@
|
|||
{
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,731 @@
|
|||
{
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,730 @@
|
|||
{
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,746 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 18,
|
||||
"identityHash": "c5e3716925065d7419fb23efabf6691f",
|
||||
"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, `threadId` INTEGER, `isThread` INTEGER 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": "threadId",
|
||||
"columnName": "threadId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isThread",
|
||||
"columnName": "isThread",
|
||||
"affinity": "INTEGER",
|
||||
"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, `threadId` INTEGER, `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": "threadId",
|
||||
"columnName": "threadId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"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, 'c5e3716925065d7419fb23efabf6691f')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,746 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 19,
|
||||
"identityHash": "c5e3716925065d7419fb23efabf6691f",
|
||||
"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, `threadId` INTEGER, `isThread` INTEGER 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": "threadId",
|
||||
"columnName": "threadId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isThread",
|
||||
"columnName": "isThread",
|
||||
"affinity": "INTEGER",
|
||||
"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, `threadId` INTEGER, `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": "threadId",
|
||||
"columnName": "threadId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"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, 'c5e3716925065d7419fb23efabf6691f')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,751 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 20,
|
||||
"identityHash": "7330dad871a0b42e36931ffe8c7d4bcf",
|
||||
"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, `messageDraft` TEXT, 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
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageDraft",
|
||||
"columnName": "messageDraft",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"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, `threadId` INTEGER, `isThread` INTEGER 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": "threadId",
|
||||
"columnName": "threadId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isThread",
|
||||
"columnName": "isThread",
|
||||
"affinity": "INTEGER",
|
||||
"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, `threadId` INTEGER, `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": "threadId",
|
||||
"columnName": "threadId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"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, '7330dad871a0b42e36931ffe8c7d4bcf')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,761 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 21,
|
||||
"identityHash": "8077a29304b3d28882e4b37fb10d0081",
|
||||
"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, `messageDraft` TEXT, 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
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageDraft",
|
||||
"columnName": "messageDraft",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"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, `threadId` INTEGER, `isThread` INTEGER 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, `threadTitle` TEXT, `threadReplies` INTEGER, `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": "threadId",
|
||||
"columnName": "threadId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isThread",
|
||||
"columnName": "isThread",
|
||||
"affinity": "INTEGER",
|
||||
"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": "threadTitle",
|
||||
"columnName": "threadTitle",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "threadReplies",
|
||||
"columnName": "threadReplies",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"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, `threadId` INTEGER, `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": "threadId",
|
||||
"columnName": "threadId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"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, '8077a29304b3d28882e4b37fb10d0081')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "055a9d64f28216e2981bea2fb6cc4b28",
|
||||
"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, `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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pushConfigurationState",
|
||||
"columnName": "pushConfigurationState",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "capabilities",
|
||||
"columnName": "capabilities",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientCertificate",
|
||||
"columnName": "clientCertificate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalSignalingServer",
|
||||
"columnName": "externalSignalingServer",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "current",
|
||||
"columnName": "current",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scheduledForDeletion",
|
||||
"columnName": "scheduledForDeletion",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "ArbitraryStorage",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "accountIdentifier",
|
||||
"columnName": "accountIdentifier",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "storageObject",
|
||||
"columnName": "object",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"accountIdentifier"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"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, '055a9d64f28216e2981bea2fb6cc4b28')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 9,
|
||||
"identityHash": "666fcc4bbbdf3ff121b8f1ace8fcbcb8",
|
||||
"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, `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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pushConfigurationState",
|
||||
"columnName": "pushConfigurationState",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "capabilities",
|
||||
"columnName": "capabilities",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientCertificate",
|
||||
"columnName": "clientCertificate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalSignalingServer",
|
||||
"columnName": "externalSignalingServer",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "current",
|
||||
"columnName": "current",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scheduledForDeletion",
|
||||
"columnName": "scheduledForDeletion",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"accountIdentifier",
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"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, '666fcc4bbbdf3ff121b8f1ace8fcbcb8')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
assertNotNull(appContext.getPackageName());
|
||||
assertTrue("The package name must start with 'com.nextcloud.talk2'", appContext.getPackageName().startsWith("com.nextcloud.talk2"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.activities
|
||||
|
||||
import androidx.test.espresso.intent.rule.IntentsTestRule
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class MainActivityTest {
|
||||
@get:Rule
|
||||
val activityRule: IntentsTestRule<MainActivity> = IntentsTestRule(
|
||||
MainActivity::class.java,
|
||||
true,
|
||||
false
|
||||
)
|
||||
|
||||
@Test
|
||||
fun login() {
|
||||
val sut = activityRule.launchActivity(null)
|
||||
|
||||
val user = sut.userManager.storeProfile(
|
||||
"test",
|
||||
UserManager.UserAttributes(
|
||||
null,
|
||||
serverUrl = "http://server/nc",
|
||||
currentUser = true,
|
||||
userId = "test",
|
||||
token = "test",
|
||||
displayName = "Test Name",
|
||||
pushConfigurationState = null,
|
||||
capabilities = null,
|
||||
serverVersion = null,
|
||||
certificateAlias = null,
|
||||
externalSignalingServer = null
|
||||
)
|
||||
).blockingGet()
|
||||
|
||||
assertNotNull("Error creating user", user)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.data.database.dao
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import com.nextcloud.talk.data.database.model.ChatBlockEntity
|
||||
import com.nextcloud.talk.data.database.model.ConversationEntity
|
||||
import com.nextcloud.talk.data.source.local.TalkDatabase
|
||||
import com.nextcloud.talk.data.user.UsersDao
|
||||
import com.nextcloud.talk.data.user.model.UserEntity
|
||||
import com.nextcloud.talk.models.json.conversations.ConversationEnums
|
||||
import com.nextcloud.talk.models.json.participants.Participant
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.lang.Boolean
|
||||
import kotlin.Long
|
||||
import kotlin.String
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ChatBlocksDaoTest {
|
||||
private lateinit var usersDao: UsersDao
|
||||
private lateinit var conversationsDao: ConversationsDao
|
||||
private lateinit var chatBlocksDao: ChatBlocksDao
|
||||
private lateinit var db: TalkDatabase
|
||||
private val tag = ChatBlocksDaoTest::class.java.simpleName
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = Room.inMemoryDatabaseBuilder(
|
||||
context,
|
||||
TalkDatabase::class.java
|
||||
).build()
|
||||
usersDao = db.usersDao()
|
||||
conversationsDao = db.conversationsDao()
|
||||
chatBlocksDao = db.chatBlocksDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() = db.close()
|
||||
|
||||
@Test
|
||||
fun testGetChatBlocksContainingMessageId() =
|
||||
runTest {
|
||||
val user = createUserEntity("account1", "Account 1")
|
||||
usersDao.saveUser(user)
|
||||
val account1 = usersDao.getUserWithUserId("account1").blockingGet()
|
||||
|
||||
conversationsDao.upsertConversations(
|
||||
accountId = user.id,
|
||||
listOf(
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"abc",
|
||||
roomName = "Conversation One"
|
||||
),
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"def",
|
||||
roomName = "Conversation Two"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0]
|
||||
|
||||
val chatBlock1 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = 123,
|
||||
oldestMessageId = 50,
|
||||
newestMessageId = 60,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlock2 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = 123,
|
||||
oldestMessageId = 10,
|
||||
newestMessageId = 20,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlock3 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 50,
|
||||
newestMessageId = 60,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
chatBlocksDao.upsertChatBlock(chatBlock1)
|
||||
chatBlocksDao.upsertChatBlock(chatBlock2)
|
||||
chatBlocksDao.upsertChatBlock(chatBlock3)
|
||||
|
||||
val chatBlocksOfThread = chatBlocksDao.getChatBlocksContainingMessageId(
|
||||
internalConversationId = conversation1.internalId,
|
||||
threadId = 123,
|
||||
messageId = 55
|
||||
)
|
||||
|
||||
assertEquals(1, chatBlocksOfThread.first().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetConnectedChatBlocks() =
|
||||
runTest {
|
||||
val user = createUserEntity("account1", "Account 1")
|
||||
usersDao.saveUser(user)
|
||||
val account1 = usersDao.getUserWithUserId("account1").blockingGet()
|
||||
|
||||
conversationsDao.upsertConversations(
|
||||
account1.id,
|
||||
listOf(
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"abc",
|
||||
roomName = "Conversation One"
|
||||
),
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"def",
|
||||
roomName = "Conversation Two"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0]
|
||||
val conversation2 = conversationsDao.getConversationsForUser(account1.id).first()[1]
|
||||
|
||||
val searchedChatBlock = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 50,
|
||||
newestMessageId = 60,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockTooOld = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 10,
|
||||
newestMessageId = 20,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockOverlap1 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 45,
|
||||
newestMessageId = 55,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockWithin = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 52,
|
||||
newestMessageId = 58,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockOverall = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 1,
|
||||
newestMessageId = 99,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockOverlap2 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 59,
|
||||
newestMessageId = 70,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockTooNew = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 80,
|
||||
newestMessageId = 90,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockWithinButOtherConversation = ChatBlockEntity(
|
||||
internalConversationId = conversation2.internalId,
|
||||
accountId = conversation2.accountId,
|
||||
token = conversation2.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 53,
|
||||
newestMessageId = 57,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
chatBlocksDao.upsertChatBlock(searchedChatBlock)
|
||||
|
||||
chatBlocksDao.upsertChatBlock(chatBlockTooOld)
|
||||
chatBlocksDao.upsertChatBlock(chatBlockOverlap1)
|
||||
chatBlocksDao.upsertChatBlock(chatBlockWithin)
|
||||
chatBlocksDao.upsertChatBlock(chatBlockOverall)
|
||||
chatBlocksDao.upsertChatBlock(chatBlockOverlap2)
|
||||
chatBlocksDao.upsertChatBlock(chatBlockTooNew)
|
||||
chatBlocksDao.upsertChatBlock(chatBlockWithinButOtherConversation)
|
||||
|
||||
val results = chatBlocksDao.getConnectedChatBlocks(
|
||||
internalConversationId = conversation1.internalId,
|
||||
threadId = null,
|
||||
oldestMessageId = searchedChatBlock.oldestMessageId,
|
||||
newestMessageId = searchedChatBlock.newestMessageId
|
||||
)
|
||||
|
||||
assertEquals(5, results.first().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetConnectedChatBlocksWithThreadsScenario() =
|
||||
runTest {
|
||||
val user = createUserEntity("account1", "Account 1")
|
||||
usersDao.saveUser(user)
|
||||
val account1 = usersDao.getUserWithUserId("account1").blockingGet()
|
||||
|
||||
conversationsDao.upsertConversations(
|
||||
account1.id,
|
||||
listOf(
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"abc",
|
||||
roomName = "Conversation One"
|
||||
),
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"def",
|
||||
roomName = "Conversation Two"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0]
|
||||
|
||||
val searchedChatBlock = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = 123,
|
||||
oldestMessageId = 50,
|
||||
newestMessageId = 60,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockOverlap1 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 45,
|
||||
newestMessageId = 55,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockOverlap2 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = 123,
|
||||
oldestMessageId = 59,
|
||||
newestMessageId = 70,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
chatBlocksDao.upsertChatBlock(searchedChatBlock)
|
||||
|
||||
chatBlocksDao.upsertChatBlock(chatBlockOverlap1)
|
||||
chatBlocksDao.upsertChatBlock(chatBlockOverlap2)
|
||||
|
||||
val resultsForThreadIdNull = chatBlocksDao.getConnectedChatBlocks(
|
||||
internalConversationId = conversation1.internalId,
|
||||
threadId = null,
|
||||
oldestMessageId = searchedChatBlock.oldestMessageId,
|
||||
newestMessageId = searchedChatBlock.newestMessageId
|
||||
)
|
||||
|
||||
assertEquals(1, resultsForThreadIdNull.first().size)
|
||||
|
||||
val resultsForThreadId123 = chatBlocksDao.getConnectedChatBlocks(
|
||||
internalConversationId = conversation1.internalId,
|
||||
threadId = 123,
|
||||
oldestMessageId = searchedChatBlock.oldestMessageId,
|
||||
newestMessageId = searchedChatBlock.newestMessageId
|
||||
)
|
||||
|
||||
assertEquals(2, resultsForThreadId123.first().size)
|
||||
}
|
||||
|
||||
private fun createUserEntity(userId: String, userName: String) =
|
||||
UserEntity(
|
||||
userId = userId,
|
||||
username = userName,
|
||||
baseUrl = null,
|
||||
token = null,
|
||||
displayName = null,
|
||||
pushConfigurationState = null,
|
||||
capabilities = null,
|
||||
serverVersion = null,
|
||||
clientCertificate = null,
|
||||
externalSignalingServer = null,
|
||||
current = Boolean.FALSE,
|
||||
scheduledForDeletion = Boolean.FALSE
|
||||
)
|
||||
|
||||
private fun createConversationEntity(accountId: Long, token: String, roomName: String) =
|
||||
ConversationEntity(
|
||||
internalId = "$accountId@$token",
|
||||
accountId = accountId,
|
||||
token = token,
|
||||
name = roomName,
|
||||
actorId = "",
|
||||
actorType = "",
|
||||
messageExpiration = 0,
|
||||
unreadMessages = 0,
|
||||
statusMessage = null,
|
||||
lastMessage = null,
|
||||
canDeleteConversation = false,
|
||||
canLeaveConversation = false,
|
||||
lastCommonReadMessage = 0,
|
||||
lastReadMessage = 0,
|
||||
type = ConversationEnums.ConversationType.DUMMY,
|
||||
status = "",
|
||||
callFlag = 1,
|
||||
favorite = false,
|
||||
lastPing = 0,
|
||||
hasCall = false,
|
||||
sessionId = "",
|
||||
canStartCall = false,
|
||||
lastActivity = 0,
|
||||
remoteServer = "",
|
||||
avatarVersion = "",
|
||||
unreadMentionDirect = false,
|
||||
callRecording = 1,
|
||||
callStartTime = 0,
|
||||
statusClearAt = 0,
|
||||
unreadMention = false,
|
||||
lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY,
|
||||
lobbyTimer = 0,
|
||||
objectType = ConversationEnums.ObjectType.FILE,
|
||||
objectId = "",
|
||||
statusIcon = null,
|
||||
description = "",
|
||||
displayName = "",
|
||||
hasPassword = false,
|
||||
permissions = 0,
|
||||
notificationCalls = 0,
|
||||
remoteToken = "",
|
||||
notificationLevel = ConversationEnums.NotificationLevel.ALWAYS,
|
||||
conversationReadOnlyState = ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY,
|
||||
hasCustomAvatar = false,
|
||||
participantType = Participant.ParticipantType.DUMMY,
|
||||
recordingConsentRequired = 1
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.data.database.dao
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.data.database.model.ChatMessageEntity
|
||||
import com.nextcloud.talk.data.database.model.ConversationEntity
|
||||
import com.nextcloud.talk.data.source.local.TalkDatabase
|
||||
import com.nextcloud.talk.data.user.UsersDao
|
||||
import com.nextcloud.talk.data.user.model.UserEntity
|
||||
import com.nextcloud.talk.models.json.conversations.ConversationEnums
|
||||
import com.nextcloud.talk.models.json.participants.Participant
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ChatMessagesDaoTest {
|
||||
|
||||
private lateinit var usersDao: UsersDao
|
||||
private lateinit var conversationsDao: ConversationsDao
|
||||
private lateinit var chatMessagesDao: ChatMessagesDao
|
||||
private lateinit var db: TalkDatabase
|
||||
private val tag = ChatMessagesDaoTest::class.java.simpleName
|
||||
|
||||
var chatMessageCounter: Long = 1
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = Room.inMemoryDatabaseBuilder(
|
||||
context,
|
||||
TalkDatabase::class.java
|
||||
).build()
|
||||
usersDao = db.usersDao()
|
||||
conversationsDao = db.conversationsDao()
|
||||
chatMessagesDao = db.chatMessagesDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() = db.close()
|
||||
|
||||
@Test
|
||||
fun test() =
|
||||
runTest {
|
||||
usersDao.saveUser(createUserEntity("account1", "Account 1"))
|
||||
usersDao.saveUser(createUserEntity("account2", "Account 2"))
|
||||
|
||||
val account1 = usersDao.getUserWithUserId("account1").blockingGet()
|
||||
val account2 = usersDao.getUserWithUserId("account2").blockingGet()
|
||||
|
||||
// Problem: lets say we want to update the conv list -> We don#t know the primary keys!
|
||||
// with account@token that would be easier!
|
||||
conversationsDao.upsertConversations(
|
||||
account1.id,
|
||||
listOf(
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
roomName = "Conversation One"
|
||||
),
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
roomName = "Conversation Two"
|
||||
),
|
||||
createConversationEntity(
|
||||
accountId = account2.id,
|
||||
roomName = "Conversation Three"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(2, conversationsDao.getConversationsForUser(account1.id).first().size)
|
||||
assertEquals(1, conversationsDao.getConversationsForUser(account2.id).first().size)
|
||||
|
||||
// Lets imagine we are on conversations screen...
|
||||
conversationsDao.getConversationsForUser(account1.id).first().forEach {
|
||||
Log.d(tag, "- next Conversation for account1 -")
|
||||
Log.d(tag, "internalId (PK): " + it.internalId)
|
||||
Log.d(tag, "accountId: " + it.accountId)
|
||||
Log.d(tag, "name: " + it.name)
|
||||
Log.d(tag, "token: " + it.token)
|
||||
}
|
||||
|
||||
// User sees all conversations and clicks on a item. That's how we get a conversation
|
||||
val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0]
|
||||
val conversation2 = conversationsDao.getConversationsForUser(account1.id).first()[1]
|
||||
|
||||
// Having a conversation token, we can also get a conversation directly
|
||||
val conversation1GotByToken = conversationsDao.getConversationForUser(
|
||||
account1.id,
|
||||
conversation1.token!!
|
||||
).first()
|
||||
|
||||
assertEquals(conversation1, conversation1GotByToken)
|
||||
|
||||
// Lets insert some messages to the conversations
|
||||
chatMessagesDao.upsertChatMessages(
|
||||
listOf(
|
||||
createChatMessageEntity(conversation1.internalId, "hello"),
|
||||
createChatMessageEntity(conversation1.internalId, "here"),
|
||||
createChatMessageEntity(conversation1.internalId, "are"),
|
||||
createChatMessageEntity(conversation1.internalId, "some"),
|
||||
createChatMessageEntity(conversation1.internalId, "messages")
|
||||
)
|
||||
)
|
||||
chatMessagesDao.upsertChatMessages(
|
||||
listOf(
|
||||
createChatMessageEntity(conversation2.internalId, "first message in conversation 2")
|
||||
)
|
||||
)
|
||||
|
||||
chatMessagesDao.getMessagesForConversation(conversation1.internalId).first().forEach {
|
||||
Log.d(tag, "- next Message for conversation1 (account1)-")
|
||||
Log.d(tag, "id (PK): " + it.id)
|
||||
Log.d(tag, "message: " + it.message)
|
||||
}
|
||||
|
||||
val chatMessagesConv1 = chatMessagesDao.getMessagesForConversation(conversation1.internalId)
|
||||
assertEquals(5, chatMessagesConv1.first().size)
|
||||
|
||||
val chatMessagesConv2 = chatMessagesDao.getMessagesForConversation(conversation2.internalId)
|
||||
assertEquals(1, chatMessagesConv2.first().size)
|
||||
|
||||
assertEquals("some", chatMessagesConv1.first()[1].message)
|
||||
|
||||
val conv1chatMessage3 = chatMessagesDao.getChatMessageForConversation(conversation1.internalId, 3).first()
|
||||
assertEquals("are", conv1chatMessage3.message)
|
||||
|
||||
val chatMessagesConv1Since =
|
||||
chatMessagesDao.getMessagesForConversationSince(
|
||||
conversation1.internalId,
|
||||
conv1chatMessage3.id,
|
||||
null
|
||||
)
|
||||
assertEquals(3, chatMessagesConv1Since.first().size)
|
||||
assertEquals("are", chatMessagesConv1Since.first()[0].message)
|
||||
assertEquals("some", chatMessagesConv1Since.first()[1].message)
|
||||
assertEquals("messages", chatMessagesConv1Since.first()[2].message)
|
||||
|
||||
val chatMessagesConv1To =
|
||||
chatMessagesDao.getMessagesForConversationBeforeAndEqual(
|
||||
conversation1.internalId,
|
||||
conv1chatMessage3.id,
|
||||
3,
|
||||
null
|
||||
)
|
||||
assertEquals(3, chatMessagesConv1To.first().size)
|
||||
assertEquals("hello", chatMessagesConv1To.first()[2].message)
|
||||
assertEquals("here", chatMessagesConv1To.first()[1].message)
|
||||
assertEquals("are", chatMessagesConv1To.first()[0].message)
|
||||
}
|
||||
|
||||
private fun createUserEntity(userId: String, userName: String) =
|
||||
UserEntity(
|
||||
userId = userId,
|
||||
username = userName,
|
||||
baseUrl = null,
|
||||
token = null,
|
||||
displayName = null,
|
||||
pushConfigurationState = null,
|
||||
capabilities = null,
|
||||
serverVersion = null,
|
||||
clientCertificate = null,
|
||||
externalSignalingServer = null,
|
||||
current = java.lang.Boolean.FALSE,
|
||||
scheduledForDeletion = java.lang.Boolean.FALSE
|
||||
)
|
||||
|
||||
private fun createConversationEntity(accountId: Long, roomName: String): ConversationEntity {
|
||||
val token = (0..10000000).random().toString()
|
||||
|
||||
return ConversationEntity(
|
||||
internalId = "$accountId@$token",
|
||||
accountId = accountId,
|
||||
token = token,
|
||||
name = roomName,
|
||||
actorId = "",
|
||||
actorType = "",
|
||||
messageExpiration = 0,
|
||||
unreadMessages = 0,
|
||||
statusMessage = null,
|
||||
lastMessage = null,
|
||||
canDeleteConversation = false,
|
||||
canLeaveConversation = false,
|
||||
lastCommonReadMessage = 0,
|
||||
lastReadMessage = 0,
|
||||
type = ConversationEnums.ConversationType.DUMMY,
|
||||
status = "",
|
||||
callFlag = 1,
|
||||
favorite = false,
|
||||
lastPing = 0,
|
||||
hasCall = false,
|
||||
sessionId = "",
|
||||
canStartCall = false,
|
||||
lastActivity = 0,
|
||||
remoteServer = "",
|
||||
avatarVersion = "",
|
||||
unreadMentionDirect = false,
|
||||
callRecording = 1,
|
||||
callStartTime = 0,
|
||||
statusClearAt = 0,
|
||||
unreadMention = false,
|
||||
lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY,
|
||||
lobbyTimer = 0,
|
||||
objectType = ConversationEnums.ObjectType.FILE,
|
||||
objectId = "",
|
||||
statusIcon = null,
|
||||
description = "",
|
||||
displayName = "",
|
||||
hasPassword = false,
|
||||
permissions = 0,
|
||||
notificationCalls = 0,
|
||||
remoteToken = "",
|
||||
notificationLevel = ConversationEnums.NotificationLevel.ALWAYS,
|
||||
conversationReadOnlyState = ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY,
|
||||
hasCustomAvatar = false,
|
||||
participantType = Participant.ParticipantType.DUMMY,
|
||||
recordingConsentRequired = 1
|
||||
)
|
||||
}
|
||||
|
||||
private fun createChatMessageEntity(internalConversationId: String, message: String): ChatMessageEntity {
|
||||
val id = chatMessageCounter++
|
||||
|
||||
val emoji1 = "\uD83D\uDE00" // 😀
|
||||
val emoji2 = "\uD83D\uDE1C" // 😜
|
||||
val reactions = LinkedHashMap<String, Int>()
|
||||
reactions[emoji1] = 3
|
||||
reactions[emoji2] = 4
|
||||
|
||||
val reactionsSelf = ArrayList<String>()
|
||||
reactionsSelf.add(emoji1)
|
||||
|
||||
val entity = ChatMessageEntity(
|
||||
internalId = "$internalConversationId@$id",
|
||||
internalConversationId = internalConversationId,
|
||||
id = id,
|
||||
message = message,
|
||||
reactions = reactions,
|
||||
reactionsSelf = reactionsSelf,
|
||||
deleted = false,
|
||||
token = "",
|
||||
actorId = "",
|
||||
actorType = "",
|
||||
accountId = 1,
|
||||
messageParameters = null,
|
||||
messageType = "",
|
||||
parentMessageId = null,
|
||||
systemMessageType = ChatMessage.SystemMessageType.DUMMY,
|
||||
replyable = false,
|
||||
timestamp = 0,
|
||||
expirationTimestamp = 0,
|
||||
actorDisplayName = "",
|
||||
lastEditActorType = null,
|
||||
lastEditTimestamp = null,
|
||||
renderMarkdown = true,
|
||||
lastEditActorId = "",
|
||||
lastEditActorDisplayName = ""
|
||||
)
|
||||
return entity
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@email.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.data.database.migrations
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.nextcloud.talk.data.source.local.Migrations
|
||||
import com.nextcloud.talk.data.source.local.TalkDatabase
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MigrationsTest {
|
||||
companion object {
|
||||
private const val TEST_DB = "migration-test"
|
||||
private const val INIT_VERSION = 10 // last version before update to offline first
|
||||
private val TAG = MigrationsTest::class.java.simpleName
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
TalkDatabase::class.java
|
||||
)
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
@Suppress("SpreadOperator")
|
||||
fun migrateAll() {
|
||||
helper.createDatabase(TEST_DB, INIT_VERSION).apply {
|
||||
close()
|
||||
}
|
||||
|
||||
Room.databaseBuilder(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
TalkDatabase::class.java,
|
||||
TEST_DB
|
||||
).addMigrations(*TalkDatabase.MIGRATIONS).build().apply {
|
||||
openHelper.writableDatabase.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate10To11() {
|
||||
helper.createDatabase(TEST_DB, 10).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 11, true, Migrations.MIGRATION_10_11)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate11To12() {
|
||||
helper.createDatabase(TEST_DB, 11).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 12, true, Migrations.MIGRATION_11_12)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate12To13() {
|
||||
helper.createDatabase(TEST_DB, 12).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 13, true, Migrations.MIGRATION_12_13)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate13To14() {
|
||||
helper.createDatabase(TEST_DB, 13).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 14, true, Migrations.MIGRATION_13_14)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate14To15() {
|
||||
helper.createDatabase(TEST_DB, 14).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 15, true, Migrations.MIGRATION_14_15)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate15To16() {
|
||||
helper.createDatabase(TEST_DB, 15).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 16, true, Migrations.MIGRATION_15_16)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate17To19() {
|
||||
helper.createDatabase(TEST_DB, 17).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 19, true, Migrations.MIGRATION_17_19)
|
||||
}
|
||||
}
|
||||
132
app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java
Normal file
132
app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.nextcloud.talk.R;
|
||||
import com.nextcloud.talk.activities.MainActivity;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.espresso.NoMatchingViewException;
|
||||
import androidx.test.espresso.web.webdriver.DriverAtoms;
|
||||
import androidx.test.espresso.web.webdriver.Locator;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static androidx.test.espresso.web.sugar.Web.onWebView;
|
||||
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
|
||||
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
|
||||
//@LargeTest
|
||||
@Ignore("This test is ignored because it constantly fails on CI")
|
||||
public class LoginIT {
|
||||
|
||||
@Test
|
||||
public void login() throws InterruptedException {
|
||||
|
||||
ActivityScenario<MainActivity> activityScenario = ActivityScenario.launch(MainActivity.class);
|
||||
|
||||
Bundle arguments = androidx.test.platform.app.InstrumentationRegistry.getArguments();
|
||||
|
||||
String baseUrl = arguments.getString("TEST_SERVER_URL");
|
||||
String loginName = arguments.getString("TEST_SERVER_USERNAME");
|
||||
String password = arguments.getString("TEST_SERVER_PASSWORD");
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
try {
|
||||
onView(withId(R.id.serverEntryTextInputEditText)).check(matches(isDisplayed()));
|
||||
} catch (NoMatchingViewException e) {
|
||||
try {
|
||||
// can happen that an invalid account from previous tests is existing
|
||||
onView(withText(R.string.nc_settings_remove_account)).perform(click());
|
||||
Thread.sleep(2000);
|
||||
} catch (NoMatchingViewException ie) {
|
||||
// is OK if the dialog is not shown
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete account if exists
|
||||
onView(withId(R.id.switch_account_button)).perform(click());
|
||||
onView(withId(R.id.manage_settings)).perform(click());
|
||||
onView(withId(R.id.settings_remove_account)).perform(click());
|
||||
onView(withText(R.string.nc_settings_remove)).perform(click());
|
||||
// The remove button must be clicked two times
|
||||
onView(withId(R.id.settings_remove_account)).perform(click());
|
||||
// And yes: The button must be clicked two times
|
||||
onView(withText(R.string.nc_settings_remove)).perform(click());
|
||||
onView(withText(R.string.nc_settings_remove)).perform(click());
|
||||
} catch (Exception ie2) {
|
||||
// ignore
|
||||
} finally {
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onView(withId(R.id.serverEntryTextInputEditText)).perform(typeText(baseUrl));
|
||||
// Click on EditText's drawable right
|
||||
onView(withContentDescription(R.string.nc_server_connect)).perform(click());
|
||||
|
||||
Thread.sleep(4000);
|
||||
|
||||
onWebView().forceJavascriptEnabled();
|
||||
|
||||
// click on login
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.XPATH, "//p[@id='redirect-link']/a"))
|
||||
.perform(webClick());
|
||||
|
||||
// username
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.XPATH, "//input[@id='user']"))
|
||||
.perform(DriverAtoms.webKeys(loginName));
|
||||
|
||||
// password
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.XPATH, "//input[@id='password']"))
|
||||
.perform(DriverAtoms.webKeys(password));
|
||||
|
||||
// click login
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.XPATH, "//input[@id='submit-form']"))
|
||||
.perform(webClick());
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
// grant access
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.XPATH, "//input[@type='submit']"))
|
||||
.perform(webClick());
|
||||
|
||||
Thread.sleep(5 * 1000);
|
||||
|
||||
onView(withId(R.id.switch_account_button)).perform(click());
|
||||
onView(withId(R.id.user_name)).check(matches(withText("User One")));
|
||||
|
||||
activityScenario.onActivity(activity -> {
|
||||
assertEquals(loginName,
|
||||
Objects.requireNonNull(activity.currentUserProvider.getCurrentUser().blockingGet()).getUserId());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* 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 org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class ColorGeneratorTest {
|
||||
|
||||
@Test
|
||||
fun testUsernameToColor() {
|
||||
usernameToColorHexHelper("", "#0082c9")
|
||||
usernameToColorHexHelper(",", "#1e78c1")
|
||||
usernameToColorHexHelper(".", "#c98879")
|
||||
usernameToColorHexHelper("admin", "#d09e6d")
|
||||
usernameToColorHexHelper("123e4567-e89b-12d3-a456-426614174000", "#bc5c91")
|
||||
usernameToColorHexHelper("Akeel Robertson", "#9750a4")
|
||||
usernameToColorHexHelper("Brayden Truong", "#d09e6d")
|
||||
usernameToColorHexHelper("Daphne Roy", "#9750a4")
|
||||
usernameToColorHexHelper("Ellena Wright Frederic Conway", "#c37285")
|
||||
usernameToColorHexHelper("Gianluca Hills", "#d6b461")
|
||||
usernameToColorHexHelper("Haseeb Stephens", "#d6b461")
|
||||
usernameToColorHexHelper("Idris Mac", "#9750a4")
|
||||
usernameToColorHexHelper("Kristi Fisher", "#0082c9")
|
||||
usernameToColorHexHelper("Lillian Wall", "#bc5c91")
|
||||
usernameToColorHexHelper("Lorelai Taylor", "#ddcb55")
|
||||
usernameToColorHexHelper("Madina Knight", "#9750a4")
|
||||
usernameToColorHexHelper("Meeting", "#c98879")
|
||||
usernameToColorHexHelper("Private Circle", "#c37285")
|
||||
usernameToColorHexHelper("Rae Hope", "#795aab")
|
||||
usernameToColorHexHelper("Santiago Singleton", "#bc5c91")
|
||||
usernameToColorHexHelper("Sid Combs", "#d09e6d")
|
||||
usernameToColorHexHelper("TestCircle", "#499aa2")
|
||||
usernameToColorHexHelper("Tom Mörtel", "#248eb5")
|
||||
usernameToColorHexHelper("Vivienne Jacobs", "#1e78c1")
|
||||
usernameToColorHexHelper("Zaki Cortes", "#6ea68f")
|
||||
usernameToColorHexHelper("a user", "#5b64b3")
|
||||
usernameToColorHexHelper("admin@cloud.example.com", "#9750a4")
|
||||
usernameToColorHexHelper("another user", "#ddcb55")
|
||||
usernameToColorHexHelper("asd", "#248eb5")
|
||||
usernameToColorHexHelper("bar", "#0082c9")
|
||||
usernameToColorHexHelper("foo", "#d09e6d")
|
||||
usernameToColorHexHelper("wasd", "#b6469d")
|
||||
usernameToColorHexHelper("مرحبا بالعالم", "#c98879")
|
||||
usernameToColorHexHelper("🙈", "#b6469d")
|
||||
}
|
||||
|
||||
private fun usernameToColorHexHelper(username: String, expectedHexColor: String) {
|
||||
val userColorInt = ColorGenerator.usernameToColor(username) // returns Int
|
||||
val userHexColor = intToHex(userColorInt)
|
||||
|
||||
Assert.assertEquals(expectedHexColor.lowercase(), userHexColor.lowercase())
|
||||
}
|
||||
|
||||
private fun intToHex(colorInt: Int): String {
|
||||
val r = Color.red(colorInt)
|
||||
val g = Color.green(colorInt)
|
||||
val b = Color.blue(colorInt)
|
||||
return String.format("#%02x%02x%02x", r, g, b)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.utils
|
||||
|
||||
import at.bitfire.dav4jvm.HttpUtils
|
||||
import org.apache.commons.lang3.time.DateUtils
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Ignore("Test fails on CI server. See issue https://github.com/nextcloud/talk-android/issues/1737")
|
||||
class ShareUtilsIT {
|
||||
@Test
|
||||
fun date() {
|
||||
assertEquals(TEST_DATE_IN_MILLIS, parseDate2("Mon, 09 Apr 2008 23:55:38 GMT").time)
|
||||
assertEquals(TEST_DATE_IN_MILLIS, HttpUtils.parseDate("Mon, 09 Apr 2008 23:55:38 GMT")?.time)
|
||||
}
|
||||
|
||||
private fun parseDate2(dateStr: String): Date =
|
||||
DateUtils.parseDate(
|
||||
dateStr, Locale.US,
|
||||
HttpUtils.httpDateFormatStr,
|
||||
// RFC 822, updated by RFC 1123 with any TZ
|
||||
"EEE, dd MMM yyyy HH:mm:ss zzz",
|
||||
// RFC 850, obsoleted by RFC 1036 with any TZ.
|
||||
"EEEE, dd-MMM-yy HH:mm:ss zzz",
|
||||
// ANSI C's asctime() format
|
||||
"EEE MMM d HH:mm:ss yyyy",
|
||||
// Alternative formats.
|
||||
"EEE, dd-MMM-yyyy HH:mm:ss z",
|
||||
"EEE, dd-MMM-yyyy HH-mm-ss z",
|
||||
"EEE, dd MMM yy HH:mm:ss z",
|
||||
"EEE dd-MMM-yyyy HH:mm:ss z",
|
||||
"EEE dd MMM yyyy HH:mm:ss z",
|
||||
"EEE dd-MMM-yyyy HH-mm-ss z",
|
||||
"EEE dd-MMM-yy HH:mm:ss z",
|
||||
"EEE dd MMM yy HH:mm:ss z",
|
||||
"EEE,dd-MMM-yy HH:mm:ss z",
|
||||
"EEE,dd-MMM-yyyy HH:mm:ss z",
|
||||
"EEE, dd-MM-yyyy HH:mm:ss z",
|
||||
// RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com
|
||||
"EEE MMM d yyyy HH:mm:ss z"
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val TEST_DATE_IN_MILLIS = 1207778138000
|
||||
}
|
||||
}
|
||||
142
app/src/androidTest/java/com/nextcloud/talk/utils/UriUtilsIT.kt
Normal file
142
app/src/androidTest/java/com/nextcloud/talk/utils/UriUtilsIT.kt
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Samanwith KSN <samanwith21@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.utils
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertFalse
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class UriUtilsIT {
|
||||
|
||||
@Test
|
||||
fun testHasHttpProtocolPrefixed() {
|
||||
val uriHttp = "http://www.example.com"
|
||||
val resultHttp = UriUtils.hasHttpProtocolPrefixed(uriHttp)
|
||||
assertTrue(resultHttp)
|
||||
|
||||
val uriHttps = "https://www.example.com"
|
||||
val resultHttps = UriUtils.hasHttpProtocolPrefixed(uriHttps)
|
||||
assertTrue(resultHttps)
|
||||
|
||||
val uriWithoutPrefix = "www.example.com"
|
||||
val resultWithoutPrefix = UriUtils.hasHttpProtocolPrefixed(uriWithoutPrefix)
|
||||
assertFalse(resultWithoutPrefix)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractInstanceInternalFileFileId() {
|
||||
assertEquals(
|
||||
"42",
|
||||
UriUtils.extractInstanceInternalFileFileId(
|
||||
"https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=42"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractInstanceInternalFileShareFileId() {
|
||||
assertEquals(
|
||||
"42",
|
||||
UriUtils.extractInstanceInternalFileShareFileId("https://cloud.nextcloud.com/f/42")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsInstanceInternalFileShareUrl() {
|
||||
assertTrue(
|
||||
UriUtils.isInstanceInternalFileShareUrl(
|
||||
"https://cloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/f/42"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileShareUrl(
|
||||
"https://nextcloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/f/42"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileShareUrl(
|
||||
"https://nextcloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/f/"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileShareUrl(
|
||||
"https://nextcloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/f/test123"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsInstanceInternalFileUrl() {
|
||||
assertTrue(
|
||||
UriUtils.isInstanceInternalFileUrl(
|
||||
"https://cloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileUrl(
|
||||
"https://nextcloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileUrl(
|
||||
"https://nextcloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=test123"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileUrl(
|
||||
"https://nextcloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid="
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileUrl(
|
||||
"https://cloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/apps/files/?dir=/Engineering"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsInstanceInternalFileUrlNew() {
|
||||
assertTrue(
|
||||
UriUtils.isInstanceInternalFileUrlNew(
|
||||
"https://cloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/apps/files/files/41?dir=/"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileUrlNew(
|
||||
"https://nextcloud.nextcloud.com",
|
||||
"https://cloud.nextcloud.com/apps/files/files/41?dir=/"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractInstanceInternalFileFileIdNew() {
|
||||
assertEquals(
|
||||
"42",
|
||||
UriUtils.extractInstanceInternalFileFileIdNew("https://cloud.nextcloud.com/apps/files/files/42?dir=/")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Samanwith KSN <samanwith21@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.junit.MockitoJUnitRunner
|
||||
|
||||
@RunWith(MockitoJUnitRunner::class)
|
||||
class VibrationUtilsTest {
|
||||
|
||||
@Mock
|
||||
private lateinit var mockContext: Context
|
||||
|
||||
@Mock
|
||||
private lateinit var mockVibrator: Vibrator
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Mockito.`when`(mockContext.getSystemService(Context.VIBRATOR_SERVICE)).thenReturn(mockVibrator)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVibrateShort() {
|
||||
VibrationUtils.vibrateShort(mockContext)
|
||||
Mockito.verify(mockVibrator)
|
||||
.vibrate(
|
||||
VibrationEffect
|
||||
.createOneShot(
|
||||
VibrationUtils.SHORT_VIBRATE,
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
Birebir ya da grup ile ses ya da görüntü çağrıları yapmak, web konferansları oluşturmak ve sohbet iletileri göndermek için Nextcloud Talk uygulamasını kullanabilisiniz. Tüm bilgiler tamamen şifrelenmiş olarak sizin sunucunuz üzerinden iletilerek olabilecek en yüksek gizlilik düzeyi sağlanır.
|
||||
|
||||
Nextcloud Talk uygulamasının kullanımı kolaydır ve herhangi bir ücret ödemeniz gerekmez!
|
||||
|
||||
Nextcloud Talk şu özellikleri destekler:
|
||||
* HD (H.265) ses ve görüntü çağrıları
|
||||
* Grup ve birebir çağrılar
|
||||
* Webinar ve herkese açık web toplantıları
|
||||
* Bireysel ve grup sohbetleri
|
||||
* Kolay ekran paylaşımı
|
||||
* Android ve iOS uygulamaları
|
||||
* Mobil çağrı ve anında sohbet bildirimleri
|
||||
* Nextcloud Files ve Nextcloud Groupware ile bütünleşme
|
||||
* Kendi veri merkezinizde, %100 açık kaynaklı
|
||||
* Uçtan uca şifrelenmiş çağrılar
|
||||
* Milyonlarca kullanıcıya göre
|
||||
* SIP geçidi üzerinden telefon ile arama
|
||||
|
||||
Nextcloud Talk uygulamasının kullanılabilmesi için Nextcloud Talk sunucusu gereklidir. Nextcloud verilerinizin kontrolunu yeniden size veren, size özel, kendi sunucularınızda barındırabileceğiniz bir dosya eşitleme ve iletişim platformudur. Evinizde, hizmet sağlayıcınızda ya da kurumunuzda bulunan bir sunucu üzerinde kullanarak, belgelerinize, takvimlerinize, kişilerinize, e-postalarınıza ve diğer verilerinize erişmenizi sağlar. Verilerinizi farklı Nextcloud sunucuları kullanan kişiler ile paylaşarak dosyalar üzerinde birlikte çalışabilirsiniz. Nextcloud tamamen açık kaynaklı olduğundan özelliklerini gereksinimlerinize göre genişletebilir, geliştirilmesine katkıda bulunabilir ya da vaatlerimizin doğruluğunu görebilirsiniz.
|
||||
|
||||
|
||||
Dünya üzerinde milyonlarca kullanıcı ev ya da iş yerlerinde günlük işlemleri için Nextcloud kullanıyor. Kurumsal kullanıcılar Nextcloud GmbH tarafından sunulan destek hizmetleri ile tamamen kendi BT bölümlerinin denetimindeki kurumsal üretim ve iş birliği platformu için eksiksiz destek alıyorlar.
|
||||
|
||||
|
||||
Ayrıntılı bilgi almak için https://nextcloud.com/talk
|
||||
|
||||
Nextcloud web sitesi https://nextcloud.com
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.utils;
|
||||
|
||||
|
||||
import com.nextcloud.talk.interfaces.ClosedInterface;
|
||||
|
||||
public class ClosedInterfaceImpl implements ClosedInterface {
|
||||
@Override
|
||||
public void providerInstallerInstallIfNeededAsync() {
|
||||
// does absolutely nothing :)
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGooglePlayServicesAvailable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUpPushTokenRegistration() {
|
||||
// no push notifications for generic build variant
|
||||
}
|
||||
}
|
||||
36
app/src/gplay/AndroidManifest.xml
Normal file
36
app/src/gplay/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!--
|
||||
~ Nextcloud Talk - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
~ SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
|
||||
~ SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:name=".application.NextcloudTalkApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/nc_app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme.Launcher"
|
||||
tools:replace="label, icon, theme, name, allowBackup"
|
||||
tools:ignore="UnusedAttribute, ExportedService">
|
||||
|
||||
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
|
||||
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
|
||||
|
||||
<service
|
||||
android:name=".services.firebase.NCFirebaseMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.jobs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.gms.tasks.OnCompleteListener
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerParameters) :
|
||||
Worker(context, workerParameters) {
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@SuppressLint("LongLogTag")
|
||||
override fun doWork(): Result {
|
||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
FirebaseMessaging.getInstance().token.addOnCompleteListener(
|
||||
OnCompleteListener { task ->
|
||||
if (!task.isSuccessful) {
|
||||
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
|
||||
return@OnCompleteListener
|
||||
}
|
||||
|
||||
val pushToken = task.result
|
||||
Log.d(TAG, "Fetched firebase push token is: $pushToken")
|
||||
|
||||
appPreferences.pushToken = pushToken
|
||||
appPreferences.pushTokenLatestFetch = System.currentTimeMillis()
|
||||
|
||||
val data: Data =
|
||||
Data.Builder().putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker").build()
|
||||
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueue(pushRegistrationWork)
|
||||
}
|
||||
)
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = GetFirebasePushTokenWorker::class.simpleName
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.services.firebase
|
||||
|
||||
import android.util.Log
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.jobs.NotificationWorker
|
||||
import com.nextcloud.talk.jobs.PushRegistrationWorker
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class NCFirebaseMessagingService : FirebaseMessagingService() {
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
override fun onCreate() {
|
||||
Log.d(TAG, "onCreate")
|
||||
super.onCreate()
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||
Log.d(TAG, "onMessageReceived")
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
Log.d(TAG, "remoteMessage.priority: " + remoteMessage.priority)
|
||||
Log.d(TAG, "remoteMessage.originalPriority: " + remoteMessage.originalPriority)
|
||||
|
||||
val data = remoteMessage.data
|
||||
val subject = data[KEY_NOTIFICATION_SUBJECT]
|
||||
val signature = data[KEY_NOTIFICATION_SIGNATURE]
|
||||
|
||||
if (!subject.isNullOrEmpty() && !signature.isNullOrEmpty()) {
|
||||
val messageData = Data.Builder()
|
||||
.putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject)
|
||||
.putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature)
|
||||
.build()
|
||||
val notificationWork =
|
||||
OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData)
|
||||
.build()
|
||||
WorkManager.getInstance().enqueue(notificationWork)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
Log.d(TAG, "onNewToken. token = $token")
|
||||
|
||||
appPreferences.pushToken = token
|
||||
appPreferences.pushTokenLatestGeneration = System.currentTimeMillis()
|
||||
|
||||
val data: Data =
|
||||
Data.Builder().putString(PushRegistrationWorker.ORIGIN, "NCFirebaseMessagingService#onNewToken").build()
|
||||
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
WorkManager.getInstance().enqueue(pushRegistrationWork)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = NCFirebaseMessagingService::class.simpleName
|
||||
const val KEY_NOTIFICATION_SUBJECT = "subject"
|
||||
const val KEY_NOTIFICATION_SIGNATURE = "signature"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.utils
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.interfaces.ClosedInterface
|
||||
import com.nextcloud.talk.jobs.GetFirebasePushTokenWorker
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class ClosedInterfaceImpl :
|
||||
ClosedInterface,
|
||||
ProviderInstaller.ProviderInstallListener {
|
||||
|
||||
override val isGooglePlayServicesAvailable: Boolean = isGPlayServicesAvailable()
|
||||
|
||||
override fun providerInstallerInstallIfNeededAsync() {
|
||||
NextcloudTalkApplication.sharedApplication?.let {
|
||||
ProviderInstaller.installIfNeededAsync(
|
||||
it.applicationContext,
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProviderInstalled() {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onProviderInstallFailed(p0: Int, p1: Intent?) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
private fun isGPlayServicesAvailable(): Boolean {
|
||||
val api = GoogleApiAvailability.getInstance()
|
||||
val code =
|
||||
NextcloudTalkApplication.sharedApplication?.let {
|
||||
api.isGooglePlayServicesAvailable(it.applicationContext)
|
||||
}
|
||||
return if (code == ConnectionResult.SUCCESS) {
|
||||
true
|
||||
} else {
|
||||
Log.w(TAG, "GooglePlayServices are not available. Code:$code")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUpPushTokenRegistration() {
|
||||
val firebasePushTokenWorker = OneTimeWorkRequest.Builder(GetFirebasePushTokenWorker::class.java).build()
|
||||
WorkManager.getInstance().enqueue(firebasePushTokenWorker)
|
||||
|
||||
setUpPeriodicTokenRefreshFromFCM()
|
||||
}
|
||||
|
||||
private fun setUpPeriodicTokenRefreshFromFCM() {
|
||||
val periodicTokenRefreshFromFCM = PeriodicWorkRequest.Builder(
|
||||
GetFirebasePushTokenWorker::class.java,
|
||||
DAILY,
|
||||
TimeUnit.HOURS,
|
||||
FLEX_INTERVAL,
|
||||
TimeUnit.HOURS
|
||||
).build()
|
||||
|
||||
WorkManager.getInstance()
|
||||
.enqueueUniquePeriodicWork(
|
||||
"periodicTokenRefreshFromFCM",
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
periodicTokenRefreshFromFCM
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ClosedInterfaceImpl::class.java.simpleName
|
||||
const val DAILY: Long = 24
|
||||
const val FLEX_INTERVAL: Long = 10
|
||||
}
|
||||
}
|
||||
332
app/src/main/AndroidManifest.xml
Normal file
332
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
<!--
|
||||
~ Nextcloud Talk - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
~ SPDX-FileCopyrightText: 2021-2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
~ SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
android:required="true" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.AUTHENTICATE_ACCOUNTS"
|
||||
android:maxSdkVersion="22" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission
|
||||
android:name="android.permission.GET_ACCOUNTS"
|
||||
android:maxSdkVersion="22" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_PROFILE" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.USE_CREDENTIALS"
|
||||
android:maxSdkVersion="22" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<!-- This permission is deprecated in Android P -->
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.${broadcastPermission}"
|
||||
android:protectionLevel="signature" />
|
||||
<uses-permission android:name="${applicationId}.${broadcastPermission}" />
|
||||
|
||||
<application
|
||||
android:name=".application.NextcloudTalkApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/nc_app_name"
|
||||
android:largeHeap="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme.Launcher"
|
||||
android:taskAffinity=""
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:replace="label, icon, theme, name, allowBackup">
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="10" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".account.ServerSelectionActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".account.WebViewLoginActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity android:name=".contacts.ContactsActivity"
|
||||
android:theme="@style/AppTheme"/>
|
||||
|
||||
<activity android:name=".conversationcreation.ConversationCreationActivity"
|
||||
android:theme="@style/AppTheme"/>
|
||||
|
||||
<activity
|
||||
android:name=".account.AccountVerificationActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".account.SwitchAccountActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".conversationlist.ConversationsListActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".chat.ChatActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.CallActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:showOnLockScreen="true"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity=".call"
|
||||
android:theme="@style/AppTheme.CallLauncher" />
|
||||
|
||||
<activity
|
||||
android:name=".callnotification.CallNotificationActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:showOnLockScreen="true"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity=".call"
|
||||
android:theme="@style/AppTheme.CallLauncher" />
|
||||
|
||||
<activity
|
||||
android:name=".fullscreenfile.FullScreenImageActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/FullScreenImageTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".fullscreenfile.FullScreenMediaActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/FullScreenMediaTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".fullscreenfile.FullScreenTextViewerActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/FullScreenTextTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.TakePhotoActivity"
|
||||
android:theme="@style/TakePhotoTheme"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".shareditems.activities.SharedItemsActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".remotefilebrowser.activities.RemoteFileBrowserActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".messagesearch.MessageSearchActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".location.LocationPickerActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".location.GeocodingActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".translate.ui.TranslateActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".diagnose.DiagnoseActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".conversationinfo.ConversationInfoActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".conversationinfoedit.ConversationInfoEditActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".openconversations.ListOpenConversationsActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".invitation.InvitationsActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".lock.LockedActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".threadsoverview.ThreadsOverviewActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.PackageReplacedReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".receivers.DirectReplyReceiver" />
|
||||
<receiver android:name=".receivers.MarkAsReadReceiver" />
|
||||
<receiver android:name=".receivers.DismissRecordingAvailableReceiver" />
|
||||
<receiver android:name=".receivers.ShareRecordingToChatReceiver" />
|
||||
|
||||
<service
|
||||
android:name=".utils.SyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/syncadapter" />
|
||||
<meta-data
|
||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||
android:resource="@xml/contacts" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".utils.AuthenticatorService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/auth" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider_paths" />
|
||||
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.media.action.VIDEO_CAPTURE" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
42
app/src/main/assets/leafletMapMessagePreview.html
Normal file
42
app/src/main/assets/leafletMapMessagePreview.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv='content-Type' content='text/html; charset=UTF-8' />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
|
||||
<style>
|
||||
html, body, #map {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map" ></div>
|
||||
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
|
||||
<script>
|
||||
var queryString = window.location.search;
|
||||
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
var locationLat = urlParams.get('locationLat')
|
||||
var locationLon = urlParams.get('locationLon')
|
||||
var locationGeoLink = urlParams.get('locationGeoLink')
|
||||
var mapProviderUrl = urlParams.get('mapProviderUrl')
|
||||
var mapProviderAttribution = urlParams.get('mapProviderAttribution')
|
||||
|
||||
var map = L.map('map', {
|
||||
zoomControl: false,
|
||||
scrollWheelZoom: false
|
||||
}).setView([locationLat, locationLon], 13);
|
||||
map.dragging.disable();
|
||||
|
||||
L.tileLayer(mapProviderUrl, {
|
||||
attribution: '© ' + mapProviderAttribution
|
||||
}).addTo(map);
|
||||
|
||||
L.marker([locationLat, locationLon]).addTo(map);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
BIN
app/src/main/ic_launcher-web.png
Normal file
BIN
app/src/main/ic_launcher-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
12
app/src/main/java/com/nextcloud/talk/PhoneUtils.kt
Normal file
12
app/src/main/java/com/nextcloud/talk/PhoneUtils.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk
|
||||
|
||||
object PhoneUtils {
|
||||
fun isPhoneNumber(input: String?): Boolean = input?.matches(Regex("^\\+?\\d+$")) == true
|
||||
}
|
||||
|
|
@ -0,0 +1,521 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.account
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import com.bluelinelabs.logansquare.LoganSquare
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.BaseActivity
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.conversationlist.ConversationsListActivity
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.ActivityAccountVerificationBinding
|
||||
import com.nextcloud.talk.events.EventStatus
|
||||
import com.nextcloud.talk.jobs.AccountRemovalWorker
|
||||
import com.nextcloud.talk.jobs.CapabilitiesWorker
|
||||
import com.nextcloud.talk.jobs.SignalingSettingsWorker
|
||||
import com.nextcloud.talk.jobs.WebsocketConnectionsWorker
|
||||
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
|
||||
import com.nextcloud.talk.models.json.generic.Status
|
||||
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.ClosedInterfaceImpl
|
||||
import com.nextcloud.talk.utils.UriUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PASSWORD
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
|
||||
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
|
||||
import io.reactivex.MaybeObserver
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.net.CookieManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class AccountVerificationActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityAccountVerificationBinding
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
@Inject
|
||||
lateinit var cookieManager: CookieManager
|
||||
|
||||
private var internalAccountId: Long = -1
|
||||
private val disposables: MutableList<Disposable> = ArrayList()
|
||||
private var baseUrl: String? = null
|
||||
private var username: String? = null
|
||||
private var token: String? = null
|
||||
private var isAccountImport = false
|
||||
private var originalProtocol: String? = null
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding = ActivityAccountVerificationBinding.inflate(layoutInflater)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
setContentView(binding.root)
|
||||
actionBar?.hide()
|
||||
initSystemBars()
|
||||
|
||||
handleIntent()
|
||||
}
|
||||
|
||||
private fun handleIntent() {
|
||||
val extras = intent.extras!!
|
||||
baseUrl = extras.getString(KEY_BASE_URL)
|
||||
username = extras.getString(KEY_USERNAME)
|
||||
token = extras.getString(KEY_TOKEN)
|
||||
if (extras.containsKey(KEY_IS_ACCOUNT_IMPORT)) {
|
||||
isAccountImport = true
|
||||
}
|
||||
if (extras.containsKey(KEY_ORIGINAL_PROTOCOL)) {
|
||||
originalProtocol = extras.getString(KEY_ORIGINAL_PROTOCOL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (
|
||||
isAccountImport &&
|
||||
!UriUtils.hasHttpProtocolPrefixed(baseUrl!!) ||
|
||||
isNotSameProtocol(baseUrl!!, originalProtocol)
|
||||
) {
|
||||
determineBaseUrlProtocol(true)
|
||||
} else {
|
||||
findServerTalkApp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNotSameProtocol(baseUrl: String, originalProtocol: String?): Boolean {
|
||||
if (originalProtocol == null) {
|
||||
return true
|
||||
}
|
||||
return !TextUtils.isEmpty(originalProtocol) && !baseUrl.startsWith(originalProtocol)
|
||||
}
|
||||
|
||||
private fun determineBaseUrlProtocol(checkForcedHttps: Boolean) {
|
||||
cookieManager.cookieStore.removeAll()
|
||||
baseUrl = baseUrl!!.replace("http://", "").replace("https://", "")
|
||||
val queryUrl: String = if (checkForcedHttps) {
|
||||
"https://" + baseUrl + ApiUtils.getUrlPostfixForStatus()
|
||||
} else {
|
||||
"http://" + baseUrl + ApiUtils.getUrlPostfixForStatus()
|
||||
}
|
||||
ncApi.getServerStatus(queryUrl)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(object : Observer<Status?> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposables.add(d)
|
||||
}
|
||||
|
||||
override fun onNext(status: Status) {
|
||||
baseUrl = if (checkForcedHttps) {
|
||||
"https://$baseUrl"
|
||||
} else {
|
||||
"http://$baseUrl"
|
||||
}
|
||||
if (isAccountImport) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_BASE_URL, baseUrl)
|
||||
bundle.putString(KEY_USERNAME, username)
|
||||
bundle.putString(KEY_PASSWORD, "")
|
||||
|
||||
val intent = Intent(context, WebViewLoginActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
findServerTalkApp()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
if (checkForcedHttps) {
|
||||
determineBaseUrlProtocol(false)
|
||||
} else {
|
||||
abortVerification()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun findServerTalkApp() {
|
||||
val credentials = ApiUtils.getCredentials(username, token)
|
||||
cookieManager.cookieStore.removeAll()
|
||||
|
||||
ncApi.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl!!))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(object : Observer<CapabilitiesOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposables.add(d)
|
||||
}
|
||||
|
||||
override fun onNext(capabilitiesOverall: CapabilitiesOverall) {
|
||||
val hasTalk =
|
||||
capabilitiesOverall.ocs!!.data!!.capabilities != null &&
|
||||
capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability != null &&
|
||||
capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability!!.features != null &&
|
||||
!capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability!!.features!!.isEmpty()
|
||||
if (hasTalk) {
|
||||
fetchProfile(credentials!!, capabilitiesOverall)
|
||||
} else {
|
||||
if (resources != null) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text = String.format(
|
||||
resources!!.getString(R.string.nc_nextcloud_talk_app_not_installed),
|
||||
resources!!.getString(R.string.nc_app_product_name)
|
||||
)
|
||||
}
|
||||
}
|
||||
ApplicationWideMessageHolder.getInstance().messageType =
|
||||
ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
|
||||
abortVerification()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
if (resources != null) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text = String.format(
|
||||
resources!!.getString(R.string.nc_nextcloud_talk_app_not_installed),
|
||||
resources!!.getString(R.string.nc_app_product_name)
|
||||
)
|
||||
}
|
||||
}
|
||||
ApplicationWideMessageHolder.getInstance().messageType =
|
||||
ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
|
||||
abortVerification()
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun storeProfile(displayName: String?, userId: String, capabilitiesOverall: CapabilitiesOverall) {
|
||||
userManager.storeProfile(
|
||||
username,
|
||||
UserManager.UserAttributes(
|
||||
id = null,
|
||||
serverUrl = baseUrl,
|
||||
currentUser = true,
|
||||
userId = userId,
|
||||
token = token,
|
||||
displayName = displayName,
|
||||
pushConfigurationState = null,
|
||||
capabilities = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.capabilities),
|
||||
serverVersion = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.serverVersion),
|
||||
certificateAlias = appPreferences.temporaryClientCertAlias,
|
||||
externalSignalingServer = null
|
||||
)
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(object : MaybeObserver<User> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposables.add(d)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onSuccess(user: User) {
|
||||
internalAccountId = user.id!!
|
||||
if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) {
|
||||
ClosedInterfaceImpl().setUpPushTokenRegistration()
|
||||
} else {
|
||||
Log.w(TAG, "Skipping push registration.")
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
""" ${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_push_disabled)}
|
||||
""".trimIndent()
|
||||
}
|
||||
fetchAndStoreCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onError(e: Throwable) {
|
||||
binding.progressText.text = """ ${binding.progressText.text}""".trimIndent() +
|
||||
resources!!.getString(R.string.nc_display_name_not_stored)
|
||||
abortVerification()
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun fetchProfile(credentials: String, capabilitiesOverall: CapabilitiesOverall) {
|
||||
ncApi.getUserProfile(
|
||||
credentials,
|
||||
ApiUtils.getUrlForUserProfile(baseUrl!!)
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(object : Observer<UserProfileOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposables.add(d)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onNext(userProfileOverall: UserProfileOverall) {
|
||||
var displayName: String? = null
|
||||
if (!TextUtils.isEmpty(userProfileOverall.ocs!!.data!!.displayName)) {
|
||||
displayName = userProfileOverall.ocs!!.data!!.displayName
|
||||
} else if (!TextUtils.isEmpty(userProfileOverall.ocs!!.data!!.displayNameAlt)) {
|
||||
displayName = userProfileOverall.ocs!!.data!!.displayNameAlt
|
||||
}
|
||||
if (!TextUtils.isEmpty(displayName)) {
|
||||
storeProfile(
|
||||
displayName,
|
||||
userProfileOverall.ocs!!.data!!.userId!!,
|
||||
capabilitiesOverall
|
||||
)
|
||||
} else {
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
"""
|
||||
${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_display_name_not_fetched)}
|
||||
""".trimIndent()
|
||||
}
|
||||
abortVerification()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onError(e: Throwable) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
"""
|
||||
${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_display_name_not_fetched)}
|
||||
""".trimIndent()
|
||||
}
|
||||
abortVerification()
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Subscribe(threadMode = ThreadMode.BACKGROUND)
|
||||
fun onMessageEvent(eventStatus: EventStatus) {
|
||||
Log.d(TAG, "caught EventStatus of type " + eventStatus.eventType.toString())
|
||||
if (eventStatus.eventType == EventStatus.EventType.PUSH_REGISTRATION) {
|
||||
if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
"""
|
||||
${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_push_disabled)}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
fetchAndStoreCapabilities()
|
||||
} else if (eventStatus.eventType == EventStatus.EventType.CAPABILITIES_FETCH) {
|
||||
if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
"""
|
||||
${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_capabilities_failed)}
|
||||
""".trimIndent()
|
||||
}
|
||||
abortVerification()
|
||||
} else if (internalAccountId == eventStatus.userId && eventStatus.isAllGood) {
|
||||
fetchAndStoreExternalSignalingSettings()
|
||||
}
|
||||
} else if (eventStatus.eventType == EventStatus.EventType.SIGNALING_SETTINGS) {
|
||||
if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
"""
|
||||
${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_external_server_failed)}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
proceedWithLogin()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchAndStoreCapabilities() {
|
||||
val userData =
|
||||
Data.Builder()
|
||||
.putLong(KEY_INTERNAL_USER_ID, internalAccountId)
|
||||
.build()
|
||||
val capabilitiesWork =
|
||||
OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java)
|
||||
.setInputData(userData)
|
||||
.build()
|
||||
WorkManager.getInstance().enqueue(capabilitiesWork)
|
||||
}
|
||||
|
||||
private fun fetchAndStoreExternalSignalingSettings() {
|
||||
val userData =
|
||||
Data.Builder()
|
||||
.putLong(KEY_INTERNAL_USER_ID, internalAccountId)
|
||||
.build()
|
||||
val signalingSettingsWorker = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java)
|
||||
.setInputData(userData)
|
||||
.build()
|
||||
val websocketConnectionsWorker = OneTimeWorkRequest.Builder(WebsocketConnectionsWorker::class.java).build()
|
||||
|
||||
WorkManager.getInstance(applicationContext!!)
|
||||
.beginWith(signalingSettingsWorker)
|
||||
.then(websocketConnectionsWorker)
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
private fun proceedWithLogin() {
|
||||
cookieManager.cookieStore.removeAll()
|
||||
|
||||
if (userManager.users.blockingGet().size == 1 ||
|
||||
currentUserProvider.currentUser.blockingGet().id != internalAccountId
|
||||
) {
|
||||
val userToSetAsActive = userManager.getUserWithId(internalAccountId).blockingGet()
|
||||
Log.d(TAG, "userToSetAsActive: " + userToSetAsActive.username)
|
||||
|
||||
if (userManager.setUserAsActive(userToSetAsActive).blockingGet()) {
|
||||
runOnUiThread {
|
||||
if (userManager.users.blockingGet().size == 1) {
|
||||
val intent = Intent(context, ConversationsListActivity::class.java)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
if (isAccountImport) {
|
||||
ApplicationWideMessageHolder.getInstance().messageType =
|
||||
ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED
|
||||
}
|
||||
val intent = Intent(context, ConversationsListActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "failed to set active user")
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "continuing proceedWithLogin was skipped for this user")
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispose() {
|
||||
for (i in disposables.indices) {
|
||||
if (!disposables[i].isDisposed) {
|
||||
disposables[i].dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun abortVerification() {
|
||||
if (isAccountImport) {
|
||||
ApplicationWideMessageHolder.getInstance().messageType = ApplicationWideMessageHolder.MessageType
|
||||
.FAILED_TO_IMPORT_ACCOUNT
|
||||
runOnUiThread {
|
||||
Handler().postDelayed({
|
||||
val intent = Intent(this, ServerSelectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}, DELAY_IN_MILLIS)
|
||||
}
|
||||
} else {
|
||||
if (internalAccountId != -1L) {
|
||||
runOnUiThread {
|
||||
deleteUserAndStartServerSelection(internalAccountId)
|
||||
}
|
||||
} else {
|
||||
runOnUiThread {
|
||||
Handler().postDelayed({
|
||||
val intent = Intent(this, ServerSelectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}, DELAY_IN_MILLIS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private fun deleteUserAndStartServerSelection(userId: Long) {
|
||||
userManager.scheduleUserForDeletionWithId(userId).blockingGet()
|
||||
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
|
||||
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
|
||||
|
||||
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
|
||||
.observeForever { workInfo: WorkInfo? ->
|
||||
|
||||
when (workInfo?.state) {
|
||||
WorkInfo.State.SUCCEEDED -> {
|
||||
val intent = Intent(this, ServerSelectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.resources.getString(R.string.nc_common_error_sorry),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
Log.e(TAG, "something went wrong when deleting user with id $userId")
|
||||
val intent = Intent(this, ServerSelectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = AccountVerificationActivity::class.java.simpleName
|
||||
const val DELAY_IN_MILLIS: Long = 7500
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,468 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.account
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.security.KeyChain
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import autodagger.AutoInjector
|
||||
import com.blikoon.qrcodescanner.QrCodeActivity
|
||||
import com.github.dhaval2404.imagepicker.util.PermissionUtil
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.BaseActivity
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.databinding.ActivityServerSelectionBinding
|
||||
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
|
||||
import com.nextcloud.talk.models.json.generic.Status
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.AccountUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.CapabilitiesUtil
|
||||
import com.nextcloud.talk.utils.UriUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ADDITIONAL_ACCOUNT
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
|
||||
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.security.cert.CertificateException
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class ServerSelectionActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityServerSelectionBinding
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
private var statusQueryDisposable: Disposable? = null
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (intent.hasExtra(ADD_ADDITIONAL_ACCOUNT) && intent.getBooleanExtra(ADD_ADDITIONAL_ACCOUNT, false)) {
|
||||
finish()
|
||||
} else {
|
||||
finishAffinity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding = ActivityServerSelectionBinding.inflate(layoutInflater)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
setContentView(binding.root)
|
||||
actionBar?.hide()
|
||||
initSystemBars()
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
binding.hostUrlInputHelperText.text = String.format(
|
||||
resources!!.getString(R.string.nc_server_helper_text),
|
||||
resources!!.getString(R.string.nc_server_product_name)
|
||||
)
|
||||
binding.serverEntryTextInputLayout.setEndIconOnClickListener { checkServerAndProceed() }
|
||||
|
||||
if (resources!!.getBoolean(R.bool.hide_auth_cert)) {
|
||||
binding.certTextView.visibility = View.GONE
|
||||
}
|
||||
|
||||
val loggedInUsers = userManager.users.blockingGet()
|
||||
val availableAccounts = AccountUtils.findAvailableAccountsOnDevice(loggedInUsers)
|
||||
|
||||
if (isImportAccountNameSet() && availableAccounts.isNotEmpty()) {
|
||||
showImportAccountsInfo(availableAccounts)
|
||||
} else if (isAbleToShowProviderLink() && loggedInUsers.isEmpty()) {
|
||||
showVisitProvidersInfo()
|
||||
} else {
|
||||
binding.importOrChooseProviderText.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
binding.serverEntryTextInputEditText.requestFocus()
|
||||
if (!TextUtils.isEmpty(resources!!.getString(R.string.weblogin_url))) {
|
||||
binding.serverEntryTextInputEditText.setText(resources!!.getString(R.string.weblogin_url))
|
||||
checkServerAndProceed()
|
||||
}
|
||||
binding.serverEntryTextInputEditText.setOnEditorActionListener { _: TextView?, i: Int, _: KeyEvent? ->
|
||||
if (i == EditorInfo.IME_ACTION_DONE) {
|
||||
checkServerAndProceed()
|
||||
}
|
||||
false
|
||||
}
|
||||
binding.certTextView.setOnClickListener { onCertClick() }
|
||||
|
||||
binding.scanQr.setOnClickListener { onScan() }
|
||||
|
||||
if (ApplicationWideMessageHolder.getInstance().messageType != null) {
|
||||
if (ApplicationWideMessageHolder.getInstance().messageType
|
||||
== ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
|
||||
) {
|
||||
setErrorText(resources!!.getString(R.string.nc_settings_no_talk_installed))
|
||||
} else if (ApplicationWideMessageHolder.getInstance().messageType
|
||||
== ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT
|
||||
) {
|
||||
setErrorText(resources!!.getString(R.string.nc_server_failed_to_import_account))
|
||||
}
|
||||
ApplicationWideMessageHolder.getInstance().messageType = null
|
||||
}
|
||||
setCertTextView()
|
||||
}
|
||||
|
||||
fun onCertClick() {
|
||||
KeyChain.choosePrivateKeyAlias(
|
||||
this,
|
||||
{ alias: String? ->
|
||||
if (alias != null) {
|
||||
appPreferences.temporaryClientCertAlias = alias
|
||||
} else {
|
||||
appPreferences.removeTemporaryClientCertAlias()
|
||||
}
|
||||
setCertTextView()
|
||||
},
|
||||
arrayOf("RSA", "EC"),
|
||||
null,
|
||||
null,
|
||||
-1,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun isAbleToShowProviderLink(): Boolean =
|
||||
!resources!!.getBoolean(R.bool.hide_provider) &&
|
||||
!TextUtils.isEmpty(resources!!.getString(R.string.nc_providers_url))
|
||||
|
||||
private fun showImportAccountsInfo(availableAccounts: List<Account>) {
|
||||
if (!TextUtils.isEmpty(
|
||||
AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
|
||||
)
|
||||
) {
|
||||
if (availableAccounts.size > 1) {
|
||||
binding.importOrChooseProviderText.text = String.format(
|
||||
resources!!.getString(R.string.nc_server_import_accounts),
|
||||
AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
|
||||
)
|
||||
} else {
|
||||
binding.importOrChooseProviderText.text = String.format(
|
||||
resources!!.getString(R.string.nc_server_import_account),
|
||||
AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (availableAccounts.size > 1) {
|
||||
binding.importOrChooseProviderText.text =
|
||||
resources!!.getString(R.string.nc_server_import_accounts_plain)
|
||||
} else {
|
||||
binding.importOrChooseProviderText.text =
|
||||
resources!!.getString(R.string.nc_server_import_account_plain)
|
||||
}
|
||||
}
|
||||
binding.importOrChooseProviderText.setOnClickListener {
|
||||
val bundle = Bundle()
|
||||
bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true)
|
||||
val intent = Intent(context, SwitchAccountActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showVisitProvidersInfo() {
|
||||
binding.importOrChooseProviderText.setText(R.string.nc_get_from_provider)
|
||||
binding.importOrChooseProviderText.setOnClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
resources!!.getString(R.string.nc_providers_url).toUri()
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isImportAccountNameSet(): Boolean =
|
||||
!TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type))
|
||||
|
||||
@SuppressLint("LongLogTag")
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
private fun checkServerAndProceed() {
|
||||
dispose()
|
||||
var url: String = binding.serverEntryTextInputEditText.text.toString().trim()
|
||||
showserverEntryProgressBar()
|
||||
if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) {
|
||||
binding.importOrChooseProviderText.visibility = View.INVISIBLE
|
||||
binding.certTextView.visibility = View.INVISIBLE
|
||||
}
|
||||
if (url.endsWith("/")) {
|
||||
url = url.substring(0, url.length - 1)
|
||||
}
|
||||
|
||||
if (UriUtils.hasHttpProtocolPrefixed(url)) {
|
||||
checkServer(url, false)
|
||||
} else {
|
||||
checkServer("https://$url", true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkServer(url: String, checkForcedHttps: Boolean) {
|
||||
val queryStatusUrl = url + ApiUtils.getUrlPostfixForStatus()
|
||||
|
||||
statusQueryDisposable = ncApi.getServerStatus(queryStatusUrl)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ status: Status ->
|
||||
val versionString: String = status.version!!.substring(0, status.version!!.indexOf("."))
|
||||
val version: Int = versionString.toInt()
|
||||
|
||||
if (isServerStatusQueryable(status) && version >= MIN_SERVER_MAJOR_VERSION) {
|
||||
findServerTalkApp(url)
|
||||
} else {
|
||||
showErrorTextForStatus(status)
|
||||
}
|
||||
}, { throwable: Throwable ->
|
||||
if (checkForcedHttps) {
|
||||
checkServer(queryStatusUrl.replace("https://", "http://"), false)
|
||||
} else {
|
||||
if (throwable.localizedMessage != null) {
|
||||
setErrorText(throwable.localizedMessage)
|
||||
} else if (throwable.cause is CertificateException) {
|
||||
setErrorText(resources!!.getString(R.string.nc_certificate_error))
|
||||
} else {
|
||||
hideserverEntryProgressBar()
|
||||
}
|
||||
|
||||
if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) {
|
||||
binding.importOrChooseProviderText.visibility = View.VISIBLE
|
||||
binding.certTextView.visibility = View.VISIBLE
|
||||
}
|
||||
dispose()
|
||||
}
|
||||
}) {
|
||||
hideserverEntryProgressBar()
|
||||
if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) {
|
||||
binding.importOrChooseProviderText.visibility = View.VISIBLE
|
||||
binding.certTextView.visibility = View.VISIBLE
|
||||
}
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorTextForStatus(status: Status) {
|
||||
if (!status.installed) {
|
||||
setErrorText(
|
||||
String.format(
|
||||
resources!!.getString(R.string.nc_server_not_installed),
|
||||
resources!!.getString(R.string.nc_server_product_name)
|
||||
)
|
||||
)
|
||||
} else if (status.needsUpgrade) {
|
||||
setErrorText(
|
||||
String.format(
|
||||
resources!!.getString(R.string.nc_server_db_upgrade_needed),
|
||||
resources!!.getString(R.string.nc_server_product_name)
|
||||
)
|
||||
)
|
||||
} else if (status.maintenance) {
|
||||
setErrorText(
|
||||
String.format(
|
||||
resources!!.getString(R.string.nc_server_maintenance),
|
||||
resources!!.getString(R.string.nc_server_product_name)
|
||||
)
|
||||
)
|
||||
} else if (!status.version!!.startsWith("13.")) {
|
||||
setErrorText(
|
||||
String.format(
|
||||
resources!!.getString(R.string.nc_server_version),
|
||||
resources!!.getString(R.string.nc_app_product_name),
|
||||
resources!!.getString(R.string.nc_server_product_name)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findServerTalkApp(queryUrl: String) {
|
||||
ncApi.getCapabilities(ApiUtils.getUrlForCapabilities(queryUrl))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(object : Observer<CapabilitiesOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onNext(capabilitiesOverall: CapabilitiesOverall) {
|
||||
val capabilities = capabilitiesOverall.ocs?.data?.capabilities
|
||||
|
||||
val hasTalk =
|
||||
capabilities?.spreedCapability != null &&
|
||||
capabilities.spreedCapability?.features != null &&
|
||||
capabilities.spreedCapability?.features?.isNotEmpty() == true
|
||||
|
||||
if (hasTalk) {
|
||||
runOnUiThread {
|
||||
if (CapabilitiesUtil.isServerEOL(capabilitiesOverall.ocs?.data?.serverVersion?.major)) {
|
||||
if (resources != null) {
|
||||
runOnUiThread {
|
||||
setErrorText(resources!!.getString(R.string.nc_settings_server_eol))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(BundleKeys.KEY_BASE_URL, queryUrl.replace("/status.php", ""))
|
||||
|
||||
val intent = Intent(context, WebViewLoginActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (resources != null) {
|
||||
runOnUiThread {
|
||||
setErrorText(resources!!.getString(R.string.nc_server_unsupported))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "Error while checking capabilities", e)
|
||||
if (resources != null) {
|
||||
runOnUiThread {
|
||||
setErrorText(resources!!.getString(R.string.nc_common_error_sorry))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun isServerStatusQueryable(status: Status): Boolean =
|
||||
status.installed && !status.maintenance && !status.needsUpgrade
|
||||
|
||||
private fun setErrorText(text: String?) {
|
||||
binding.errorWrapper.visibility = View.VISIBLE
|
||||
binding.errorText.text = text
|
||||
hideserverEntryProgressBar()
|
||||
}
|
||||
|
||||
private fun showserverEntryProgressBar() {
|
||||
binding.errorWrapper.visibility = View.INVISIBLE
|
||||
binding.serverEntryProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun hideserverEntryProgressBar() {
|
||||
binding.serverEntryProgressBar.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
@SuppressLint("LongLogTag")
|
||||
private fun setCertTextView() {
|
||||
runOnUiThread {
|
||||
if (!TextUtils.isEmpty(appPreferences.temporaryClientCertAlias)) {
|
||||
binding.certTextView.setText(R.string.nc_change_cert_auth)
|
||||
} else {
|
||||
binding.certTextView.setText(R.string.nc_configure_cert_auth)
|
||||
}
|
||||
hideserverEntryProgressBar()
|
||||
}
|
||||
}
|
||||
|
||||
private val requestCameraPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
if (isGranted) {
|
||||
// Permission was granted
|
||||
startQRScanner()
|
||||
}
|
||||
}
|
||||
|
||||
fun onScan() {
|
||||
if (PermissionUtil.isPermissionGranted(this, Manifest.permission.CAMERA)) {
|
||||
startQRScanner()
|
||||
} else {
|
||||
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startQRScanner() {
|
||||
val intent = Intent(this, QrCodeActivity::class.java)
|
||||
qrScanResultLauncher.launch(intent)
|
||||
}
|
||||
|
||||
private val qrScanResultLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val data = result.data
|
||||
|
||||
if (data == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val resultData = data.getStringExtra(QR_URI)
|
||||
|
||||
if (resultData == null || !resultData.startsWith("nc")) {
|
||||
Snackbar.make(binding.root, getString(R.string.qr_code_error), Snackbar.LENGTH_SHORT).show()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val intent = Intent(this, WebViewLoginActivity::class.java)
|
||||
val bundle = bundleOf().apply {
|
||||
putString(BundleKeys.KEY_FROM_QR, resultData)
|
||||
}
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
dispose()
|
||||
}
|
||||
|
||||
private fun dispose() {
|
||||
if (statusQueryDisposable != null && !statusQueryDisposable!!.isDisposed) {
|
||||
statusQueryDisposable!!.dispose()
|
||||
}
|
||||
statusQueryDisposable = null
|
||||
}
|
||||
|
||||
override val appBarLayoutType: AppBarLayoutType
|
||||
get() = AppBarLayoutType.EMPTY
|
||||
|
||||
companion object {
|
||||
private val TAG = ServerSelectionActivity::class.java.simpleName
|
||||
const val MIN_SERVER_MAJOR_VERSION = 13
|
||||
private const val QR_URI = "com.blikoon.qrcodescanner.got_qr_scan_relult"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import autodagger.AutoInjector
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.BaseActivity
|
||||
import com.nextcloud.talk.adapters.items.AdvancedUserItem
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.ActivitySwitchAccountBinding
|
||||
import com.nextcloud.talk.models.ImportAccount
|
||||
import com.nextcloud.talk.models.json.participants.Participant
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.AccountUtils.findAvailableAccountsOnDevice
|
||||
import com.nextcloud.talk.utils.AccountUtils.getInformationFromAccount
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import org.osmdroid.config.Configuration
|
||||
import java.net.CookieManager
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Parts related to account import were either copied from or inspired by the great work done by David Luhmer at:
|
||||
* https://github.com/nextcloud/ownCloud-Account-Importer
|
||||
*/
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class SwitchAccountActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivitySwitchAccountBinding
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
@Inject
|
||||
lateinit var cookieManager: CookieManager
|
||||
|
||||
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
|
||||
private val userItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
|
||||
private var isAccountImport = false
|
||||
|
||||
private val onImportItemClickListener = FlexibleAdapter.OnItemClickListener { _, position ->
|
||||
if (userItems.size > position) {
|
||||
val account = (userItems[position] as AdvancedUserItem).account
|
||||
reauthorizeFromImport(account)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
private val onSwitchItemClickListener = FlexibleAdapter.OnItemClickListener { _, position ->
|
||||
if (userItems.size > position) {
|
||||
val user = (userItems[position] as AdvancedUserItem).user
|
||||
|
||||
if (userManager.setUserAsActive(user!!).blockingGet()) {
|
||||
cookieManager.cookieStore.removeAll()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding = ActivitySwitchAccountBinding.inflate(layoutInflater)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
setContentView(binding.root)
|
||||
setupActionBar()
|
||||
initSystemBars()
|
||||
|
||||
Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))
|
||||
|
||||
handleIntent()
|
||||
}
|
||||
|
||||
private fun handleIntent() {
|
||||
intent.extras?.let {
|
||||
if (it.containsKey(KEY_IS_ACCOUNT_IMPORT)) {
|
||||
isAccountImport = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupActionBar() {
|
||||
setSupportActionBar(binding.toolbar)
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
supportActionBar?.setIcon(resources!!.getColor(R.color.transparent, null).toDrawable())
|
||||
supportActionBar?.title = resources!!.getString(R.string.nc_select_an_account)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.NestedBlockDepth")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (adapter == null) {
|
||||
adapter = FlexibleAdapter(userItems, this, false)
|
||||
var participant: Participant
|
||||
|
||||
if (!isAccountImport) {
|
||||
for (user in userManager.users.blockingGet()) {
|
||||
if (!user.current) {
|
||||
val userId: String? = if (user.userId != null) {
|
||||
user.userId
|
||||
} else {
|
||||
user.username
|
||||
}
|
||||
participant = Participant()
|
||||
participant.actorType = Participant.ActorType.USERS
|
||||
participant.actorId = userId
|
||||
participant.displayName = user.displayName
|
||||
userItems.add(AdvancedUserItem(participant, user, null, viewThemeUtils, 0))
|
||||
}
|
||||
}
|
||||
adapter!!.addListener(onSwitchItemClickListener)
|
||||
adapter!!.updateDataSet(userItems, false)
|
||||
} else {
|
||||
var account: Account
|
||||
var importAccount: ImportAccount
|
||||
var user: User
|
||||
for (accountObject in findAvailableAccountsOnDevice(userManager.users.blockingGet())) {
|
||||
account = accountObject
|
||||
importAccount = getInformationFromAccount(account)
|
||||
participant = Participant()
|
||||
participant.actorType = Participant.ActorType.USERS
|
||||
participant.actorId = importAccount.getUsername()
|
||||
participant.displayName = importAccount.getUsername()
|
||||
user = User()
|
||||
user.baseUrl = importAccount.getBaseUrl()
|
||||
userItems.add(AdvancedUserItem(participant, user, account, viewThemeUtils, 0))
|
||||
}
|
||||
adapter!!.addListener(onImportItemClickListener)
|
||||
adapter!!.updateDataSet(userItems, false)
|
||||
}
|
||||
}
|
||||
prepareViews()
|
||||
}
|
||||
|
||||
private fun prepareViews() {
|
||||
val layoutManager: LinearLayoutManager = SmoothScrollLinearLayoutManager(this)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun reauthorizeFromImport(account: Account?) {
|
||||
val importAccount = getInformationFromAccount(account!!)
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_BASE_URL, importAccount.getBaseUrl())
|
||||
bundle.putString(KEY_USERNAME, importAccount.getUsername())
|
||||
bundle.putString(KEY_TOKEN, importAccount.getToken())
|
||||
bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true)
|
||||
|
||||
val intent = Intent(context, AccountVerificationActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,471 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.account
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.net.http.SslError
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.security.KeyChain
|
||||
import android.security.KeyChainException
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.webkit.ClientCertRequest
|
||||
import android.webkit.CookieSyncManager
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.BaseActivity
|
||||
import com.nextcloud.talk.activities.MainActivity
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.databinding.ActivityWebViewLoginBinding
|
||||
import com.nextcloud.talk.events.CertificateEvent
|
||||
import com.nextcloud.talk.jobs.AccountRemovalWorker
|
||||
import com.nextcloud.talk.models.LoginData
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
|
||||
import com.nextcloud.talk.utils.ssl.TrustManager
|
||||
import de.cotech.hw.fido.WebViewFidoBridge
|
||||
import de.cotech.hw.fido2.WebViewWebauthnBridge
|
||||
import de.cotech.hw.fido2.ui.WebauthnDialogOptions
|
||||
import io.reactivex.disposables.Disposable
|
||||
import java.lang.reflect.Field
|
||||
import java.net.CookieManager
|
||||
import java.net.URLDecoder
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("ReturnCount", "LongMethod")
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class WebViewLoginActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityWebViewLoginBinding
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
@Inject
|
||||
lateinit var trustManager: TrustManager
|
||||
|
||||
@Inject
|
||||
lateinit var cookieManager: CookieManager
|
||||
|
||||
private var assembledPrefix: String? = null
|
||||
private var userQueryDisposable: Disposable? = null
|
||||
private var baseUrl: String? = null
|
||||
private var reauthorizeAccount = false
|
||||
private var username: String? = null
|
||||
private var password: String? = null
|
||||
private var loginStep = 0
|
||||
private var automatedLoginAttempted = false
|
||||
private var webViewFidoBridge: WebViewFidoBridge? = null
|
||||
private var webViewWebauthnBridge: WebViewWebauthnBridge? = null
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
private val webLoginUserAgent: String
|
||||
get() = (
|
||||
Build.MANUFACTURER.substring(0, 1).uppercase(Locale.getDefault()) +
|
||||
Build.MANUFACTURER.substring(1).uppercase(Locale.getDefault()) +
|
||||
" " +
|
||||
Build.MODEL +
|
||||
" (" +
|
||||
resources!!.getString(R.string.nc_app_product_name) +
|
||||
")"
|
||||
)
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding = ActivityWebViewLoginBinding.inflate(layoutInflater)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
setContentView(binding.root)
|
||||
actionBar?.hide()
|
||||
initSystemBars()
|
||||
assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
handleIntent()
|
||||
}
|
||||
|
||||
private fun handleIntent() {
|
||||
val extras = intent.extras!!
|
||||
baseUrl = extras.getString(KEY_BASE_URL)
|
||||
username = extras.getString(KEY_USERNAME)
|
||||
|
||||
if (extras.containsKey(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)) {
|
||||
reauthorizeAccount = extras.getBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)
|
||||
}
|
||||
|
||||
if (extras.containsKey(BundleKeys.KEY_PASSWORD)) {
|
||||
password = extras.getString(BundleKeys.KEY_PASSWORD)
|
||||
}
|
||||
|
||||
if (extras.containsKey(BundleKeys.KEY_FROM_QR)) {
|
||||
extras.getString(BundleKeys.KEY_FROM_QR)?.let {
|
||||
parseAndLoginFromWebView(it)
|
||||
}
|
||||
} else {
|
||||
setupWebView()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun setupWebView() {
|
||||
binding.webview.settings.allowFileAccess = false
|
||||
binding.webview.settings.allowFileAccessFromFileURLs = false
|
||||
binding.webview.settings.javaScriptEnabled = true
|
||||
binding.webview.settings.javaScriptCanOpenWindowsAutomatically = false
|
||||
binding.webview.settings.domStorageEnabled = true
|
||||
binding.webview.settings.userAgentString = webLoginUserAgent
|
||||
binding.webview.settings.saveFormData = false
|
||||
binding.webview.settings.savePassword = false
|
||||
binding.webview.settings.setRenderPriority(WebSettings.RenderPriority.HIGH)
|
||||
binding.webview.clearCache(true)
|
||||
binding.webview.clearFormData()
|
||||
binding.webview.clearHistory()
|
||||
WebView.clearClientCertPreferences(null)
|
||||
webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(this, binding.webview)
|
||||
|
||||
val webauthnOptionsBuilder = WebauthnDialogOptions.builder().setShowSdkLogo(true).setAllowSkipPin(true)
|
||||
webViewWebauthnBridge = WebViewWebauthnBridge.createInstanceForWebView(
|
||||
this,
|
||||
binding.webview,
|
||||
webauthnOptionsBuilder
|
||||
)
|
||||
|
||||
CookieSyncManager.createInstance(this)
|
||||
android.webkit.CookieManager.getInstance().removeAllCookies(null)
|
||||
val headers: MutableMap<String, String> = HashMap()
|
||||
headers["OCS-APIRequest"] = "true"
|
||||
binding.webview.webViewClient = object : WebViewClient() {
|
||||
private var basePageLoaded = false
|
||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
||||
webViewFidoBridge?.delegateShouldInterceptRequest(view, request)
|
||||
webViewWebauthnBridge?.delegateShouldInterceptRequest(view, request)
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
webViewFidoBridge?.delegateOnPageStarted(view, url, favicon)
|
||||
webViewWebauthnBridge?.delegateOnPageStarted(view, url, favicon)
|
||||
}
|
||||
|
||||
@Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)")
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
if (url.startsWith(assembledPrefix!!)) {
|
||||
parseAndLoginFromWebView(url)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
loginStep++
|
||||
if (!basePageLoaded) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.webview.visibility = View.VISIBLE
|
||||
|
||||
basePageLoaded = true
|
||||
}
|
||||
if (!TextUtils.isEmpty(username)) {
|
||||
if (loginStep == 1) {
|
||||
binding.webview.loadUrl(
|
||||
"javascript: {document.getElementsByClassName('login')[0].click(); };"
|
||||
)
|
||||
} else if (!automatedLoginAttempted) {
|
||||
automatedLoginAttempted = true
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
binding.webview.loadUrl(
|
||||
"javascript:var justStore = document.getElementById('user').value = '$username';"
|
||||
)
|
||||
} else {
|
||||
binding.webview.loadUrl(
|
||||
"javascript: {" +
|
||||
"document.getElementById('user').value = '" + username + "';" +
|
||||
"document.getElementById('password').value = '" + password + "';" +
|
||||
"document.getElementById('submit').click(); };"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
|
||||
override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) {
|
||||
var alias: String? = null
|
||||
if (!reauthorizeAccount) {
|
||||
alias = appPreferences.temporaryClientCertAlias
|
||||
}
|
||||
val user = currentUserProvider.currentUser.blockingGet()
|
||||
if (TextUtils.isEmpty(alias) && user != null) {
|
||||
alias = user.clientCertificate
|
||||
}
|
||||
if (!TextUtils.isEmpty(alias)) {
|
||||
val finalAlias = alias
|
||||
Thread {
|
||||
try {
|
||||
val privateKey = KeyChain.getPrivateKey(applicationContext, finalAlias!!)
|
||||
val certificates = KeyChain.getCertificateChain(
|
||||
applicationContext,
|
||||
finalAlias
|
||||
)
|
||||
if (privateKey != null && certificates != null) {
|
||||
request.proceed(privateKey, certificates)
|
||||
} else {
|
||||
request.cancel()
|
||||
}
|
||||
} catch (e: KeyChainException) {
|
||||
request.cancel()
|
||||
} catch (e: InterruptedException) {
|
||||
request.cancel()
|
||||
}
|
||||
}.start()
|
||||
} else {
|
||||
KeyChain.choosePrivateKeyAlias(
|
||||
this@WebViewLoginActivity,
|
||||
{ chosenAlias: String? ->
|
||||
if (chosenAlias != null) {
|
||||
appPreferences!!.temporaryClientCertAlias = chosenAlias
|
||||
Thread {
|
||||
var privateKey: PrivateKey? = null
|
||||
try {
|
||||
privateKey = KeyChain.getPrivateKey(applicationContext, chosenAlias)
|
||||
val certificates = KeyChain.getCertificateChain(
|
||||
applicationContext,
|
||||
chosenAlias
|
||||
)
|
||||
if (privateKey != null && certificates != null) {
|
||||
request.proceed(privateKey, certificates)
|
||||
} else {
|
||||
request.cancel()
|
||||
}
|
||||
} catch (e: KeyChainException) {
|
||||
request.cancel()
|
||||
} catch (e: InterruptedException) {
|
||||
request.cancel()
|
||||
}
|
||||
}.start()
|
||||
} else {
|
||||
request.cancel()
|
||||
}
|
||||
},
|
||||
arrayOf("RSA", "EC"),
|
||||
null,
|
||||
request.host,
|
||||
request.port,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "WebViewClientOnReceivedSslError")
|
||||
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
|
||||
try {
|
||||
val sslCertificate = error.certificate
|
||||
val f: Field = sslCertificate.javaClass.getDeclaredField("mX509Certificate")
|
||||
f.isAccessible = true
|
||||
val cert = f[sslCertificate] as X509Certificate
|
||||
try {
|
||||
trustManager.checkServerTrusted(arrayOf(cert), "generic")
|
||||
handler.proceed()
|
||||
} catch (exception: CertificateException) {
|
||||
eventBus.post(CertificateEvent(cert, trustManager, handler))
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
handler.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in super implementation")
|
||||
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
|
||||
super.onReceivedError(view, errorCode, description, failingUrl)
|
||||
}
|
||||
}
|
||||
binding.webview.loadUrl("$baseUrl/index.php/login/flow", headers)
|
||||
}
|
||||
|
||||
private fun dispose() {
|
||||
if (userQueryDisposable != null && !userQueryDisposable!!.isDisposed) {
|
||||
userQueryDisposable!!.dispose()
|
||||
}
|
||||
userQueryDisposable = null
|
||||
}
|
||||
|
||||
private fun parseAndLoginFromWebView(dataString: String) {
|
||||
val loginData = parseLoginData(assembledPrefix, dataString)
|
||||
if (loginData != null) {
|
||||
dispose()
|
||||
cookieManager.cookieStore.removeAll()
|
||||
|
||||
if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, loginData.serverUrl!!)
|
||||
.blockingGet()
|
||||
) {
|
||||
Log.e(TAG, "Tried to add already existing user who is scheduled for deletion.")
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
// however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it.
|
||||
startAccountRemovalWorkerAndRestartApp()
|
||||
} else if (userManager.checkIfUserExists(loginData.username!!, loginData.serverUrl!!)
|
||||
.blockingGet()
|
||||
) {
|
||||
if (reauthorizeAccount) {
|
||||
updateUserAndRestartApp(loginData)
|
||||
} else {
|
||||
Log.w(TAG, "It was tried to add an account that account already exists. Skipped user creation.")
|
||||
restartApp()
|
||||
}
|
||||
} else {
|
||||
startAccountVerification(loginData)
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Login Data was null")
|
||||
restartApp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAccountVerification(loginData: LoginData) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_USERNAME, loginData.username)
|
||||
bundle.putString(KEY_TOKEN, loginData.token)
|
||||
bundle.putString(KEY_BASE_URL, loginData.serverUrl)
|
||||
var protocol = ""
|
||||
if (loginData.serverUrl!!.startsWith("http://")) {
|
||||
protocol = "http://"
|
||||
} else if (loginData.serverUrl!!.startsWith("https://")) {
|
||||
protocol = "https://"
|
||||
}
|
||||
if (!TextUtils.isEmpty(protocol)) {
|
||||
bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
|
||||
}
|
||||
val intent = Intent(context, AccountVerificationActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun restartApp() {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun updateUserAndRestartApp(loginData: LoginData) {
|
||||
val currentUser = currentUserProvider.currentUser.blockingGet()
|
||||
if (currentUser != null) {
|
||||
currentUser.clientCertificate = appPreferences.temporaryClientCertAlias
|
||||
currentUser.token = loginData.token
|
||||
val rowsUpdated = userManager.updateOrCreateUser(currentUser).blockingGet()
|
||||
Log.d(TAG, "User rows updated: $rowsUpdated")
|
||||
restartApp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAccountRemovalWorkerAndRestartApp() {
|
||||
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
|
||||
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
|
||||
|
||||
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
|
||||
.observeForever { workInfo: WorkInfo? ->
|
||||
|
||||
when (workInfo?.state) {
|
||||
WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
|
||||
restartApp()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLoginData(prefix: String?, dataString: String): LoginData? {
|
||||
if (dataString.length < prefix!!.length) {
|
||||
return null
|
||||
}
|
||||
val loginData = LoginData()
|
||||
|
||||
// format is xxx://login/server:xxx&user:xxx&password:xxx
|
||||
val data: String = dataString.substring(prefix.length)
|
||||
val values: Array<String> = data.split("&").toTypedArray()
|
||||
if (values.size != PARAMETER_COUNT) {
|
||||
return null
|
||||
}
|
||||
for (value in values) {
|
||||
if (value.startsWith("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
|
||||
loginData.username = URLDecoder.decode(
|
||||
value.substring(("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length)
|
||||
)
|
||||
} else if (value.startsWith("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
|
||||
loginData.token = URLDecoder.decode(
|
||||
value.substring(("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length)
|
||||
)
|
||||
} else if (value.startsWith("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
|
||||
loginData.serverUrl = URLDecoder.decode(
|
||||
value.substring(("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length)
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return if (!TextUtils.isEmpty(loginData.serverUrl) &&
|
||||
!TextUtils.isEmpty(loginData.username) &&
|
||||
!TextUtils.isEmpty(loginData.token)
|
||||
) {
|
||||
loginData
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
dispose()
|
||||
}
|
||||
|
||||
init {
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
}
|
||||
|
||||
override val appBarLayoutType: AppBarLayoutType
|
||||
get() = AppBarLayoutType.EMPTY
|
||||
|
||||
companion object {
|
||||
private val TAG = WebViewLoginActivity::class.java.simpleName
|
||||
private const val PROTOCOL_SUFFIX = "://"
|
||||
private const val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"
|
||||
private const val PARAMETER_COUNT = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.account.data
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.nextcloud.talk.account.data.io.LocalLoginDataSource
|
||||
import com.nextcloud.talk.account.data.model.LoginCompletion
|
||||
import com.nextcloud.talk.account.data.model.LoginResponse
|
||||
import com.nextcloud.talk.account.data.network.NetworkLoginDataSource
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URLDecoder
|
||||
|
||||
@Suppress("TooManyFunctions", "ReturnCount")
|
||||
class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLoginDataSource) {
|
||||
|
||||
companion object {
|
||||
val TAG: String = LoginRepository::class.java.simpleName
|
||||
private const val INTERVAL = 250L
|
||||
private const val HTTP_OK = 200
|
||||
private const val USER_KEY = "user:"
|
||||
private const val SERVER_KEY = "server:"
|
||||
private const val PASS_KEY = "password:"
|
||||
private const val PREFIX = "nc://login/"
|
||||
private const val MAX_ARGS = 3
|
||||
}
|
||||
|
||||
private var shouldReauthorizeUser = false
|
||||
private var shouldLoop = true
|
||||
|
||||
suspend fun pollLogin(response: LoginResponse): LoginCompletion? =
|
||||
withContext(Dispatchers.IO) {
|
||||
while (shouldLoop) {
|
||||
val loginData = network.performLoginFlowV2(response)
|
||||
if (loginData == null) {
|
||||
break
|
||||
}
|
||||
|
||||
if (loginData.status == HTTP_OK) {
|
||||
return@withContext loginData
|
||||
}
|
||||
|
||||
delay(INTERVAL) // No response yet, retry
|
||||
}
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for QR scanner
|
||||
*
|
||||
*/
|
||||
fun startLoginFlowFromQR(dataString: String, reAuth: Boolean = false): LoginCompletion? {
|
||||
shouldReauthorizeUser = reAuth
|
||||
|
||||
if (!dataString.startsWith(PREFIX)) {
|
||||
Log.e(TAG, "Invalid login URL detected")
|
||||
return null
|
||||
}
|
||||
|
||||
val data = dataString.removePrefix(PREFIX)
|
||||
val values = data.split('&')
|
||||
|
||||
if (values.size !in 1..MAX_ARGS) {
|
||||
Log.e(TAG, "Illegal number of login URL elements detected: ${values.size}")
|
||||
return null
|
||||
}
|
||||
|
||||
var server = ""
|
||||
var loginName = ""
|
||||
var appPassword = ""
|
||||
values.forEach { value ->
|
||||
when {
|
||||
value.startsWith(USER_KEY) -> {
|
||||
loginName = URLDecoder.decode(value.removePrefix(USER_KEY), "UTF-8")
|
||||
}
|
||||
|
||||
value.startsWith(PASS_KEY) -> {
|
||||
appPassword = URLDecoder.decode(value.removePrefix(PASS_KEY), "UTF-8")
|
||||
}
|
||||
|
||||
value.startsWith(SERVER_KEY) -> {
|
||||
server = URLDecoder.decode(value.removePrefix(SERVER_KEY), "UTF-8")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (server.isNotEmpty() && loginName.isNotEmpty() && appPassword.isNotEmpty()) {
|
||||
LoginCompletion(HTTP_OK, server, loginName, appPassword)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point to the login process
|
||||
*/
|
||||
suspend fun startLoginFlow(baseUrl: String, reAuth: Boolean = false): LoginResponse? =
|
||||
withContext(Dispatchers.IO) {
|
||||
shouldReauthorizeUser = reAuth
|
||||
val response = network.anonymouslyPostLoginRequest(baseUrl)
|
||||
return@withContext response
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends normal login process by canceling the polling
|
||||
*/
|
||||
fun cancelLoginFlow() {
|
||||
shouldLoop = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bundle if user is not scheduled for deletion or doesn't already exist, null otherwise
|
||||
*/
|
||||
fun parseAndLogin(loginData: LoginCompletion): Bundle? {
|
||||
if (local.checkIfUserIsScheduledForDeletion(loginData)) {
|
||||
// however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it.
|
||||
local.startAccountRemovalWorker()
|
||||
return null
|
||||
} else if (local.checkIfUserExists(loginData)) {
|
||||
if (shouldReauthorizeUser) {
|
||||
local.updateUser(loginData)
|
||||
} else {
|
||||
Log.w(TAG, "Tried to add an account that account already exists. Skipped user creation.")
|
||||
}
|
||||
|
||||
return null
|
||||
} else {
|
||||
return startAccountVerification(loginData)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAccountVerification(loginData: LoginCompletion): Bundle {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_USERNAME, loginData.loginName)
|
||||
bundle.putString(KEY_TOKEN, loginData.appPassword)
|
||||
bundle.putString(KEY_BASE_URL, loginData.server)
|
||||
var protocol = ""
|
||||
if (loginData.server.startsWith("http://")) {
|
||||
protocol = "http://"
|
||||
} else if (loginData.server.startsWith("https://")) {
|
||||
protocol = "https://"
|
||||
}
|
||||
if (!TextUtils.isEmpty(protocol)) {
|
||||
bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
|
||||
}
|
||||
|
||||
return bundle
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.account.data.io
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import com.nextcloud.talk.account.data.model.LoginCompletion
|
||||
import com.nextcloud.talk.jobs.AccountRemovalWorker
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
|
||||
// local datasource for communicating with room through account manager
|
||||
// crucial for making sure the login process interacts with the db as expected.
|
||||
class LocalLoginDataSource(val userManager: UserManager, val appPreferences: AppPreferences, val context: Context) {
|
||||
|
||||
fun updateUser(loginData: LoginCompletion) {
|
||||
val currentUser = userManager.currentUser.blockingGet()
|
||||
if (currentUser != null) {
|
||||
currentUser.clientCertificate = appPreferences.temporaryClientCertAlias
|
||||
currentUser.token = loginData.appPassword
|
||||
userManager.updateOrCreateUser(currentUser)
|
||||
}
|
||||
}
|
||||
|
||||
fun startAccountRemovalWorker(): LiveData<WorkInfo?> {
|
||||
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
|
||||
WorkManager.getInstance(context).enqueue(accountRemovalWork)
|
||||
|
||||
return WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
|
||||
}
|
||||
|
||||
fun checkIfUserIsScheduledForDeletion(data: LoginCompletion): Boolean =
|
||||
userManager.checkIfUserIsScheduledForDeletion(data.loginName, data.server).blockingGet()
|
||||
|
||||
fun checkIfUserExists(data: LoginCompletion): Boolean =
|
||||
userManager.checkIfUserExists(data.loginName, data.server).blockingGet()
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.account.data.model
|
||||
|
||||
data class LoginResponse(val token: String, val pollUrl: String, val loginUrl: String)
|
||||
|
||||
data class LoginCompletion(val status: Int, val server: String, val loginName: String, val appPassword: String)
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.account.data.network
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.nextcloud.talk.account.data.model.LoginCompletion
|
||||
import com.nextcloud.talk.account.data.model.LoginResponse
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import java.io.IOException
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
// This class handles the network and polling logic in isolation, which makes it easier to test
|
||||
// Login and Authentication is critical, thus it needs to be working properly.
|
||||
class NetworkLoginDataSource(val okHttpClient: OkHttpClient) {
|
||||
|
||||
companion object {
|
||||
val TAG: String = NetworkLoginDataSource::class.java.simpleName
|
||||
}
|
||||
|
||||
fun anonymouslyPostLoginRequest(baseUrl: String): LoginResponse? {
|
||||
val url = "$baseUrl/index.php/login/v2"
|
||||
var result: LoginResponse? = null
|
||||
runCatching {
|
||||
val response = getResponseOfAnonymouslyPostLoginRequest(url)
|
||||
val jsonObject: JsonObject = JsonParser.parseString(response).asJsonObject
|
||||
val loginUrl: String = getLoginUrl(jsonObject)
|
||||
val token = jsonObject.getAsJsonObject("poll").get("token").asString
|
||||
val pollUrl = jsonObject.getAsJsonObject("poll").get("endpoint").asString
|
||||
result = LoginResponse(token, pollUrl, loginUrl)
|
||||
}.getOrElse { e ->
|
||||
when (e) {
|
||||
is SSLHandshakeException,
|
||||
is NullPointerException,
|
||||
is IOException -> {
|
||||
Log.e(TAG, "Error caught at anonymouslyPostLoginRequest: $e")
|
||||
}
|
||||
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getResponseOfAnonymouslyPostLoginRequest(url: String): String? {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.post(FormBody.Builder().build())
|
||||
.addHeader("Clear-Site-Data", "cookies")
|
||||
.build()
|
||||
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Unexpected code $response")
|
||||
}
|
||||
return response.body?.string()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLoginUrl(response: JsonObject): String {
|
||||
var result: String? = response.get("login").asString
|
||||
if (result == null) {
|
||||
result = ""
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun performLoginFlowV2(response: LoginResponse): LoginCompletion? {
|
||||
val requestBody: RequestBody = FormBody.Builder()
|
||||
.add("token", response.token)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(response.pollUrl)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
var result: LoginCompletion? = null
|
||||
runCatching {
|
||||
okHttpClient.newCall(request).execute()
|
||||
.use { response ->
|
||||
val status: Int = response.code
|
||||
val responseBody = response.body?.string()
|
||||
|
||||
result = if (response.isSuccessful && responseBody?.isNotEmpty() == true) {
|
||||
val jsonObject = JsonParser.parseString(responseBody).asJsonObject
|
||||
val server: String = jsonObject.get("server").asString
|
||||
val loginName: String = jsonObject.get("loginName").asString
|
||||
val appPassword: String = jsonObject.get("appPassword").asString
|
||||
|
||||
LoginCompletion(status, server, loginName, appPassword)
|
||||
} else {
|
||||
LoginCompletion(status, "", "", "")
|
||||
}
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
when (e) {
|
||||
is NullPointerException,
|
||||
is SSLHandshakeException,
|
||||
is IllegalStateException,
|
||||
is IOException -> {
|
||||
Log.e(TAG, "Error caught at performLoginFlowV2: $e")
|
||||
}
|
||||
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.account.viewmodels
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.nextcloud.talk.account.data.LoginRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class BrowserLoginActivityViewModel @Inject constructor(val repository: LoginRepository) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = BrowserLoginActivityViewModel::class.java.simpleName
|
||||
}
|
||||
|
||||
sealed class InitialLoginViewState {
|
||||
data object None : InitialLoginViewState()
|
||||
data class InitialLoginRequestSuccess(val loginUrl: String) : InitialLoginViewState()
|
||||
data object InitialLoginRequestError : InitialLoginViewState()
|
||||
}
|
||||
|
||||
private val _initialLoginRequestState = MutableStateFlow<InitialLoginViewState>(InitialLoginViewState.None)
|
||||
val initialLoginRequestState: StateFlow<InitialLoginViewState> = _initialLoginRequestState
|
||||
|
||||
sealed class PostLoginViewState {
|
||||
data object None : PostLoginViewState()
|
||||
data object PostLoginRestartApp : PostLoginViewState()
|
||||
data object PostLoginError : PostLoginViewState()
|
||||
data class PostLoginContinue(val data: Bundle) : PostLoginViewState()
|
||||
}
|
||||
|
||||
private val _postLoginState = MutableStateFlow<PostLoginViewState>(PostLoginViewState.None)
|
||||
val postLoginState: StateFlow<PostLoginViewState> = _postLoginState
|
||||
|
||||
fun loginNormally(baseUrl: String, reAuth: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
val response = repository.startLoginFlow(baseUrl, reAuth)
|
||||
|
||||
if (response == null) {
|
||||
_initialLoginRequestState.value = InitialLoginViewState.InitialLoginRequestError
|
||||
return@launch
|
||||
}
|
||||
|
||||
_initialLoginRequestState.value =
|
||||
InitialLoginViewState.InitialLoginRequestSuccess(response.loginUrl)
|
||||
|
||||
val loginCompletionResponse = repository.pollLogin(response)
|
||||
|
||||
if (loginCompletionResponse == null) {
|
||||
_postLoginState.value = PostLoginViewState.PostLoginError
|
||||
return@launch
|
||||
}
|
||||
|
||||
val bundle = repository.parseAndLogin(loginCompletionResponse)
|
||||
if (bundle == null) {
|
||||
_postLoginState.value = PostLoginViewState.PostLoginRestartApp
|
||||
return@launch
|
||||
}
|
||||
|
||||
_postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
fun loginWithQR(dataString: String, reAuth: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
val loginCompletionResponse = repository.startLoginFlowFromQR(dataString, reAuth)
|
||||
if (loginCompletionResponse == null) {
|
||||
_postLoginState.value = PostLoginViewState.PostLoginError
|
||||
return@launch
|
||||
}
|
||||
|
||||
val bundle = repository.parseAndLogin(loginCompletionResponse)
|
||||
if (bundle == null) {
|
||||
_postLoginState.value = PostLoginViewState.PostLoginRestartApp
|
||||
return@launch
|
||||
}
|
||||
|
||||
_postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelLogin() = repository.cancelLoginFlow()
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2016 BlueLine Labs, Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.nextcloud.talk.activities;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
public interface ActionBarProvider {
|
||||
ActionBar getSupportActionBar();
|
||||
}
|
||||
295
app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
Normal file
295
app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.account.AccountVerificationActivity
|
||||
import com.nextcloud.talk.account.ServerSelectionActivity
|
||||
import com.nextcloud.talk.account.SwitchAccountActivity
|
||||
import com.nextcloud.talk.account.WebViewLoginActivity
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.events.CertificateEvent
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.FileViewerUtils
|
||||
import com.nextcloud.talk.utils.UriUtils
|
||||
import com.nextcloud.talk.utils.adjustUIForAPILevel35
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.nextcloud.talk.utils.ssl.TrustManager
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.security.cert.CertificateParsingException
|
||||
import java.security.cert.X509Certificate
|
||||
import java.text.DateFormat
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
open class BaseActivity : AppCompatActivity() {
|
||||
|
||||
enum class AppBarLayoutType {
|
||||
TOOLBAR,
|
||||
SEARCH_BAR,
|
||||
EMPTY
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var eventBus: EventBus
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var currentUserProvider: CurrentUserProviderNew
|
||||
|
||||
open val appBarLayoutType: AppBarLayoutType
|
||||
get() = AppBarLayoutType.TOOLBAR
|
||||
|
||||
open val view: View?
|
||||
get() = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
adjustUIForAPILevel35()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
cleanTempCertPreference()
|
||||
}
|
||||
|
||||
public override fun onStart() {
|
||||
super.onStart()
|
||||
eventBus.register(this)
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (appPreferences.isKeyboardIncognito) {
|
||||
val viewGroup = (findViewById<View>(android.R.id.content) as ViewGroup).getChildAt(0) as ViewGroup
|
||||
disableKeyboardPersonalisedLearning(viewGroup)
|
||||
}
|
||||
|
||||
if (appPreferences.isScreenSecured || appPreferences.isScreenLocked) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onStop() {
|
||||
super.onStop()
|
||||
eventBus.unregister(this)
|
||||
}
|
||||
|
||||
/*
|
||||
* May be aligned with android-common lib in the future: .../ui/util/extensions/AppCompatActivityExtensions.kt
|
||||
*/
|
||||
fun initSystemBars() {
|
||||
val decorView = window.decorView
|
||||
decorView.setOnApplyWindowInsetsListener { view, insets ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
val systemBars = insets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars() or
|
||||
WindowInsetsCompat.Type.displayCutout()
|
||||
)
|
||||
val color = ResourcesCompat.getColor(resources, R.color.bg_default, context.theme)
|
||||
view.setBackgroundColor(color)
|
||||
view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
} else {
|
||||
colorizeStatusBar()
|
||||
colorizeNavigationBar()
|
||||
}
|
||||
insets
|
||||
}
|
||||
ViewCompat.requestApplyInsets(decorView)
|
||||
}
|
||||
|
||||
open fun colorizeStatusBar() {
|
||||
if (resources != null) {
|
||||
if (appBarLayoutType == AppBarLayoutType.SEARCH_BAR) {
|
||||
viewThemeUtils.platform.resetStatusBar(this)
|
||||
} else {
|
||||
viewThemeUtils.platform.themeStatusBar(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun colorizeNavigationBar() {
|
||||
if (resources != null) {
|
||||
DisplayUtils.applyColorToNavigationBar(
|
||||
this.window,
|
||||
ResourcesCompat.getColor(resources, R.color.bg_default, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disableKeyboardPersonalisedLearning(viewGroup: ViewGroup) {
|
||||
var view: View?
|
||||
var editText: EditText
|
||||
for (i in 0 until viewGroup.childCount) {
|
||||
view = viewGroup.getChildAt(i)
|
||||
if (view is EditText) {
|
||||
editText = view
|
||||
editText.imeOptions = editText.imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||
} else if (view is ViewGroup) {
|
||||
disableKeyboardPersonalisedLearning(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Detekt.NestedBlockDepth")
|
||||
private fun showCertificateDialog(
|
||||
cert: X509Certificate,
|
||||
trustManager: TrustManager,
|
||||
sslErrorHandler: SslErrorHandler?
|
||||
) {
|
||||
val formatter = DateFormat.getDateInstance(DateFormat.LONG)
|
||||
val validFrom = formatter.format(cert.notBefore)
|
||||
val validUntil = formatter.format(cert.notAfter)
|
||||
|
||||
val issuedBy = cert.issuerDN.toString()
|
||||
val issuedFor: String
|
||||
|
||||
try {
|
||||
if (cert.subjectAlternativeNames != null) {
|
||||
val stringBuilder = StringBuilder()
|
||||
for (o in cert.subjectAlternativeNames) {
|
||||
val list = o as List<*>
|
||||
val type = list[0] as Int
|
||||
if (type == 2) {
|
||||
val name = list[1] as String
|
||||
stringBuilder.append("[").append(type).append("]").append(name).append(" ")
|
||||
}
|
||||
}
|
||||
issuedFor = stringBuilder.toString()
|
||||
} else {
|
||||
issuedFor = cert.subjectDN.name
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
val dialogText = String.format(
|
||||
resources.getString(R.string.nc_certificate_dialog_text),
|
||||
issuedBy,
|
||||
issuedFor,
|
||||
validFrom,
|
||||
validUntil
|
||||
)
|
||||
|
||||
val dialogBuilder = MaterialAlertDialogBuilder(this).setIcon(
|
||||
viewThemeUtils.dialog.colorMaterialAlertDialogIcon(
|
||||
context,
|
||||
R.drawable.ic_security_white_24dp
|
||||
)
|
||||
).setTitle(R.string.nc_certificate_dialog_title)
|
||||
.setMessage(dialogText)
|
||||
.setPositiveButton(R.string.nc_yes) { _, _ ->
|
||||
trustManager.addCertInTrustStore(cert)
|
||||
sslErrorHandler?.proceed()
|
||||
}.setNegativeButton(R.string.nc_no) { _, _ ->
|
||||
sslErrorHandler?.cancel()
|
||||
}
|
||||
|
||||
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(context, dialogBuilder)
|
||||
|
||||
val dialog = dialogBuilder.show()
|
||||
|
||||
viewThemeUtils.platform.colorTextButtons(
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE),
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
|
||||
)
|
||||
} catch (e: CertificateParsingException) {
|
||||
Log.d(TAG, "Failed to parse the certificate")
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanTempCertPreference() {
|
||||
val temporaryClassNames: MutableList<String> = ArrayList()
|
||||
temporaryClassNames.add(ServerSelectionActivity::class.java.name)
|
||||
temporaryClassNames.add(AccountVerificationActivity::class.java.name)
|
||||
temporaryClassNames.add(WebViewLoginActivity::class.java.name)
|
||||
temporaryClassNames.add(SwitchAccountActivity::class.java.name)
|
||||
if (!temporaryClassNames.contains(javaClass.name)) {
|
||||
appPreferences.removeTemporaryClientCertAlias()
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onMessageEvent(event: CertificateEvent) {
|
||||
showCertificateDialog(event.x509Certificate, event.trustManager, event.sslErrorHandler)
|
||||
}
|
||||
|
||||
override fun startActivity(intent: Intent) {
|
||||
val user = currentUserProvider.currentUser.blockingGet()
|
||||
if (intent.data != null && TextUtils.equals(intent.action, Intent.ACTION_VIEW)) {
|
||||
val uri = intent.data.toString()
|
||||
if (user?.baseUrl != null && uri.startsWith(user.baseUrl!!)) {
|
||||
if (UriUtils.isInstanceInternalFileShareUrl(user.baseUrl!!, uri)) {
|
||||
// https://cloud.nextcloud.com/f/41
|
||||
val fileViewerUtils = FileViewerUtils(applicationContext, user)
|
||||
fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileShareFileId(uri))
|
||||
} else if (UriUtils.isInstanceInternalFileUrl(user.baseUrl!!, uri)) {
|
||||
// https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41
|
||||
val fileViewerUtils = FileViewerUtils(applicationContext, user)
|
||||
fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileFileId(uri))
|
||||
} else if (UriUtils.isInstanceInternalFileUrlNew(user.baseUrl!!, uri)) {
|
||||
// https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41
|
||||
val fileViewerUtils = FileViewerUtils(applicationContext, user)
|
||||
fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileFileIdNew(uri))
|
||||
} else if (UriUtils.isInstanceInternalTalkUrl(user.baseUrl!!, uri)) {
|
||||
// https://cloud.nextcloud.com/call/123456789
|
||||
val bundle = Bundle()
|
||||
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, UriUtils.extractRoomTokenFromTalkUrl(uri))
|
||||
val chatIntent = Intent(context, ChatActivity::class.java)
|
||||
chatIntent.putExtras(bundle)
|
||||
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
startActivity(chatIntent)
|
||||
} else {
|
||||
super.startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
super.startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
super.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = BaseActivity::class.java.simpleName
|
||||
}
|
||||
}
|
||||
3355
app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
Normal file
3355
app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.activities;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AppOpsManager;
|
||||
import android.app.KeyguardManager;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Rational;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.nextcloud.talk.BuildConfig;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
|
||||
public abstract class CallBaseActivity extends BaseActivity {
|
||||
|
||||
public static final String TAG = "CallBaseActivity";
|
||||
|
||||
public PictureInPictureParams.Builder mPictureInPictureParamsBuilder;
|
||||
public Boolean isInPipMode = Boolean.FALSE;
|
||||
long onCreateTime;
|
||||
|
||||
|
||||
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (isPipModePossible()) {
|
||||
enterPipMode();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
onCreateTime = System.currentTimeMillis();
|
||||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
dismissKeyguard();
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
|
||||
if (isPipModePossible()) {
|
||||
mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
|
||||
}
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
|
||||
}
|
||||
|
||||
public void hideNavigationIfNoPipAvailable(){
|
||||
if (!isPipModePossible()) {
|
||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||
suppressFitsSystemWindows();
|
||||
}
|
||||
}
|
||||
|
||||
void dismissKeyguard() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
setShowWhenLocked(true);
|
||||
setTurnScreenOn(true);
|
||||
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
|
||||
keyguardManager.requestDismissKeyguard(this, null);
|
||||
} else {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
|
||||
void enableKeyguard() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
setShowWhenLocked(false);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (isInPipMode) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUserLeaveHint() {
|
||||
super.onUserLeaveHint();
|
||||
long onUserLeaveHintTime = System.currentTimeMillis();
|
||||
long diff = onUserLeaveHintTime - onCreateTime;
|
||||
Log.d(TAG, "onUserLeaveHintTime - onCreateTime: " + diff);
|
||||
|
||||
if (diff < 3000) {
|
||||
Log.d(TAG, "enterPipMode skipped");
|
||||
} else {
|
||||
enterPipMode();
|
||||
}
|
||||
}
|
||||
|
||||
void enterPipMode() {
|
||||
enableKeyguard();
|
||||
if (isPipModePossible()) {
|
||||
Rational pipRatio = new Rational(300, 500);
|
||||
mPictureInPictureParamsBuilder.setAspectRatio(pipRatio);
|
||||
enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
|
||||
} else {
|
||||
// we don't support other solutions than PIP to have a call in the background.
|
||||
// If PIP is not available the call is ended when user presses the home button.
|
||||
Log.d(TAG, "Activity was finished because PIP is not available.");
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
boolean isPipModePossible() {
|
||||
boolean deviceHasPipFeature = getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
||||
|
||||
AppOpsManager appOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
|
||||
boolean isPipFeatureGranted = appOpsManager.checkOpNoThrow(
|
||||
AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
|
||||
android.os.Process.myUid(),
|
||||
BuildConfig.APPLICATION_ID) == AppOpsManager.MODE_ALLOWED;
|
||||
return deviceHasPipFeature && isPipFeatureGranted;
|
||||
}
|
||||
|
||||
public abstract void updateUiForPipMode();
|
||||
|
||||
public abstract void updateUiForNormalMode();
|
||||
|
||||
public abstract void suppressFitsSystemWindows();
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.activities
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
enum class CallStatus : Parcelable {
|
||||
CONNECTING,
|
||||
CALLING_TIMEOUT,
|
||||
JOINED,
|
||||
IN_CONVERSATION,
|
||||
RECONNECTING,
|
||||
OFFLINE,
|
||||
LEAVING,
|
||||
PUBLISHER_FAILED
|
||||
}
|
||||
289
app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
Normal file
289
app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2023 Ezhil Shanmugham <ezhil56x.contact@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.activities
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.account.ServerSelectionActivity
|
||||
import com.nextcloud.talk.account.WebViewLoginActivity
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.conversationlist.ConversationsListActivity
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.ActivityMainBinding
|
||||
import com.nextcloud.talk.invitation.InvitationsActivity
|
||||
import com.nextcloud.talk.lock.LockedActivity
|
||||
import com.nextcloud.talk.models.json.conversations.RoomOverall
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.ClosedInterfaceImpl
|
||||
import com.nextcloud.talk.utils.SecurityUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.SingleObserver
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class MainActivity :
|
||||
BaseActivity(),
|
||||
ActionBarProvider {
|
||||
|
||||
lateinit var binding: ActivityMainBinding
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Log.d(TAG, "onCreate: Activity: " + System.identityHashCode(this).toString())
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
lockScreenIfConditionsApply()
|
||||
}
|
||||
})
|
||||
|
||||
// Set the default theme to replace the launch screen theme.
|
||||
setTheme(R.style.AppTheme)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
handleIntent(intent)
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
fun lockScreenIfConditionsApply() {
|
||||
val keyguardManager = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
|
||||
if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) {
|
||||
if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)) {
|
||||
val lockIntent = Intent(context, LockedActivity::class.java)
|
||||
startActivity(lockIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchServerSelection() {
|
||||
if (isBrandingUrlSet()) {
|
||||
val intent = Intent(context, WebViewLoginActivity::class.java)
|
||||
val bundle = Bundle()
|
||||
bundle.putString(BundleKeys.KEY_BASE_URL, resources.getString(R.string.weblogin_url))
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
val intent = Intent(context, ServerSelectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBrandingUrlSet() = !TextUtils.isEmpty(resources.getString(R.string.weblogin_url))
|
||||
|
||||
override fun onStart() {
|
||||
Log.d(TAG, "onStart: Activity: " + System.identityHashCode(this).toString())
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
Log.d(TAG, "onResume: Activity: " + System.identityHashCode(this).toString())
|
||||
super.onResume()
|
||||
|
||||
if (appPreferences.isScreenLocked) {
|
||||
SecurityUtils.createKey(appPreferences.screenLockTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
Log.d(TAG, "onPause: Activity: " + System.identityHashCode(this).toString())
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Log.d(TAG, "onStop: Activity: " + System.identityHashCode(this).toString())
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
private fun openConversationList() {
|
||||
val intent = Intent(this, ConversationsListActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
intent.putExtras(Bundle())
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun handleActionFromContact(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
|
||||
val cursor = contentResolver.query(intent.data!!, null, null, null, null)
|
||||
|
||||
var userId = ""
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
// userId @ server
|
||||
userId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.DATA1))
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
when (intent.type) {
|
||||
"vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" -> {
|
||||
val user = userId.substringBeforeLast("@")
|
||||
val baseUrl = userId.substringAfterLast("@")
|
||||
|
||||
if (currentUserProvider.currentUser.blockingGet()?.baseUrl!!.endsWith(baseUrl) == true) {
|
||||
startConversation(user)
|
||||
} else {
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
R.string.nc_phone_book_integration_account_not_found,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startConversation(userId: String) {
|
||||
val roomType = "1"
|
||||
|
||||
val currentUser = currentUserProvider.currentUser.blockingGet()
|
||||
|
||||
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, 1))
|
||||
val credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)
|
||||
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
|
||||
version = apiVersion,
|
||||
baseUrl = currentUser?.baseUrl!!,
|
||||
roomType = roomType,
|
||||
invite = userId
|
||||
)
|
||||
|
||||
ncApi.createRoom(
|
||||
credentials,
|
||||
retrofitBucket.url,
|
||||
retrofitBucket.queryMap
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(object : Observer<RoomOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onNext(roomOverall: RoomOverall) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
|
||||
|
||||
val chatIntent = Intent(context, ChatActivity::class.java)
|
||||
chatIntent.putExtras(bundle)
|
||||
startActivity(chatIntent)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Log.d(TAG, "onNewIntent Activity: " + System.identityHashCode(this).toString())
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent) {
|
||||
handleActionFromContact(intent)
|
||||
|
||||
val internalUserId = intent.extras?.getLong(BundleKeys.KEY_INTERNAL_USER_ID)
|
||||
|
||||
var user: User? = null
|
||||
if (internalUserId != null) {
|
||||
user = userManager.getUserWithId(internalUserId).blockingGet()
|
||||
}
|
||||
|
||||
if (user != null && userManager.setUserAsActive(user).blockingGet()) {
|
||||
if (intent.hasExtra(BundleKeys.KEY_REMOTE_TALK_SHARE)) {
|
||||
if (intent.getBooleanExtra(BundleKeys.KEY_REMOTE_TALK_SHARE, false)) {
|
||||
val invitationsIntent = Intent(this, InvitationsActivity::class.java)
|
||||
startActivity(invitationsIntent)
|
||||
}
|
||||
} else {
|
||||
val chatIntent = Intent(context, ChatActivity::class.java)
|
||||
chatIntent.putExtras(intent.extras!!)
|
||||
startActivity(chatIntent)
|
||||
}
|
||||
} else {
|
||||
userManager.users.subscribe(object : SingleObserver<List<User>> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onSuccess(users: List<User>) {
|
||||
if (users.isNotEmpty()) {
|
||||
ClosedInterfaceImpl().setUpPushTokenRegistration()
|
||||
runOnUiThread {
|
||||
openConversationList()
|
||||
}
|
||||
} else {
|
||||
runOnUiThread {
|
||||
launchServerSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "Error loading existing users", e)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.resources.getString(R.string.nc_common_error_sorry),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MainActivity::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Stefan Niedermann <info@niedermann.it>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.hardware.camera2.CameraMetadata;
|
||||
import android.hardware.camera2.CaptureRequest;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.view.OrientationEventListener;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.nextcloud.talk.R;
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication;
|
||||
import com.nextcloud.talk.databinding.ActivityTakePictureBinding;
|
||||
import com.nextcloud.talk.models.TakePictureViewModel;
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils;
|
||||
import com.nextcloud.talk.utils.BitmapShrinker;
|
||||
import com.nextcloud.talk.utils.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.camera.camera2.interop.Camera2Interop;
|
||||
import androidx.camera.core.AspectRatio;
|
||||
import androidx.camera.core.Camera;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCaptureException;
|
||||
import androidx.camera.core.Preview;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import autodagger.AutoInjector;
|
||||
|
||||
import static com.nextcloud.talk.utils.Mimetype.IMAGE_JPEG;
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication.class)
|
||||
public class TakePhotoActivity extends AppCompatActivity {
|
||||
private static final String TAG = TakePhotoActivity.class.getSimpleName();
|
||||
|
||||
private static final float MAX_SCALE = 6.0f;
|
||||
private static final float MEDIUM_SCALE = 2.45f;
|
||||
|
||||
private ActivityTakePictureBinding binding;
|
||||
private TakePictureViewModel viewModel;
|
||||
|
||||
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
|
||||
private OrientationEventListener orientationEventListener;
|
||||
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss", Locale.ROOT);
|
||||
|
||||
private Camera camera;
|
||||
|
||||
@Inject
|
||||
ViewThemeUtils viewThemeUtils;
|
||||
|
||||
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
Uri uri = (Uri) binding.photoPreview.getTag();
|
||||
|
||||
if (uri != null) {
|
||||
File photoFile = new File(uri.getPath());
|
||||
if (!photoFile.delete()) {
|
||||
Log.w(TAG, "Error deleting temp camera image");
|
||||
}
|
||||
binding.photoPreview.setTag(null);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
|
||||
|
||||
binding = ActivityTakePictureBinding.inflate(getLayoutInflater());
|
||||
viewModel = new ViewModelProvider(this).get(TakePictureViewModel.class);
|
||||
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
viewThemeUtils.material.themeFAB(binding.takePhoto);
|
||||
viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.send);
|
||||
|
||||
cameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
||||
cameraProviderFuture.addListener(() -> {
|
||||
try {
|
||||
final ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
|
||||
|
||||
camera = cameraProvider.bindToLifecycle(
|
||||
this,
|
||||
viewModel.getCameraSelector(),
|
||||
getImageCapture(
|
||||
viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
|
||||
getPreview(viewModel.isCropEnabled().getValue()));
|
||||
|
||||
viewModel.getTorchToggleButtonImageResource()
|
||||
.observe(
|
||||
this,
|
||||
res -> binding.toggleTorch.setIcon(ContextCompat.getDrawable(this, res)));
|
||||
viewModel.isTorchEnabled()
|
||||
.observe(
|
||||
this,
|
||||
enabled -> camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue()));
|
||||
binding.toggleTorch.setOnClickListener((v) -> viewModel.toggleTorchEnabled());
|
||||
|
||||
viewModel.getCropToggleButtonImageResource()
|
||||
.observe(
|
||||
this,
|
||||
res -> binding.toggleCrop.setIcon(ContextCompat.getDrawable(this, res)));
|
||||
viewModel.isCropEnabled()
|
||||
.observe(
|
||||
this,
|
||||
enabled -> {
|
||||
cameraProvider.unbindAll();
|
||||
camera = cameraProvider.bindToLifecycle(
|
||||
this,
|
||||
viewModel.getCameraSelector(),
|
||||
getImageCapture(
|
||||
viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
|
||||
getPreview(viewModel.isCropEnabled().getValue()));
|
||||
camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue());
|
||||
});
|
||||
binding.toggleCrop.setOnClickListener((v) -> viewModel.toggleCropEnabled());
|
||||
|
||||
viewModel.getLowResolutionToggleButtonImageResource()
|
||||
.observe(
|
||||
this,
|
||||
res -> binding.toggleLowres.setIcon(ContextCompat.getDrawable(this, res)));
|
||||
viewModel.isLowResolutionEnabled()
|
||||
.observe(
|
||||
this,
|
||||
enabled -> {
|
||||
cameraProvider.unbindAll();
|
||||
camera = cameraProvider.bindToLifecycle(
|
||||
this,
|
||||
viewModel.getCameraSelector(),
|
||||
getImageCapture(
|
||||
viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
|
||||
getPreview(viewModel.isCropEnabled().getValue()));
|
||||
camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue());
|
||||
});
|
||||
binding.toggleLowres.setOnClickListener((v) -> viewModel.toggleLowResolutionEnabled());
|
||||
|
||||
binding.switchCamera.setOnClickListener((v) -> {
|
||||
viewModel.toggleCameraSelector();
|
||||
cameraProvider.unbindAll();
|
||||
camera = cameraProvider.bindToLifecycle(
|
||||
this,
|
||||
viewModel.getCameraSelector(),
|
||||
getImageCapture(
|
||||
viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
|
||||
getPreview(viewModel.isCropEnabled().getValue()));
|
||||
});
|
||||
binding.retake.setOnClickListener((v) -> {
|
||||
Uri uri = (Uri) binding.photoPreview.getTag();
|
||||
File photoFile = new File(uri.getPath());
|
||||
if (!photoFile.delete()) {
|
||||
Log.w(TAG, "Error deleting temp camera image");
|
||||
}
|
||||
binding.takePhoto.setEnabled(true);
|
||||
binding.photoPreview.setTag(null);
|
||||
showCameraElements();
|
||||
});
|
||||
binding.send.setOnClickListener((v) -> {
|
||||
Uri uri = (Uri) binding.photoPreview.getTag();
|
||||
setResult(RESULT_OK, new Intent().setDataAndType(uri, IMAGE_JPEG));
|
||||
binding.photoPreview.setTag(null);
|
||||
finish();
|
||||
});
|
||||
|
||||
ScaleGestureDetector mDetector =
|
||||
new ScaleGestureDetector(this, new ScaleGestureDetector.SimpleOnScaleGestureListener(){
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector){
|
||||
float ratio = camera.getCameraInfo().getZoomState().getValue().getZoomRatio();
|
||||
float delta = detector.getScaleFactor();
|
||||
camera.getCameraControl().setZoomRatio(ratio * delta);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
binding.preview.setOnTouchListener((v, event) -> {
|
||||
v.performClick();
|
||||
mDetector.onTouchEvent(event);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Enable enlarging the image more than default 3x maximumScale.
|
||||
// Medium scale adapted to make double-tap behaviour more consistent.
|
||||
binding.photoPreview.setMaximumScale(MAX_SCALE);
|
||||
binding.photoPreview.setMediumScale(MEDIUM_SCALE);
|
||||
} catch (IllegalArgumentException | ExecutionException | InterruptedException e) {
|
||||
Log.e(TAG, "Error taking picture", e);
|
||||
Snackbar.make(binding.getRoot(), e.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
|
||||
}
|
||||
|
||||
private void showCameraElements() {
|
||||
binding.send.setVisibility(View.GONE);
|
||||
binding.retake.setVisibility(View.GONE);
|
||||
binding.photoPreview.setVisibility(View.INVISIBLE);
|
||||
|
||||
binding.preview.setVisibility(View.VISIBLE);
|
||||
binding.takePhoto.setVisibility(View.VISIBLE);
|
||||
binding.switchCamera.setVisibility(View.VISIBLE);
|
||||
binding.toggleTorch.setVisibility(View.VISIBLE);
|
||||
binding.toggleCrop.setVisibility(View.VISIBLE);
|
||||
binding.toggleLowres.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void showPictureProcessingElements() {
|
||||
binding.preview.setVisibility(View.INVISIBLE);
|
||||
binding.takePhoto.setVisibility(View.GONE);
|
||||
binding.switchCamera.setVisibility(View.GONE);
|
||||
binding.toggleTorch.setVisibility(View.GONE);
|
||||
binding.toggleCrop.setVisibility(View.GONE);
|
||||
binding.toggleLowres.setVisibility(View.GONE);
|
||||
|
||||
binding.send.setVisibility(View.VISIBLE);
|
||||
binding.retake.setVisibility(View.VISIBLE);
|
||||
binding.photoPreview.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private ImageCapture getImageCapture(Boolean crop, Boolean lowres) {
|
||||
final ImageCapture imageCapture;
|
||||
if (lowres) imageCapture = new ImageCapture.Builder()
|
||||
.setTargetResolution(new Size(crop ? 1080 : 1440, 1920)).build();
|
||||
else imageCapture = new ImageCapture.Builder()
|
||||
.setTargetAspectRatio(crop ? AspectRatio.RATIO_16_9 : AspectRatio.RATIO_4_3).build();
|
||||
|
||||
orientationEventListener = new OrientationEventListener(this) {
|
||||
@Override
|
||||
public void onOrientationChanged(int orientation) {
|
||||
int rotation;
|
||||
|
||||
// Monitors orientation values to determine the target rotation value
|
||||
if (orientation >= 45 && orientation < 135) {
|
||||
rotation = Surface.ROTATION_270;
|
||||
} else if (orientation >= 135 && orientation < 225) {
|
||||
rotation = Surface.ROTATION_180;
|
||||
} else if (orientation >= 225 && orientation < 315) {
|
||||
rotation = Surface.ROTATION_90;
|
||||
} else {
|
||||
rotation = Surface.ROTATION_0;
|
||||
}
|
||||
|
||||
imageCapture.setTargetRotation(rotation);
|
||||
}
|
||||
};
|
||||
orientationEventListener.enable();
|
||||
|
||||
binding.takePhoto.setOnClickListener((v) -> {
|
||||
binding.takePhoto.setEnabled(false);
|
||||
final String photoFileName = dateFormat.format(new Date()) + ".jpg";
|
||||
try {
|
||||
final File photoFile = FileUtils.getTempCacheFile(this, "photos/" + photoFileName);
|
||||
final ImageCapture.OutputFileOptions options =
|
||||
new ImageCapture.OutputFileOptions.Builder(photoFile).build();
|
||||
imageCapture.takePicture(
|
||||
options,
|
||||
ContextCompat.getMainExecutor(this),
|
||||
new ImageCapture.OnImageSavedCallback() {
|
||||
|
||||
@Override
|
||||
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
|
||||
setPreviewImage(photoFile);
|
||||
showPictureProcessingElements();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull ImageCaptureException e) {
|
||||
Log.e(TAG, "Error", e);
|
||||
|
||||
if (!photoFile.delete()) {
|
||||
Log.w(TAG, "Deleting picture failed");
|
||||
}
|
||||
binding.takePhoto.setEnabled(true);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "error while taking picture", e);
|
||||
Snackbar.make(binding.getRoot(), R.string.take_photo_error_deleting_picture, Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
|
||||
return imageCapture;
|
||||
}
|
||||
|
||||
private void setPreviewImage(File photoFile) {
|
||||
final Uri savedUri = Uri.fromFile(photoFile);
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int doubleScreenWidth = displayMetrics.widthPixels * 2;
|
||||
int doubleScreenHeight = displayMetrics.heightPixels * 2;
|
||||
|
||||
Bitmap bitmap = BitmapShrinker.shrinkBitmap(photoFile.getAbsolutePath(),
|
||||
doubleScreenWidth,
|
||||
doubleScreenHeight);
|
||||
|
||||
binding.photoPreview.setImageBitmap(bitmap);
|
||||
binding.photoPreview.setTag(savedUri);
|
||||
viewModel.disableTorchIfEnabled();
|
||||
}
|
||||
|
||||
public int getImageOrientation(File imageFile) {
|
||||
int rotate = 0;
|
||||
try {
|
||||
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
|
||||
int orientation = exif.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_NORMAL);
|
||||
|
||||
switch (orientation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
rotate = 270;
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
rotate = 180;
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
rotate = 90;
|
||||
break;
|
||||
default:
|
||||
rotate = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
Log.i(TAG, "ImageOrientation - Exif orientation: " + orientation + " - " + "Rotate value: " + rotate);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error calculation rotation value");
|
||||
}
|
||||
return rotate;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)
|
||||
private Preview getPreview(boolean crop) {
|
||||
Preview.Builder previewBuilder = new Preview.Builder()
|
||||
.setTargetAspectRatio(crop ? AspectRatio.RATIO_16_9 : AspectRatio.RATIO_4_3);
|
||||
new Camera2Interop.Extender<>(previewBuilder)
|
||||
.setCaptureRequestOption(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
|
||||
CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF
|
||||
);
|
||||
|
||||
Preview preview = previewBuilder.build();
|
||||
preview.setSurfaceProvider(binding.preview.getSurfaceProvider());
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
if (this.orientationEventListener != null) {
|
||||
this.orientationEventListener.disable();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (this.orientationEventListener != null) {
|
||||
this.orientationEventListener.enable();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle savedInstanceState) {
|
||||
if (binding.photoPreview.getTag() != null) {
|
||||
savedInstanceState.putString("Uri", ((Uri) binding.photoPreview.getTag()).getPath());
|
||||
}
|
||||
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
|
||||
String uri = savedInstanceState.getString("Uri", null);
|
||||
|
||||
if (uri != null) {
|
||||
File photoFile = new File(uri);
|
||||
setPreviewImage(photoFile);
|
||||
showPictureProcessingElements();
|
||||
}
|
||||
}
|
||||
|
||||
public static Intent createIntent(@NonNull Context context) {
|
||||
return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import fr.dudie.nominatim.model.Address
|
||||
|
||||
class GeocodingAdapter(private val context: Context, private var dataSource: List<Address>) :
|
||||
RecyclerView.Adapter<GeocodingAdapter.ViewHolder>() {
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(position: Int)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateData(data: List<Address>) {
|
||||
this.dataSource = data
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private var listener: OnItemClickListener? = null
|
||||
fun setOnItemClickListener(listener: OnItemClickListener) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.geocoding_item, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val address = dataSource[position]
|
||||
holder.nameView.text = address.displayName
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
listener?.onItemClick(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = dataSource.size
|
||||
|
||||
fun getItem(position: Int): Any = dataSource[position]
|
||||
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val nameView: TextView = itemView.findViewById(R.id.name)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2021-2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import com.nextcloud.talk.call.CallParticipantModel
|
||||
import com.nextcloud.talk.call.RaisedHand
|
||||
import com.nextcloud.talk.models.json.participants.Participant.ActorType
|
||||
import com.nextcloud.talk.utils.ApiUtils.getUrlForAvatar
|
||||
import com.nextcloud.talk.utils.ApiUtils.getUrlForFederatedAvatar
|
||||
import com.nextcloud.talk.utils.ApiUtils.getUrlForGuestAvatar
|
||||
import com.nextcloud.talk.utils.DisplayUtils.isDarkModeOn
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.webrtc.EglBase
|
||||
import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection.IceConnectionState
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
|
||||
data class ParticipantUiState(
|
||||
val sessionKey: String,
|
||||
val nick: String,
|
||||
val isConnected: Boolean,
|
||||
val isAudioEnabled: Boolean,
|
||||
val isStreamEnabled: Boolean,
|
||||
val raisedHand: Boolean,
|
||||
val avatarUrl: String?,
|
||||
val mediaStream: MediaStream?
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class ParticipantDisplayItem(
|
||||
private val context: Context,
|
||||
private val baseUrl: String,
|
||||
private val defaultGuestNick: String,
|
||||
val rootEglBase: EglBase,
|
||||
private val streamType: String,
|
||||
private val roomToken: String,
|
||||
private val callParticipantModel: CallParticipantModel
|
||||
) {
|
||||
private val participantDisplayItemNotifier = ParticipantDisplayItemNotifier()
|
||||
|
||||
private val _uiStateFlow = MutableStateFlow(buildUiState())
|
||||
val uiStateFlow: StateFlow<ParticipantUiState> = _uiStateFlow.asStateFlow()
|
||||
|
||||
private val session: String = callParticipantModel.sessionId
|
||||
|
||||
var actorType: ActorType? = null
|
||||
private set
|
||||
private var actorId: String? = null
|
||||
private var userId: String? = null
|
||||
private var iceConnectionState: IceConnectionState? = null
|
||||
var nick: String? = null
|
||||
get() = (if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(field)) defaultGuestNick else field)
|
||||
|
||||
var urlForAvatar: String? = null
|
||||
private set
|
||||
var mediaStream: MediaStream? = null
|
||||
private set
|
||||
var isStreamEnabled: Boolean = false
|
||||
private set
|
||||
var isAudioEnabled: Boolean = false
|
||||
private set
|
||||
var raisedHand: RaisedHand? = null
|
||||
private set
|
||||
var surfaceViewRenderer: SurfaceViewRenderer? = null
|
||||
|
||||
val sessionKey: String
|
||||
get() = "$session-$streamType"
|
||||
|
||||
interface Observer {
|
||||
fun onChange()
|
||||
}
|
||||
|
||||
private val callParticipantModelObserver: CallParticipantModel.Observer = object : CallParticipantModel.Observer {
|
||||
override fun onChange() {
|
||||
updateFromModel()
|
||||
}
|
||||
|
||||
override fun onReaction(reaction: String) {
|
||||
// unused
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
callParticipantModel.addObserver(callParticipantModelObserver, handler)
|
||||
|
||||
updateFromModel()
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
fun destroy() {
|
||||
callParticipantModel.removeObserver(callParticipantModelObserver)
|
||||
|
||||
surfaceViewRenderer?.let { renderer ->
|
||||
try {
|
||||
mediaStream?.videoTracks?.firstOrNull()?.removeSink(renderer)
|
||||
renderer.clearImage()
|
||||
renderer.release()
|
||||
(renderer.parent as? ViewGroup)?.removeView(renderer)
|
||||
} catch (e: Exception) {
|
||||
Log.w("ParticipantDisplayItem", "Error releasing renderer", e)
|
||||
}
|
||||
}
|
||||
surfaceViewRenderer = null
|
||||
}
|
||||
|
||||
private fun updateFromModel() {
|
||||
actorType = callParticipantModel.actorType
|
||||
actorId = callParticipantModel.actorId
|
||||
userId = callParticipantModel.userId
|
||||
nick = callParticipantModel.nick
|
||||
|
||||
updateUrlForAvatar()
|
||||
|
||||
if (streamType == "screen") {
|
||||
iceConnectionState = callParticipantModel.screenIceConnectionState
|
||||
mediaStream = callParticipantModel.screenMediaStream
|
||||
isAudioEnabled = true
|
||||
isStreamEnabled = true
|
||||
} else {
|
||||
iceConnectionState = callParticipantModel.iceConnectionState
|
||||
mediaStream = callParticipantModel.mediaStream
|
||||
isAudioEnabled = callParticipantModel.isAudioAvailable ?: false
|
||||
isStreamEnabled = callParticipantModel.isVideoAvailable ?: false
|
||||
}
|
||||
|
||||
raisedHand = callParticipantModel.raisedHand
|
||||
|
||||
if (surfaceViewRenderer == null && mediaStream != null) {
|
||||
val renderer = SurfaceViewRenderer(context).apply {
|
||||
init(rootEglBase.eglBaseContext, null)
|
||||
setEnableHardwareScaler(true)
|
||||
setMirror(false)
|
||||
}
|
||||
surfaceViewRenderer = renderer
|
||||
mediaStream?.videoTracks?.firstOrNull()?.addSink(renderer)
|
||||
}
|
||||
|
||||
_uiStateFlow.value = buildUiState()
|
||||
participantDisplayItemNotifier.notifyChange()
|
||||
}
|
||||
|
||||
private fun buildUiState(): ParticipantUiState =
|
||||
ParticipantUiState(
|
||||
sessionKey = sessionKey,
|
||||
nick = nick ?: "Guest",
|
||||
isConnected = isConnected,
|
||||
isAudioEnabled = isAudioEnabled,
|
||||
isStreamEnabled = isStreamEnabled,
|
||||
raisedHand = raisedHand?.state == true,
|
||||
avatarUrl = urlForAvatar,
|
||||
mediaStream = mediaStream
|
||||
)
|
||||
|
||||
private fun updateUrlForAvatar() {
|
||||
if (actorType == ActorType.FEDERATED) {
|
||||
val darkTheme = if (isDarkModeOn(context)) 1 else 0
|
||||
urlForAvatar = getUrlForFederatedAvatar(baseUrl, roomToken, actorId!!, darkTheme, true)
|
||||
} else if (!TextUtils.isEmpty(userId)) {
|
||||
urlForAvatar = getUrlForAvatar(baseUrl, userId, true)
|
||||
} else {
|
||||
urlForAvatar = getUrlForGuestAvatar(baseUrl, nick, true)
|
||||
}
|
||||
}
|
||||
|
||||
val isConnected: Boolean
|
||||
get() = iceConnectionState == IceConnectionState.CONNECTED ||
|
||||
iceConnectionState == IceConnectionState.COMPLETED ||
|
||||
// If there is no connection state that means that no connection is needed,
|
||||
// so it is a special case that is also seen as "connected".
|
||||
iceConnectionState == null
|
||||
|
||||
fun addObserver(observer: Observer?) {
|
||||
participantDisplayItemNotifier.addObserver(observer)
|
||||
}
|
||||
|
||||
fun removeObserver(observer: Observer?) {
|
||||
participantDisplayItemNotifier.removeObserver(observer)
|
||||
}
|
||||
|
||||
override fun toString(): String =
|
||||
"ParticipantSession{" +
|
||||
"userId='" + userId + '\'' +
|
||||
", actorType='" + actorType + '\'' +
|
||||
", actorId='" + actorId + '\'' +
|
||||
", session='" + session + '\'' +
|
||||
", nick='" + nick + '\'' +
|
||||
", urlForAvatar='" + urlForAvatar + '\'' +
|
||||
", mediaStream=" + mediaStream +
|
||||
", streamType='" + streamType + '\'' +
|
||||
", streamEnabled=" + isStreamEnabled +
|
||||
", rootEglBase=" + rootEglBase +
|
||||
", raisedHand=" + raisedHand +
|
||||
'}'
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Shared handler to receive change notifications from the model on the main thread.
|
||||
*/
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Helper class to register and notify ParticipantDisplayItem.Observers.
|
||||
* <p>
|
||||
* This class is only meant for internal use by ParticipantDisplayItem; observers must register themselves against a
|
||||
* ParticipantDisplayItem rather than against a ParticipantDisplayItemNotifier.
|
||||
*/
|
||||
class ParticipantDisplayItemNotifier {
|
||||
|
||||
private final Set<ParticipantDisplayItem.Observer> participantDisplayItemObservers = new LinkedHashSet<>();
|
||||
|
||||
public synchronized void addObserver(ParticipantDisplayItem.Observer observer) {
|
||||
if (observer == null) {
|
||||
throw new IllegalArgumentException("ParticipantDisplayItem.Observer can not be null");
|
||||
}
|
||||
|
||||
participantDisplayItemObservers.add(observer);
|
||||
}
|
||||
|
||||
public synchronized void removeObserver(ParticipantDisplayItem.Observer observer) {
|
||||
participantDisplayItemObservers.remove(observer);
|
||||
}
|
||||
|
||||
public synchronized void notifyChange() {
|
||||
for (ParticipantDisplayItem.Observer observer : new ArrayList<>(participantDisplayItemObservers)) {
|
||||
observer.onChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
|
||||
|
||||
interface PredefinedStatusClickListener {
|
||||
fun onClick(predefinedStatus: PredefinedStatus)
|
||||
fun revertStatus()
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.databinding.PredefinedStatusBinding
|
||||
import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
|
||||
|
||||
class PredefinedStatusListAdapter(
|
||||
private val clickListener: PredefinedStatusClickListener,
|
||||
val context: Context,
|
||||
var isBackupStatusAvailable: Boolean
|
||||
) : RecyclerView.Adapter<PredefinedStatusViewHolder>() {
|
||||
internal var list: List<PredefinedStatus> = emptyList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PredefinedStatusViewHolder {
|
||||
val itemBinding = PredefinedStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return PredefinedStatusViewHolder(itemBinding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PredefinedStatusViewHolder, position: Int) {
|
||||
holder.bind(list[position], clickListener, context, isBackupStatusAvailable)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = list.size
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.databinding.PredefinedStatusBinding
|
||||
import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
|
||||
private const val ONE_SECOND_IN_MILLIS = 1000
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(
|
||||
status: PredefinedStatus,
|
||||
clickListener: PredefinedStatusClickListener,
|
||||
context: Context,
|
||||
isBackupStatusAvailable: Boolean
|
||||
) {
|
||||
binding.root.setOnClickListener { clickListener.onClick(status) }
|
||||
binding.icon.text = status.icon
|
||||
binding.name.text = status.message
|
||||
|
||||
if (status.clearAt == null) {
|
||||
binding.clearAt.text = context.getString(R.string.dontClear)
|
||||
} else {
|
||||
val clearAt = status.clearAt!!
|
||||
if (clearAt.type.equals("period")) {
|
||||
binding.clearAt.text = DisplayUtils.getRelativeTimestamp(
|
||||
context,
|
||||
System.currentTimeMillis() + clearAt.time.toInt() * ONE_SECOND_IN_MILLIS,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
// end-of
|
||||
if (clearAt.time.equals("day")) {
|
||||
binding.clearAt.text = context.getString(R.string.today)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isBackupStatusAvailable) {
|
||||
binding.resetStatusButton.visibility = if (position == 0) View.VISIBLE else View.GONE
|
||||
if (position == 0) {
|
||||
binding.clearAt.text = context.getString(R.string.previously_set)
|
||||
}
|
||||
binding.resetStatusButton.setOnClickListener {
|
||||
clickListener.revertStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import com.nextcloud.talk.models.json.reactions.ReactionVoter
|
||||
|
||||
data class ReactionItem(val reactionVoter: ReactionVoter, val reaction: String?)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
interface ReactionItemClickListener {
|
||||
fun onClick(reactionItem: ReactionItem)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.ReactionItemBinding
|
||||
|
||||
class ReactionsAdapter(private val clickListener: ReactionItemClickListener, private val user: User?) :
|
||||
RecyclerView.Adapter<ReactionsViewHolder>() {
|
||||
internal var list: MutableList<ReactionItem> = ArrayList<ReactionItem>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReactionsViewHolder {
|
||||
val itemBinding = ReactionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ReactionsViewHolder(itemBinding, user)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ReactionsViewHolder, position: Int) {
|
||||
holder.bind(list[position], clickListener)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = list.size
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import android.text.TextUtils
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.ReactionItemBinding
|
||||
import com.nextcloud.talk.extensions.loadGuestAvatar
|
||||
import com.nextcloud.talk.extensions.loadUserAvatar
|
||||
import com.nextcloud.talk.models.json.reactions.ReactionVoter
|
||||
|
||||
class ReactionsViewHolder(private val binding: ReactionItemBinding, private val user: User?) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(reactionItem: ReactionItem, clickListener: ReactionItemClickListener) {
|
||||
binding.root.setOnClickListener { clickListener.onClick(reactionItem) }
|
||||
binding.reaction.text = reactionItem.reaction
|
||||
binding.name.text = reactionItem.reactionVoter.actorDisplayName
|
||||
|
||||
if (user != null && user.baseUrl?.isNotEmpty() == true) {
|
||||
loadAvatar(reactionItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAvatar(reactionItem: ReactionItem) {
|
||||
if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.GUESTS) {
|
||||
var displayName = sharedApplication?.resources?.getString(R.string.nc_guest)
|
||||
if (!TextUtils.isEmpty(reactionItem.reactionVoter.actorDisplayName)) {
|
||||
displayName = reactionItem.reactionVoter.actorDisplayName!!
|
||||
}
|
||||
binding.avatar.loadGuestAvatar(user!!.baseUrl!!, displayName!!, false)
|
||||
} else if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.USERS) {
|
||||
binding.avatar.loadUserAvatar(
|
||||
user!!,
|
||||
reactionItem.reactionVoter.actorId!!,
|
||||
false,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.accounts.Account
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.core.net.toUri
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.adapters.items.AdvancedUserItem.UserItemViewHolder
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.AccountItemBinding
|
||||
import com.nextcloud.talk.extensions.loadUserAvatar
|
||||
import com.nextcloud.talk.models.json.participants.Participant
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class AdvancedUserItem(
|
||||
/**
|
||||
* @return the model object
|
||||
*/
|
||||
val model: Participant,
|
||||
@JvmField val user: User?,
|
||||
val account: Account?,
|
||||
private val viewThemeUtils: ViewThemeUtils,
|
||||
private val actionRequiredCount: Int
|
||||
) : AbstractFlexibleItem<UserItemViewHolder>(),
|
||||
IFilterable<String?> {
|
||||
|
||||
override fun equals(other: Any?): Boolean =
|
||||
if (other is AdvancedUserItem) {
|
||||
model == other.model
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = model.hashCode()
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.account_item
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View?,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
|
||||
): UserItemViewHolder = UserItemViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: UserItemViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (adapter.hasFilter()) {
|
||||
viewThemeUtils.talk.themeAndHighlightText(
|
||||
holder.binding.userName,
|
||||
model.displayName,
|
||||
adapter.getFilter(String::class.java).toString()
|
||||
)
|
||||
} else {
|
||||
holder.binding.userName.text = model.displayName
|
||||
}
|
||||
if (user != null && !TextUtils.isEmpty(user.baseUrl)) {
|
||||
val host = user.baseUrl!!.toUri().host
|
||||
if (!TextUtils.isEmpty(host)) {
|
||||
holder.binding.account.text = user.baseUrl!!.toUri().host
|
||||
} else {
|
||||
holder.binding.account.text = user.baseUrl
|
||||
}
|
||||
}
|
||||
if (user?.baseUrl != null &&
|
||||
(user.baseUrl!!.startsWith("http://") || user.baseUrl!!.startsWith("https://"))
|
||||
) {
|
||||
holder.binding.userIcon.loadUserAvatar(user, model.calculatedActorId!!, true, false)
|
||||
}
|
||||
if (actionRequiredCount > 0) {
|
||||
holder.binding.actionRequired.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.binding.actionRequired.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean =
|
||||
model.displayName != null &&
|
||||
Pattern
|
||||
.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.displayName!!.trim())
|
||||
.find()
|
||||
|
||||
class UserItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) {
|
||||
var binding: AccountItemBinding
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*/
|
||||
init {
|
||||
binding = AccountItemBinding.bind(view!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.adapters.items.ContactItem.ContactItemViewHolder
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.RvItemContactBinding
|
||||
import com.nextcloud.talk.extensions.loadUserAvatar
|
||||
import com.nextcloud.talk.models.json.participants.Participant
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import java.util.Objects
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class ContactItem(
|
||||
/**
|
||||
* @return the model object
|
||||
*/
|
||||
val model: Participant,
|
||||
private val user: User,
|
||||
private var header: GenericTextHeaderItem?,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) : AbstractFlexibleItem<ContactItemViewHolder?>(),
|
||||
ISectionable<ContactItemViewHolder?, GenericTextHeaderItem?>,
|
||||
IFilterable<String?> {
|
||||
var isOnline: Boolean = true
|
||||
|
||||
override fun equals(o: Any?): Boolean {
|
||||
if (o is ContactItem) {
|
||||
return model.calculatedActorType == o.model.calculatedActorType &&
|
||||
model.calculatedActorId == o.model.calculatedActorId
|
||||
}
|
||||
return false
|
||||
}
|
||||
override fun hashCode(): Int = model.hashCode()
|
||||
|
||||
override fun filter(constraint: String?): Boolean =
|
||||
model.displayName != null &&
|
||||
(
|
||||
Pattern.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.displayName!!.trim())
|
||||
.find() ||
|
||||
Pattern.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.calculatedActorId!!.trim())
|
||||
.find()
|
||||
)
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_contact
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View?,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
|
||||
): ContactItemViewHolder = ContactItemViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?,
|
||||
holder: ContactItemViewHolder?,
|
||||
position: Int,
|
||||
payloads: List<Any>?
|
||||
) {
|
||||
if (model.selected) {
|
||||
holder?.binding?.checkedImageView?.let { viewThemeUtils.platform.colorImageView(it) }
|
||||
holder?.binding?.checkedImageView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder?.binding?.checkedImageView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (!isOnline) {
|
||||
holder?.binding?.nameText?.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
holder.binding.nameText.context.resources,
|
||||
R.color.medium_emphasis_text,
|
||||
null
|
||||
)
|
||||
)
|
||||
holder?.binding?.avatarView?.alpha = SEMI_TRANSPARENT
|
||||
} else {
|
||||
holder?.binding?.nameText?.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
holder.binding.nameText.context.resources,
|
||||
R.color.high_emphasis_text,
|
||||
null
|
||||
)
|
||||
)
|
||||
holder?.binding?.avatarView?.alpha = FULLY_OPAQUE
|
||||
}
|
||||
|
||||
holder?.binding?.nameText?.text = model.displayName
|
||||
|
||||
if (adapter != null) {
|
||||
if (adapter.hasFilter()) {
|
||||
holder?.binding?.let {
|
||||
viewThemeUtils.talk.themeAndHighlightText(
|
||||
it.nameText,
|
||||
model.displayName,
|
||||
adapter.getFilter(String::class.java).toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(model.displayName) &&
|
||||
(
|
||||
model.type == Participant.ParticipantType.GUEST ||
|
||||
model.type == Participant.ParticipantType.USER_FOLLOWING_LINK
|
||||
)
|
||||
) {
|
||||
holder?.binding?.nameText?.text = sharedApplication!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
setAvatar(holder)
|
||||
}
|
||||
|
||||
private fun setAvatar(holder: ContactItemViewHolder?) {
|
||||
if (model.calculatedActorType == Participant.ActorType.GROUPS ||
|
||||
model.calculatedActorType == Participant.ActorType.CIRCLES
|
||||
) {
|
||||
setGenericAvatar(holder!!, R.drawable.ic_avatar_group)
|
||||
} else if (model.calculatedActorType == Participant.ActorType.EMAILS) {
|
||||
setGenericAvatar(holder!!, R.drawable.ic_avatar_mail)
|
||||
} else if (model.calculatedActorType == Participant.ActorType.GUESTS ||
|
||||
model.type == Participant.ParticipantType.GUEST ||
|
||||
model.type == Participant.ParticipantType.GUEST_MODERATOR
|
||||
) {
|
||||
var displayName: String?
|
||||
|
||||
displayName = if (!TextUtils.isEmpty(model.displayName)) {
|
||||
model.displayName
|
||||
} else {
|
||||
Objects.requireNonNull(sharedApplication)!!.resources!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
// absolute fallback to prevent NPE deference
|
||||
if (displayName == null) {
|
||||
displayName = "Guest"
|
||||
}
|
||||
|
||||
holder?.binding?.avatarView?.loadUserAvatar(user, displayName, true, false)
|
||||
} else if (model.calculatedActorType == Participant.ActorType.USERS) {
|
||||
holder?.binding?.avatarView
|
||||
?.loadUserAvatar(
|
||||
user,
|
||||
model.calculatedActorId!!,
|
||||
true,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setGenericAvatar(holder: ContactItemViewHolder, roundPlaceholderDrawable: Int) {
|
||||
val avatar =
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.avatarView,
|
||||
roundPlaceholderDrawable
|
||||
)
|
||||
|
||||
holder.binding.avatarView.loadUserAvatar(avatar)
|
||||
}
|
||||
|
||||
override fun getHeader(): GenericTextHeaderItem? = header
|
||||
|
||||
override fun setHeader(p0: GenericTextHeaderItem?) {
|
||||
this.header = header
|
||||
}
|
||||
|
||||
class ContactItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemContactBinding =
|
||||
RvItemContactBinding.bind(view!!)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FULLY_OPAQUE: Float = 1.0f
|
||||
private const val SEMI_TRANSPARENT: Float = 0.38f
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,507 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.TextUtils
|
||||
import android.text.format.DateUtils
|
||||
import android.text.style.ImageSpan
|
||||
import android.view.View
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import coil.dispose
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.adapters.items.ConversationItem.ConversationItemViewHolder
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage.MessageType
|
||||
import com.nextcloud.talk.data.database.mappers.asModel
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.RvItemConversationWithLastMessageBinding
|
||||
import com.nextcloud.talk.extensions.loadConversationAvatar
|
||||
import com.nextcloud.talk.extensions.loadNoteToSelfAvatar
|
||||
import com.nextcloud.talk.extensions.loadSystemAvatar
|
||||
import com.nextcloud.talk.extensions.loadUserAvatar
|
||||
import com.nextcloud.talk.models.domain.ConversationModel
|
||||
import com.nextcloud.talk.models.json.conversations.ConversationEnums
|
||||
import com.nextcloud.talk.ui.StatusDrawable
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.SpreedFeatures
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class ConversationItem(
|
||||
val model: ConversationModel,
|
||||
private val user: User,
|
||||
private val context: Context,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) : AbstractFlexibleItem<ConversationItemViewHolder>(),
|
||||
ISectionable<ConversationItemViewHolder, GenericTextHeaderItem?>,
|
||||
IFilterable<String?> {
|
||||
private var header: GenericTextHeaderItem? = null
|
||||
private val chatMessage = model.lastMessage?.asModel()
|
||||
var mHolder: ConversationItemViewHolder? = null
|
||||
|
||||
constructor(
|
||||
conversation: ConversationModel,
|
||||
user: User,
|
||||
activityContext: Context,
|
||||
genericTextHeaderItem: GenericTextHeaderItem?,
|
||||
viewThemeUtils: ViewThemeUtils
|
||||
) : this(conversation, user, activityContext, viewThemeUtils) {
|
||||
header = genericTextHeaderItem
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is ConversationItem) {
|
||||
return model == other.model
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = model.hashCode()
|
||||
result *= 31
|
||||
return result
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_conversation_with_last_message
|
||||
|
||||
override fun getItemViewType(): Int = VIEW_TYPE
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>?>?): ConversationItemViewHolder =
|
||||
ConversationItemViewHolder(view, adapter)
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<*>?>,
|
||||
holder: ConversationItemViewHolder,
|
||||
position: Int,
|
||||
payloads: List<Any>
|
||||
) {
|
||||
mHolder = holder
|
||||
val appContext = sharedApplication!!.applicationContext
|
||||
holder.binding.dialogName.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
context.resources,
|
||||
R.color.conversation_item_header,
|
||||
null
|
||||
)
|
||||
)
|
||||
if (adapter.hasFilter()) {
|
||||
viewThemeUtils.platform.highlightText(
|
||||
holder.binding.dialogName,
|
||||
model.displayName!!,
|
||||
adapter.getFilter(String::class.java).toString()
|
||||
)
|
||||
} else {
|
||||
holder.binding.dialogName.text = model.displayName
|
||||
}
|
||||
if (model.unreadMessages > 0) {
|
||||
showUnreadMessages(holder)
|
||||
} else {
|
||||
holder.binding.dialogName.setTypeface(null, Typeface.NORMAL)
|
||||
holder.binding.dialogDate.setTypeface(null, Typeface.NORMAL)
|
||||
holder.binding.dialogLastMessage.setTypeface(null, Typeface.NORMAL)
|
||||
holder.binding.dialogUnreadBubble.visibility = View.GONE
|
||||
}
|
||||
if (model.favorite) {
|
||||
holder.binding.favoriteConversationImageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.binding.favoriteConversationImageView.visibility = View.GONE
|
||||
}
|
||||
if (ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == model.type) {
|
||||
holder.binding.publicCallBadge.setImageResource(R.drawable.ic_avatar_link)
|
||||
holder.binding.publicCallBadge.visibility = View.VISIBLE
|
||||
} else if (model.remoteServer?.isNotEmpty() == true) {
|
||||
holder.binding.publicCallBadge.setImageResource(R.drawable.ic_avatar_federation)
|
||||
holder.binding.publicCallBadge.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.binding.publicCallBadge.visibility = View.GONE
|
||||
}
|
||||
if (ConversationEnums.ConversationType.ROOM_SYSTEM !== model.type) {
|
||||
val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, appContext)
|
||||
holder.binding.userStatusImage.visibility = View.VISIBLE
|
||||
holder.binding.userStatusImage.setImageDrawable(
|
||||
StatusDrawable(
|
||||
model.status,
|
||||
model.statusIcon,
|
||||
size,
|
||||
context.resources.getColor(R.color.bg_default, null),
|
||||
appContext
|
||||
)
|
||||
)
|
||||
} else {
|
||||
holder.binding.userStatusImage.visibility = View.GONE
|
||||
}
|
||||
|
||||
val dialogNameParams = holder.binding.dialogName.layoutParams as RelativeLayout.LayoutParams
|
||||
val unreadBubbleParams = holder.binding.dialogUnreadBubble.layoutParams as RelativeLayout.LayoutParams
|
||||
val relativeLayoutParams = holder.binding.relativeLayout.layoutParams as RelativeLayout.LayoutParams
|
||||
|
||||
if (model.hasSensitive == true) {
|
||||
dialogNameParams.addRule(RelativeLayout.CENTER_VERTICAL)
|
||||
relativeLayoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.dialogAvatarFrameLayout)
|
||||
dialogNameParams.marginEnd =
|
||||
context.resources.getDimensionPixelSize(R.dimen.standard_double_padding)
|
||||
unreadBubbleParams.topMargin =
|
||||
context.resources.getDimensionPixelSize(R.dimen.double_margin_between_elements)
|
||||
unreadBubbleParams.addRule(RelativeLayout.CENTER_VERTICAL)
|
||||
} else {
|
||||
dialogNameParams.removeRule(RelativeLayout.CENTER_VERTICAL)
|
||||
relativeLayoutParams.removeRule(RelativeLayout.ALIGN_TOP)
|
||||
dialogNameParams.marginEnd = 0
|
||||
unreadBubbleParams.topMargin = 0
|
||||
unreadBubbleParams.removeRule(RelativeLayout.CENTER_VERTICAL)
|
||||
}
|
||||
holder.binding.relativeLayout.layoutParams = relativeLayoutParams
|
||||
holder.binding.dialogUnreadBubble.layoutParams = unreadBubbleParams
|
||||
holder.binding.dialogName.layoutParams = dialogNameParams
|
||||
|
||||
setLastMessage(holder, appContext)
|
||||
showAvatar(holder)
|
||||
}
|
||||
|
||||
private fun showAvatar(holder: ConversationItemViewHolder) {
|
||||
holder.binding.dialogAvatar.dispose()
|
||||
holder.binding.dialogAvatar.visibility = View.VISIBLE
|
||||
|
||||
var shouldLoadAvatar = shouldLoadAvatar(holder)
|
||||
if (ConversationEnums.ConversationType.ROOM_SYSTEM == model.type) {
|
||||
holder.binding.dialogAvatar.loadSystemAvatar()
|
||||
shouldLoadAvatar = false
|
||||
}
|
||||
if (shouldLoadAvatar) {
|
||||
when (model.type) {
|
||||
ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> {
|
||||
if (!TextUtils.isEmpty(model.name)) {
|
||||
holder.binding.dialogAvatar.loadUserAvatar(
|
||||
user,
|
||||
model.name!!,
|
||||
true,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
holder.binding.dialogAvatar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
ConversationEnums.ConversationType.ROOM_GROUP_CALL,
|
||||
ConversationEnums.ConversationType.FORMER_ONE_TO_ONE,
|
||||
ConversationEnums.ConversationType.ROOM_PUBLIC_CALL ->
|
||||
holder.binding.dialogAvatar.loadConversationAvatar(user, model, false, viewThemeUtils)
|
||||
|
||||
ConversationEnums.ConversationType.NOTE_TO_SELF ->
|
||||
holder.binding.dialogAvatar.loadNoteToSelfAvatar()
|
||||
|
||||
else -> holder.binding.dialogAvatar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldLoadAvatar(holder: ConversationItemViewHolder): Boolean =
|
||||
when (model.objectType) {
|
||||
ConversationEnums.ObjectType.SHARE_PASSWORD -> {
|
||||
holder.binding.dialogAvatar.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
context,
|
||||
R.drawable.ic_circular_lock
|
||||
)
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
ConversationEnums.ObjectType.FILE -> {
|
||||
holder.binding.dialogAvatar.loadUserAvatar(
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.dialogAvatar,
|
||||
R.drawable.ic_avatar_document
|
||||
)
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
else -> true
|
||||
}
|
||||
|
||||
private fun setLastMessage(holder: ConversationItemViewHolder, appContext: Context) {
|
||||
if (chatMessage != null) {
|
||||
holder.binding.dialogDate.visibility = View.VISIBLE
|
||||
holder.binding.dialogDate.text = DateUtils.getRelativeTimeSpanString(
|
||||
model.lastActivity * MILLIES,
|
||||
System.currentTimeMillis(),
|
||||
0,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE
|
||||
)
|
||||
if (!TextUtils.isEmpty(chatMessage?.systemMessage) ||
|
||||
ConversationEnums.ConversationType.ROOM_SYSTEM === model.type
|
||||
) {
|
||||
holder.binding.dialogLastMessage.text = chatMessage.text
|
||||
} else {
|
||||
chatMessage?.activeUser = user
|
||||
|
||||
val text =
|
||||
if (
|
||||
chatMessage?.getCalculateMessageType() == MessageType.REGULAR_TEXT_MESSAGE
|
||||
) {
|
||||
calculateRegularLastMessageText(appContext)
|
||||
} else {
|
||||
lastMessageDisplayText
|
||||
}
|
||||
holder.binding.dialogLastMessage.text = text
|
||||
}
|
||||
} else {
|
||||
holder.binding.dialogDate.visibility = View.GONE
|
||||
holder.binding.dialogLastMessage.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateRegularLastMessageText(appContext: Context): CharSequence =
|
||||
if (chatMessage?.actorId == user.userId) {
|
||||
String.format(
|
||||
appContext.getString(R.string.nc_formatted_message_you),
|
||||
lastMessageDisplayText
|
||||
)
|
||||
} else if (model.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
|
||||
lastMessageDisplayText
|
||||
} else {
|
||||
val actorName = chatMessage?.actorDisplayName
|
||||
val authorDisplayName = if (!actorName.isNullOrBlank()) {
|
||||
actorName
|
||||
} else if ("guests" == chatMessage?.actorType || "emails" == chatMessage?.actorType) {
|
||||
appContext.getString(R.string.nc_guest)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
String.format(
|
||||
appContext.getString(R.string.nc_formatted_message),
|
||||
authorDisplayName,
|
||||
lastMessageDisplayText
|
||||
)
|
||||
}
|
||||
|
||||
private fun showUnreadMessages(holder: ConversationItemViewHolder) {
|
||||
holder.binding.dialogName.setTypeface(holder.binding.dialogName.typeface, Typeface.BOLD)
|
||||
holder.binding.dialogLastMessage.setTypeface(holder.binding.dialogLastMessage.typeface, Typeface.BOLD)
|
||||
holder.binding.dialogUnreadBubble.visibility = View.VISIBLE
|
||||
if (model.unreadMessages < UNREAD_MESSAGES_TRESHOLD) {
|
||||
holder.binding.dialogUnreadBubble.text = model.unreadMessages.toLong().toString()
|
||||
} else {
|
||||
holder.binding.dialogUnreadBubble.setText(R.string.tooManyUnreadMessages)
|
||||
}
|
||||
val lightBubbleFillColor = ColorStateList.valueOf(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.conversation_unread_bubble
|
||||
)
|
||||
)
|
||||
val lightBubbleTextColor = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.conversation_unread_bubble_text
|
||||
)
|
||||
if (model.type === ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
|
||||
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
|
||||
} else if (model.unreadMention) {
|
||||
if (hasSpreedFeatureCapability(user.capabilities?.spreedCapability!!, SpreedFeatures.DIRECT_MENTION_FLAG)) {
|
||||
if (model.unreadMentionDirect!!) {
|
||||
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
|
||||
} else {
|
||||
viewThemeUtils.material.colorChipOutlined(
|
||||
holder.binding.dialogUnreadBubble,
|
||||
UNREAD_BUBBLE_STROKE_WIDTH
|
||||
)
|
||||
}
|
||||
} else {
|
||||
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
|
||||
}
|
||||
} else {
|
||||
holder.binding.dialogUnreadBubble.chipBackgroundColor = lightBubbleFillColor
|
||||
holder.binding.dialogUnreadBubble.setTextColor(lightBubbleTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean =
|
||||
model.displayName != null &&
|
||||
Pattern
|
||||
.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.displayName.trim())
|
||||
.find()
|
||||
|
||||
override fun getHeader(): GenericTextHeaderItem? = header
|
||||
|
||||
override fun setHeader(header: GenericTextHeaderItem?) {
|
||||
this.header = header
|
||||
}
|
||||
|
||||
private val lastMessageDisplayText: CharSequence
|
||||
get() {
|
||||
if (chatMessage?.getCalculateMessageType() == MessageType.REGULAR_TEXT_MESSAGE ||
|
||||
chatMessage?.getCalculateMessageType() == MessageType.SYSTEM_MESSAGE ||
|
||||
chatMessage?.getCalculateMessageType() == MessageType.SINGLE_LINK_MESSAGE
|
||||
) {
|
||||
return chatMessage.text
|
||||
} else {
|
||||
if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == chatMessage?.getCalculateMessageType() ||
|
||||
MessageType.SINGLE_LINK_TENOR_MESSAGE == chatMessage?.getCalculateMessageType() ||
|
||||
MessageType.SINGLE_LINK_GIF_MESSAGE == chatMessage?.getCalculateMessageType()
|
||||
) {
|
||||
return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) {
|
||||
sharedApplication!!.getString(R.string.nc_sent_a_gif_you)
|
||||
} else {
|
||||
String.format(
|
||||
sharedApplication!!.resources.getString(R.string.nc_sent_a_gif),
|
||||
chatMessage?.getNullsafeActorDisplayName()
|
||||
)
|
||||
}
|
||||
} else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == chatMessage?.getCalculateMessageType()) {
|
||||
var locationName = chatMessage.messageParameters?.get("object")?.get("name") ?: ""
|
||||
val author = authorName(chatMessage)
|
||||
val lastMessage =
|
||||
setLastNameForAttachmentMessage(author, R.drawable.baseline_location_pin_24, locationName)
|
||||
return lastMessage
|
||||
} else if (MessageType.VOICE_MESSAGE == chatMessage?.getCalculateMessageType()) {
|
||||
var voiceMessageName = chatMessage.messageParameters?.get("file")?.get("name") ?: ""
|
||||
val author = authorName(chatMessage)
|
||||
val lastMessage = setLastNameForAttachmentMessage(
|
||||
author,
|
||||
R.drawable.baseline_mic_24,
|
||||
voiceMessageName
|
||||
)
|
||||
return lastMessage
|
||||
} else if (MessageType.SINGLE_LINK_AUDIO_MESSAGE == chatMessage?.getCalculateMessageType()) {
|
||||
return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) {
|
||||
sharedApplication!!.getString(R.string.nc_sent_an_audio_you)
|
||||
} else {
|
||||
String.format(
|
||||
sharedApplication!!.resources.getString(R.string.nc_sent_an_audio),
|
||||
chatMessage?.getNullsafeActorDisplayName()
|
||||
)
|
||||
}
|
||||
} else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == chatMessage?.getCalculateMessageType()) {
|
||||
return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) {
|
||||
sharedApplication!!.getString(R.string.nc_sent_a_video_you)
|
||||
} else {
|
||||
String.format(
|
||||
sharedApplication!!.resources.getString(R.string.nc_sent_a_video),
|
||||
chatMessage?.getNullsafeActorDisplayName()
|
||||
)
|
||||
}
|
||||
} else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == chatMessage?.getCalculateMessageType()) {
|
||||
return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) {
|
||||
sharedApplication!!.getString(R.string.nc_sent_an_image_you)
|
||||
} else {
|
||||
String.format(
|
||||
sharedApplication!!.resources.getString(R.string.nc_sent_an_image),
|
||||
chatMessage?.getNullsafeActorDisplayName()
|
||||
)
|
||||
}
|
||||
} else if (MessageType.POLL_MESSAGE == chatMessage?.getCalculateMessageType()) {
|
||||
var pollMessageTitle = chatMessage.messageParameters?.get("object")?.get("name") ?: ""
|
||||
val author = authorName(chatMessage)
|
||||
val lastMessage = setLastNameForAttachmentMessage(
|
||||
author,
|
||||
R.drawable.baseline_bar_chart_24,
|
||||
pollMessageTitle
|
||||
)
|
||||
return lastMessage
|
||||
} else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == chatMessage?.getCalculateMessageType()) {
|
||||
var attachmentName = chatMessage.text
|
||||
if (attachmentName == "{file}") {
|
||||
attachmentName = chatMessage.messageParameters?.get("file")?.get("name") ?: ""
|
||||
}
|
||||
val author = authorName(chatMessage)
|
||||
|
||||
val drawable = chatMessage.messageParameters?.get("file")?.get("mimetype")?.let {
|
||||
when {
|
||||
it.contains("image") -> R.drawable.baseline_image_24
|
||||
it.contains("video") -> R.drawable.baseline_video_24
|
||||
it.contains("application") -> R.drawable.baseline_insert_drive_file_24
|
||||
it.contains("audio") -> R.drawable.baseline_audiotrack_24
|
||||
it.contains("text/vcard") -> R.drawable.baseline_contacts_24
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
val lastMessage = setLastNameForAttachmentMessage(author, drawable, attachmentName!!)
|
||||
return lastMessage
|
||||
} else if (MessageType.DECK_CARD == chatMessage?.getCalculateMessageType()) {
|
||||
var deckTitle = chatMessage.messageParameters?.get("object")?.get("name") ?: ""
|
||||
val author = authorName(chatMessage)
|
||||
val lastMessage = setLastNameForAttachmentMessage(author, R.drawable.baseline_article_24, deckTitle)
|
||||
return lastMessage
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fun authorName(chatMessage: ChatMessage): String {
|
||||
val name = if (chatMessage.actorId == chatMessage.activeUser!!.userId) {
|
||||
sharedApplication!!.resources.getString(R.string.nc_current_user)
|
||||
} else {
|
||||
chatMessage.getNullsafeActorDisplayName()?.let { "$it:" } ?: ""
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
fun setLastNameForAttachmentMessage(actor: String, icon: Int?, attachmentName: String): SpannableStringBuilder {
|
||||
val builder = SpannableStringBuilder()
|
||||
builder.append(actor)
|
||||
|
||||
val drawable = icon?.let { it -> ContextCompat.getDrawable(context, it) }
|
||||
if (drawable != null) {
|
||||
viewThemeUtils.platform.colorDrawable(
|
||||
drawable,
|
||||
context.resources.getColor(R.color.low_emphasis_text, null)
|
||||
)
|
||||
val desiredWidth = (drawable.intrinsicWidth * IMAGE_SCALE_FACTOR).toInt()
|
||||
val desiredHeight = (drawable.intrinsicHeight * IMAGE_SCALE_FACTOR).toInt()
|
||||
drawable.setBounds(0, 0, desiredWidth, desiredHeight)
|
||||
|
||||
val imageSpan = ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM)
|
||||
val startImage = builder.length
|
||||
builder.append(" ")
|
||||
builder.setSpan(imageSpan, startImage, startImage + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
} else {
|
||||
builder.append(" ")
|
||||
}
|
||||
builder.append(attachmentName)
|
||||
return builder
|
||||
}
|
||||
|
||||
class ConversationItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemConversationWithLastMessageBinding
|
||||
|
||||
init {
|
||||
binding = RvItemConversationWithLastMessageBinding.bind(view!!)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = FlexibleItemViewType.CONVERSATION_ITEM
|
||||
private const val MILLIES = 1000L
|
||||
private const val STATUS_SIZE_IN_DP = 9f
|
||||
private const val UNREAD_BUBBLE_STROKE_WIDTH = 6.0f
|
||||
private const val UNREAD_MESSAGES_TRESHOLD = 1000
|
||||
private const val IMAGE_SCALE_FACTOR = 0.7f
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
object FlexibleItemViewType {
|
||||
const val CONVERSATION_ITEM: Int = 1120391230
|
||||
const val LOAD_MORE_RESULTS_ITEM: Int = 1120391231
|
||||
const val MESSAGE_RESULT_ITEM: Int = 1120391232
|
||||
const val MESSAGES_TEXT_HEADER_ITEM: Int = 1120391233
|
||||
const val POLL_RESULT_HEADER_ITEM: Int = 1120391234
|
||||
const val POLL_RESULT_VOTER_ITEM: Int = 1120391235
|
||||
const val POLL_RESULT_VOTERS_OVERVIEW_ITEM: Int = 1120391236
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.databinding.RvItemTitleHeaderBinding
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import java.util.Objects
|
||||
|
||||
open class GenericTextHeaderItem(title: String, viewThemeUtils: ViewThemeUtils) :
|
||||
AbstractHeaderItem<GenericTextHeaderItem.HeaderViewHolder>() {
|
||||
val model: String
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
|
||||
init {
|
||||
isHidden = false
|
||||
isSelectable = false
|
||||
this.model = title
|
||||
this.viewThemeUtils = viewThemeUtils
|
||||
}
|
||||
|
||||
override fun equals(o: Any?): Boolean {
|
||||
if (o is GenericTextHeaderItem) {
|
||||
return model == o.model
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(model)
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_title_header
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View?,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
|
||||
): HeaderViewHolder = HeaderViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<*>?>?,
|
||||
holder: HeaderViewHolder,
|
||||
position: Int,
|
||||
payloads: List<Any>
|
||||
) {
|
||||
if (payloads.size > 0) {
|
||||
Log.d(TAG, "We have payloads, so ignoring!")
|
||||
} else {
|
||||
holder.binding.titleTextView.text = model
|
||||
viewThemeUtils.platform.colorPrimaryTextViewElement(holder.binding.titleTextView)
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter, true) {
|
||||
var binding: RvItemTitleHeaderBinding =
|
||||
RvItemTitleHeaderBinding.bind(view!!)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GenericTextHeaderItem"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.databinding.RvItemLoadMoreBinding
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
|
||||
object LoadMoreResultsItem :
|
||||
AbstractFlexibleItem<LoadMoreResultsItem.ViewHolder>(),
|
||||
IFilterable<String> {
|
||||
|
||||
// layout is used as view type for uniqueness
|
||||
const val VIEW_TYPE = FlexibleItemViewType.LOAD_MORE_RESULTS_ITEM
|
||||
|
||||
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemLoadMoreBinding = RvItemLoadMoreBinding.bind(view)
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_load_more
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
|
||||
): ViewHolder = ViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>?
|
||||
) {
|
||||
// nothing, it's immutable
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean = true
|
||||
|
||||
override fun getItemViewType(): Int = VIEW_TYPE
|
||||
|
||||
override fun equals(other: Any?): Boolean = other is LoadMoreResultsItem
|
||||
|
||||
override fun hashCode(): Int = 0
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021-2022 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import com.nextcloud.talk.PhoneUtils.isPhoneNumber
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.adapters.items.ParticipantItem.ParticipantItemViewHolder
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.extensions.loadDefaultAvatar
|
||||
import com.nextcloud.talk.extensions.loadFederatedUserAvatar
|
||||
import com.nextcloud.talk.extensions.loadGuestAvatar
|
||||
import com.nextcloud.talk.extensions.loadUserAvatar
|
||||
import com.nextcloud.talk.models.json.mention.Mention
|
||||
import com.nextcloud.talk.models.json.status.StatusType
|
||||
import com.nextcloud.talk.ui.StatusDrawable
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import java.util.Objects
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class MentionAutocompleteItem(
|
||||
mention: Mention,
|
||||
private val currentUser: User,
|
||||
private val context: Context,
|
||||
@JvmField val roomToken: String,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) : AbstractFlexibleItem<ParticipantItemViewHolder>(),
|
||||
IFilterable<String?> {
|
||||
@JvmField
|
||||
var source: String?
|
||||
|
||||
@JvmField
|
||||
val mentionId: String?
|
||||
|
||||
@JvmField
|
||||
val objectId: String?
|
||||
|
||||
@JvmField
|
||||
val displayName: String?
|
||||
private val status: String?
|
||||
private val statusIcon: String?
|
||||
private val statusMessage: String?
|
||||
|
||||
init {
|
||||
mentionId = mention.mentionId
|
||||
objectId = mention.id
|
||||
|
||||
displayName = if (!mention.label.isNullOrBlank()) {
|
||||
mention.label
|
||||
} else if ("guests" == mention.source || "emails" == mention.source) {
|
||||
context.resources.getString(R.string.nc_guest)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
source = mention.source
|
||||
status = mention.status
|
||||
statusIcon = mention.statusIcon
|
||||
statusMessage = mention.statusMessage
|
||||
}
|
||||
|
||||
override fun equals(o: Any?): Boolean =
|
||||
if (o is MentionAutocompleteItem) {
|
||||
objectId == o.objectId && displayName == o.displayName
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(objectId, displayName)
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_conversation_info_participant
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>?>?): ParticipantItemViewHolder =
|
||||
ParticipantItemViewHolder(view, adapter)
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<*>?>,
|
||||
holder: ParticipantItemViewHolder,
|
||||
position: Int,
|
||||
payloads: List<Any>
|
||||
) {
|
||||
holder.binding.nameText.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
context.resources,
|
||||
R.color.conversation_item_header,
|
||||
null
|
||||
)
|
||||
)
|
||||
if (adapter.hasFilter()) {
|
||||
viewThemeUtils.talk.themeAndHighlightText(
|
||||
holder.binding.nameText,
|
||||
displayName,
|
||||
adapter.getFilter(String::class.java).toString()
|
||||
)
|
||||
viewThemeUtils.talk.themeAndHighlightText(
|
||||
holder.binding.secondaryText,
|
||||
"@$objectId",
|
||||
adapter.getFilter(String::class.java).toString()
|
||||
)
|
||||
} else {
|
||||
holder.binding.nameText.text = displayName
|
||||
}
|
||||
setAvatar(holder, objectId)
|
||||
drawStatus(holder)
|
||||
}
|
||||
|
||||
private fun setAvatar(holder: ParticipantItemViewHolder, objectId: String?) {
|
||||
when (source) {
|
||||
SOURCE_CALLS -> {
|
||||
run {
|
||||
if (isPhoneNumber(displayName)) {
|
||||
holder.binding.avatarView.loadUserAvatar(
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.avatarView,
|
||||
R.drawable.ic_phone_small
|
||||
)
|
||||
)
|
||||
} else {
|
||||
holder.binding.avatarView.loadUserAvatar(
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.avatarView,
|
||||
R.drawable.ic_avatar_group_small
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SOURCE_GROUPS -> {
|
||||
holder.binding.avatarView.loadUserAvatar(
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.avatarView,
|
||||
R.drawable
|
||||
.ic_avatar_group_small
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SOURCE_FEDERATION -> {
|
||||
val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0
|
||||
holder.binding.avatarView.loadFederatedUserAvatar(
|
||||
currentUser,
|
||||
currentUser.baseUrl!!,
|
||||
roomToken,
|
||||
objectId!!,
|
||||
darkTheme,
|
||||
requestBigSize = true,
|
||||
ignoreCache = false
|
||||
)
|
||||
}
|
||||
|
||||
SOURCE_GUESTS, SOURCE_EMAILS -> {
|
||||
if (displayName.equals(context.resources.getString(R.string.nc_guest))) {
|
||||
holder.binding.avatarView.loadDefaultAvatar(viewThemeUtils)
|
||||
} else {
|
||||
holder.binding.avatarView.loadGuestAvatar(currentUser, displayName!!, false)
|
||||
}
|
||||
}
|
||||
|
||||
SOURCE_TEAMS -> {
|
||||
holder.binding.avatarView.loadUserAvatar(
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.avatarView,
|
||||
R.drawable
|
||||
.ic_avatar_team_small
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
holder.binding.avatarView.loadUserAvatar(
|
||||
currentUser,
|
||||
objectId!!,
|
||||
requestBigSize = true,
|
||||
ignoreCache = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawStatus(holder: ParticipantItemViewHolder) {
|
||||
val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, context)
|
||||
holder.binding.userStatusImage.setImageDrawable(
|
||||
StatusDrawable(
|
||||
status,
|
||||
NO_ICON,
|
||||
size,
|
||||
context.resources.getColor(R.color.bg_default),
|
||||
context
|
||||
)
|
||||
)
|
||||
if (statusMessage != null) {
|
||||
holder.binding.conversationInfoStatusMessage.text = statusMessage
|
||||
alignUsernameVertical(holder, 0f)
|
||||
} else {
|
||||
holder.binding.conversationInfoStatusMessage.text = ""
|
||||
alignUsernameVertical(holder, NO_USER_STATUS_DP_FROM_TOP)
|
||||
}
|
||||
if (!statusIcon.isNullOrEmpty()) {
|
||||
holder.binding.participantStatusEmoji.setText(statusIcon)
|
||||
} else {
|
||||
holder.binding.participantStatusEmoji.visibility = View.GONE
|
||||
}
|
||||
if (status != null && status == StatusType.DND.string) {
|
||||
if (statusMessage.isNullOrEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.dnd)
|
||||
}
|
||||
} else if (status != null && status == StatusType.BUSY.string) {
|
||||
if (statusMessage.isNullOrEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.busy)
|
||||
}
|
||||
} else if (status != null && status == StatusType.AWAY.string) {
|
||||
if (statusMessage.isNullOrEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.away)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun alignUsernameVertical(holder: ParticipantItemViewHolder, densityPixelsFromTop: Float) {
|
||||
val layoutParams = holder.binding.nameText.layoutParams as ConstraintLayout.LayoutParams
|
||||
layoutParams.topMargin = DisplayUtils.convertDpToPixel(densityPixelsFromTop, context).toInt()
|
||||
holder.binding.nameText.setLayoutParams(layoutParams)
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean =
|
||||
objectId != null &&
|
||||
Pattern
|
||||
.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(objectId)
|
||||
.find() ||
|
||||
displayName != null &&
|
||||
Pattern
|
||||
.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(displayName)
|
||||
.find()
|
||||
|
||||
companion object {
|
||||
private const val STATUS_SIZE_IN_DP = 9f
|
||||
private const val NO_ICON = ""
|
||||
private const val NO_USER_STATUS_DP_FROM_TOP: Float = 10f
|
||||
const val SOURCE_CALLS = "calls"
|
||||
const val SOURCE_GUESTS = "guests"
|
||||
const val SOURCE_GROUPS = "groups"
|
||||
const val SOURCE_EMAILS = "emails"
|
||||
const val SOURCE_TEAMS = "teams"
|
||||
const val SOURCE_FEDERATION = "federated_users"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.RvItemSearchMessageBinding
|
||||
import com.nextcloud.talk.extensions.loadThumbnail
|
||||
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
|
||||
data class MessageResultItem(
|
||||
private val context: Context,
|
||||
private val currentUser: User,
|
||||
val messageEntry: SearchMessageEntry,
|
||||
var showHeader: Boolean = false,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) : AbstractFlexibleItem<MessageResultItem.ViewHolder>(),
|
||||
IFilterable<String>,
|
||||
ISectionable<MessageResultItem.ViewHolder, GenericTextHeaderItem> {
|
||||
|
||||
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemSearchMessageBinding
|
||||
|
||||
init {
|
||||
binding = RvItemSearchMessageBinding.bind(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_search_message
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
|
||||
): ViewHolder = ViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>?
|
||||
) {
|
||||
holder.binding.conversationTitle.text = messageEntry.title
|
||||
bindMessageExcerpt(holder)
|
||||
messageEntry.thumbnailURL?.let { holder.binding.thumbnail.loadThumbnail(it, currentUser) }
|
||||
}
|
||||
|
||||
private fun bindMessageExcerpt(holder: ViewHolder) {
|
||||
viewThemeUtils.platform.highlightText(
|
||||
holder.binding.messageExcerpt,
|
||||
messageEntry.messageExcerpt,
|
||||
messageEntry.searchTerm
|
||||
)
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean = true
|
||||
|
||||
override fun getItemViewType(): Int = VIEW_TYPE
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = FlexibleItemViewType.MESSAGE_RESULT_ITEM
|
||||
}
|
||||
|
||||
override fun getHeader(): GenericTextHeaderItem =
|
||||
MessagesTextHeaderItem(context, viewThemeUtils)
|
||||
.apply {
|
||||
isHidden = showHeader // FlexibleAdapter needs this hack for some reason
|
||||
}
|
||||
|
||||
override fun setHeader(header: GenericTextHeaderItem?) {
|
||||
// nothing, header is always the same
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.content.Context
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
|
||||
class MessagesTextHeaderItem(context: Context, viewThemeUtils: ViewThemeUtils) :
|
||||
GenericTextHeaderItem(context.getString(R.string.messages), viewThemeUtils) {
|
||||
companion object {
|
||||
const val VIEW_TYPE = FlexibleItemViewType.MESSAGES_TEXT_HEADER_ITEM
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int = VIEW_TYPE
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.nextcloud.talk.R;
|
||||
import com.nextcloud.talk.databinding.RvItemNotificationSoundBinding;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
|
||||
import eu.davidea.flexibleadapter.items.IFlexible;
|
||||
import eu.davidea.viewholders.FlexibleViewHolder;
|
||||
|
||||
public class NotificationSoundItem extends AbstractFlexibleItem<NotificationSoundItem.NotificationSoundItemViewHolder> {
|
||||
|
||||
private final String notificationSoundName;
|
||||
private final String notificationSoundUri;
|
||||
|
||||
public NotificationSoundItem(String notificationSoundName, String notificationSoundUri) {
|
||||
this.notificationSoundName = notificationSoundName;
|
||||
this.notificationSoundUri = notificationSoundUri;
|
||||
}
|
||||
|
||||
public String getNotificationSoundUri() {
|
||||
return notificationSoundUri;
|
||||
}
|
||||
|
||||
public String getNotificationSoundName() {
|
||||
return notificationSoundName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NotificationSoundItem that = (NotificationSoundItem) o;
|
||||
|
||||
if (!Objects.equals(notificationSoundName, that.notificationSoundName)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(notificationSoundUri, that.notificationSoundUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = notificationSoundName != null ? notificationSoundName.hashCode() : 0;
|
||||
return 31 * result + (notificationSoundUri != null ? notificationSoundUri.hashCode() : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLayoutRes() {
|
||||
return R.layout.rv_item_notification_sound;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotificationSoundItemViewHolder createViewHolder(View view, FlexibleAdapter<IFlexible> adapter) {
|
||||
return new NotificationSoundItemViewHolder(view, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindViewHolder(FlexibleAdapter<IFlexible> adapter,
|
||||
NotificationSoundItemViewHolder holder,
|
||||
int position,
|
||||
List<Object> payloads) {
|
||||
holder.binding.notificationNameTextView.setText(notificationSoundName);
|
||||
holder.binding.notificationNameTextView.setChecked(adapter.isSelected(position));
|
||||
}
|
||||
|
||||
static class NotificationSoundItemViewHolder extends FlexibleViewHolder {
|
||||
|
||||
RvItemNotificationSoundBinding binding;
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*/
|
||||
NotificationSoundItemViewHolder(View view, FlexibleAdapter adapter) {
|
||||
super(view, adapter);
|
||||
binding = RvItemNotificationSoundBinding.bind(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.adapters.items.ParticipantItem.ParticipantItemViewHolder
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.RvItemConversationInfoParticipantBinding
|
||||
import com.nextcloud.talk.extensions.loadDefaultAvatar
|
||||
import com.nextcloud.talk.extensions.loadDefaultGroupCallAvatar
|
||||
import com.nextcloud.talk.extensions.loadFederatedUserAvatar
|
||||
import com.nextcloud.talk.extensions.loadFirstLetterAvatar
|
||||
import com.nextcloud.talk.extensions.loadPhoneAvatar
|
||||
import com.nextcloud.talk.extensions.loadTeamAvatar
|
||||
import com.nextcloud.talk.extensions.loadUserAvatar
|
||||
import com.nextcloud.talk.models.domain.ConversationModel
|
||||
import com.nextcloud.talk.models.json.participants.Participant
|
||||
import com.nextcloud.talk.models.json.participants.Participant.InCallFlags
|
||||
import com.nextcloud.talk.models.json.status.StatusType
|
||||
import com.nextcloud.talk.ui.StatusDrawable
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ConversationUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils.convertDpToPixel
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class ParticipantItem(
|
||||
private val context: Context,
|
||||
val model: Participant,
|
||||
private val user: User,
|
||||
private val viewThemeUtils: ViewThemeUtils,
|
||||
private val conversation: ConversationModel
|
||||
) : AbstractFlexibleItem<ParticipantItemViewHolder>(),
|
||||
IFilterable<String?> {
|
||||
var isOnline = true
|
||||
override fun equals(o: Any?): Boolean =
|
||||
if (o is ParticipantItem) {
|
||||
model.calculatedActorType == o.model.calculatedActorType &&
|
||||
model.calculatedActorId == o.model.calculatedActorId
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = model.hashCode()
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_conversation_info_participant
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View?,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
|
||||
): ParticipantItemViewHolder = ParticipantItemViewHolder(view, adapter)
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?,
|
||||
holder: ParticipantItemViewHolder?,
|
||||
position: Int,
|
||||
payloads: List<*>?
|
||||
) {
|
||||
drawStatus(holder!!)
|
||||
setOnlineStateColor(holder)
|
||||
holder.binding.nameText.text = model.displayName
|
||||
|
||||
if (model.type == Participant.ParticipantType.GUEST && model.displayName.isNullOrBlank()) {
|
||||
holder.binding.nameText.text = sharedApplication!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
if (adapter!!.hasFilter()) {
|
||||
viewThemeUtils.talk.themeAndHighlightText(
|
||||
holder.binding.nameText,
|
||||
model.displayName,
|
||||
adapter.getFilter(
|
||||
String::class.java
|
||||
).toString()
|
||||
)
|
||||
}
|
||||
loadAvatars(holder)
|
||||
showCallIcons(holder)
|
||||
setParticipantInfo(holder)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun setParticipantInfo(holder: ParticipantItemViewHolder) {
|
||||
if (TextUtils.isEmpty(model.displayName) &&
|
||||
(
|
||||
model.type == Participant.ParticipantType.GUEST ||
|
||||
model.type == Participant.ParticipantType.USER_FOLLOWING_LINK
|
||||
)
|
||||
) {
|
||||
holder.binding.nameText.text = sharedApplication!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
var userType = ""
|
||||
when (model.type) {
|
||||
Participant.ParticipantType.OWNER,
|
||||
Participant.ParticipantType.MODERATOR,
|
||||
Participant.ParticipantType.GUEST_MODERATOR -> {
|
||||
userType = sharedApplication!!.getString(R.string.nc_moderator)
|
||||
}
|
||||
|
||||
Participant.ParticipantType.USER -> {
|
||||
userType = sharedApplication!!.getString(R.string.nc_user)
|
||||
if (model.calculatedActorType == Participant.ActorType.GROUPS) {
|
||||
userType = sharedApplication!!.getString(R.string.nc_group)
|
||||
}
|
||||
if (model.calculatedActorType == Participant.ActorType.CIRCLES) {
|
||||
userType = sharedApplication!!.getString(R.string.nc_team)
|
||||
}
|
||||
}
|
||||
|
||||
Participant.ParticipantType.GUEST -> {
|
||||
userType = sharedApplication!!.getString(R.string.nc_guest)
|
||||
if (model.calculatedActorType == Participant.ActorType.EMAILS) {
|
||||
userType = sharedApplication!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
if (model.invitedActorId?.isNotEmpty() == true &&
|
||||
ConversationUtils.isParticipantOwnerOrModerator(conversation)
|
||||
) {
|
||||
holder.binding.conversationInfoStatusMessage.text = model.invitedActorId
|
||||
alignUsernameVertical(holder, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
Participant.ParticipantType.USER_FOLLOWING_LINK -> {
|
||||
userType = sharedApplication!!.getString(R.string.nc_following_link)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
if (userType != sharedApplication!!.getString(R.string.nc_user)) {
|
||||
holder.binding.secondaryText.text = "($userType)"
|
||||
}
|
||||
}
|
||||
|
||||
private fun setOnlineStateColor(holder: ParticipantItemViewHolder) {
|
||||
if (!isOnline) {
|
||||
holder.binding.nameText.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
holder.binding.nameText.context.resources,
|
||||
R.color.medium_emphasis_text,
|
||||
null
|
||||
)
|
||||
)
|
||||
holder.binding.avatarView.setAlpha(NOT_ONLINE_ALPHA)
|
||||
} else {
|
||||
holder.binding.nameText.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
holder.binding.nameText.context.resources,
|
||||
R.color.high_emphasis_text,
|
||||
null
|
||||
)
|
||||
)
|
||||
holder.binding.avatarView.setAlpha(1.0f)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private fun showCallIcons(holder: ParticipantItemViewHolder) {
|
||||
val resources = sharedApplication!!.resources
|
||||
val inCallFlag = model.inCall
|
||||
if (inCallFlag and InCallFlags.WITH_PHONE.toLong() > 0) {
|
||||
holder.binding.videoCallIcon.setImageResource(R.drawable.ic_call_grey_600_24dp)
|
||||
holder.binding.videoCallIcon.setVisibility(View.VISIBLE)
|
||||
holder.binding.videoCallIcon.setContentDescription(
|
||||
resources.getString(R.string.nc_call_state_with_phone, model.displayName)
|
||||
)
|
||||
} else if (inCallFlag and InCallFlags.WITH_VIDEO.toLong() > 0) {
|
||||
holder.binding.videoCallIcon.setImageResource(R.drawable.ic_videocam_grey_600_24dp)
|
||||
holder.binding.videoCallIcon.setVisibility(View.VISIBLE)
|
||||
holder.binding.videoCallIcon.setContentDescription(
|
||||
resources.getString(R.string.nc_call_state_with_video, model.displayName)
|
||||
)
|
||||
} else if (inCallFlag > InCallFlags.DISCONNECTED) {
|
||||
holder.binding.videoCallIcon.setImageResource(R.drawable.ic_mic_grey_600_24dp)
|
||||
holder.binding.videoCallIcon.setVisibility(View.VISIBLE)
|
||||
holder.binding.videoCallIcon.setContentDescription(
|
||||
resources.getString(R.string.nc_call_state_in_call, model.displayName)
|
||||
)
|
||||
} else {
|
||||
holder.binding.videoCallIcon.setVisibility(View.GONE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAvatars(holder: ParticipantItemViewHolder) {
|
||||
when (model.calculatedActorType) {
|
||||
Participant.ActorType.GROUPS -> {
|
||||
holder.binding.avatarView.loadDefaultGroupCallAvatar(viewThemeUtils)
|
||||
}
|
||||
|
||||
Participant.ActorType.CIRCLES -> {
|
||||
holder.binding.avatarView.loadTeamAvatar(viewThemeUtils)
|
||||
}
|
||||
|
||||
Participant.ActorType.USERS -> {
|
||||
holder.binding.avatarView.loadUserAvatar(user, model.calculatedActorId!!, true, false)
|
||||
}
|
||||
|
||||
Participant.ActorType.GUESTS, Participant.ActorType.EMAILS -> {
|
||||
val actorName = model.displayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
holder.binding.avatarView.loadFirstLetterAvatar(actorName)
|
||||
} else {
|
||||
holder.binding.avatarView.loadDefaultAvatar(viewThemeUtils)
|
||||
}
|
||||
}
|
||||
|
||||
Participant.ActorType.FEDERATED -> {
|
||||
val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0
|
||||
holder.binding.avatarView.loadFederatedUserAvatar(
|
||||
user,
|
||||
user.baseUrl!!,
|
||||
conversation.token,
|
||||
model.actorId!!,
|
||||
darkTheme,
|
||||
true,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
Participant.ActorType.PHONES -> {
|
||||
holder.binding.avatarView.loadPhoneAvatar(viewThemeUtils)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Avatar not shown because of unknown ActorType " + model.calculatedActorType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun drawStatus(holder: ParticipantItemViewHolder) {
|
||||
val size = convertDpToPixel(STATUS_SIZE_IN_DP, context)
|
||||
holder.binding.userStatusImage.setImageDrawable(
|
||||
StatusDrawable(
|
||||
model.status,
|
||||
NO_ICON,
|
||||
size,
|
||||
context.resources.getColor(R.color.bg_default),
|
||||
context
|
||||
)
|
||||
)
|
||||
if (model.statusMessage != null) {
|
||||
holder.binding.conversationInfoStatusMessage.text = model.statusMessage
|
||||
alignUsernameVertical(holder, 0f)
|
||||
} else {
|
||||
holder.binding.conversationInfoStatusMessage.text = ""
|
||||
alignUsernameVertical(holder, 10f)
|
||||
}
|
||||
if (model.statusIcon != null && model.statusIcon!!.isNotEmpty()) {
|
||||
holder.binding.participantStatusEmoji.setText(model.statusIcon)
|
||||
} else {
|
||||
holder.binding.participantStatusEmoji.visibility = View.GONE
|
||||
}
|
||||
if (model.status != null && model.status == StatusType.DND.string) {
|
||||
if (model.statusMessage == null || model.statusMessage!!.isEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.dnd)
|
||||
}
|
||||
} else if (model.status != null && model.status == StatusType.BUSY.string) {
|
||||
if (model.statusMessage == null || model.statusMessage!!.isEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.busy)
|
||||
}
|
||||
} else if (model.status != null && model.status == StatusType.AWAY.string) {
|
||||
if (model.statusMessage == null || model.statusMessage!!.isEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.away)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun alignUsernameVertical(holder: ParticipantItemViewHolder, densityPixelsFromTop: Float) {
|
||||
val layoutParams = holder.binding.nameText.layoutParams as ConstraintLayout.LayoutParams
|
||||
layoutParams.topMargin = convertDpToPixel(densityPixelsFromTop, context).toInt()
|
||||
holder.binding.nameText.setLayoutParams(layoutParams)
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean =
|
||||
model.displayName != null &&
|
||||
(
|
||||
Pattern.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.displayName!!.trim()).find() ||
|
||||
Pattern.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.calculatedActorId!!.trim()).find()
|
||||
)
|
||||
|
||||
class ParticipantItemViewHolder internal constructor(view: View?, adapter: FlexibleAdapter<*>?) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemConversationInfoParticipantBinding
|
||||
|
||||
init {
|
||||
binding = RvItemConversationInfoParticipantBinding.bind(view!!)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ParticipantItem::class.simpleName
|
||||
private const val STATUS_SIZE_IN_DP = 9f
|
||||
private const val NO_ICON = ""
|
||||
private const val NOT_ONLINE_ALPHA = 0.38f
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 =
|
||||
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 = 0
|
||||
|
||||
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingDeckCardMessageBinding
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingLinkPreviewMessageBinding
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingPollMessageBinding
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding
|
||||
import com.nextcloud.talk.models.domain.ConversationModel
|
||||
import com.nextcloud.talk.models.json.conversations.ConversationEnums.ConversationType
|
||||
|
||||
interface AdjustableMessageHolderInterface {
|
||||
|
||||
val binding: ViewBinding
|
||||
|
||||
fun adjustIfNoteToSelf(currentConversation: ConversationModel?) {
|
||||
if (currentConversation?.type == ConversationType.NOTE_TO_SELF) {
|
||||
when (this.binding.javaClass) {
|
||||
ItemCustomOutcomingTextMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingTextMessageBinding).bubble
|
||||
ItemCustomOutcomingDeckCardMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingDeckCardMessageBinding).bubble
|
||||
ItemCustomOutcomingLinkPreviewMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingLinkPreviewMessageBinding).bubble
|
||||
ItemCustomOutcomingPollMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingPollMessageBinding).bubble
|
||||
ItemCustomOutcomingVoiceMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingVoiceMessageBinding).bubble
|
||||
ItemCustomOutcomingLocationMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingLocationMessageBinding).bubble
|
||||
else -> null
|
||||
}?.let {
|
||||
RelativeLayout.LayoutParams(binding.root.layoutParams).apply {
|
||||
marginStart = 0
|
||||
marginEnd = 0
|
||||
}.run {
|
||||
it.layoutParams = this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Julius Linus <julius.linus@nextcloud.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
interface CallStartedMessageInterface {
|
||||
fun joinAudioCall()
|
||||
fun joinVideoCall()
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
|
||||
interface CommonMessageInterface {
|
||||
fun onLongClickReactions(chatMessage: ChatMessage)
|
||||
fun onClickReaction(chatMessage: ChatMessage, emoji: String)
|
||||
fun onOpenMessageActionsDialog(chatMessage: ChatMessage)
|
||||
fun openThread(chatMessage: ChatMessage)
|
||||
}
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemCustomIncomingDeckCardMessageBinding
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.ChatMessageUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class IncomingDeckCardViewHolder(incomingView: View, payload: Any) :
|
||||
MessageHolders
|
||||
.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
|
||||
|
||||
private val binding: ItemCustomIncomingDeckCardMessageBinding =
|
||||
ItemCustomIncomingDeckCardMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
var stackName: String? = null
|
||||
var cardName: String? = null
|
||||
var boardName: String? = null
|
||||
var cardLink: String? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
showDeckCard(message)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
binding.cardView.findViewById<ImageView>(R.id.deckCardImage)?.let {
|
||||
viewThemeUtils.platform.colorImageView(it, ColorRole.SECONDARY)
|
||||
}
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
binding.cardView.setOnLongClickListener { l: View? ->
|
||||
commonMessageInterface.onOpenMessageActionsDialog(message)
|
||||
true
|
||||
}
|
||||
|
||||
binding.cardView.setOnClickListener {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, cardLink!!.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private fun showDeckCard(message: ChatMessage) {
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "deck-card") {
|
||||
cardName = individualHashMap["name"]
|
||||
stackName = individualHashMap["stackname"]
|
||||
boardName = individualHashMap["boardname"]
|
||||
cardLink = individualHashMap["link"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cardName?.isNotEmpty() == true) {
|
||||
val cardDescription = String.format(
|
||||
context.resources.getString(R.string.deck_card_description),
|
||||
stackName,
|
||||
boardName
|
||||
)
|
||||
binding.cardName.visibility = View.VISIBLE
|
||||
binding.cardDescription.visibility = View.VISIBLE
|
||||
binding.cardName.text = cardName
|
||||
binding.cardDescription.text = cardDescription
|
||||
}
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedMessageAuthor
|
||||
.setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = IncomingDeckCardViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemCustomIncomingLinkPreviewMessageBinding
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.ChatMessageUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) :
|
||||
MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
|
||||
|
||||
private val binding: ItemCustomIncomingLinkPreviewMessageBinding =
|
||||
ItemCustomIncomingLinkPreviewMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
var processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageText.context,
|
||||
message,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
processedMessageText = messageUtils.processMessageParameters(
|
||||
binding.messageText.context,
|
||||
viewThemeUtils,
|
||||
processedMessageText!!,
|
||||
message,
|
||||
itemView
|
||||
)
|
||||
|
||||
binding.messageText.text = processedMessageText
|
||||
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
LinkPreview().showLink(
|
||||
message,
|
||||
ncApi,
|
||||
binding.referenceInclude,
|
||||
itemView.context
|
||||
)
|
||||
binding.referenceInclude.referenceWrapper.setOnLongClickListener { l: View? ->
|
||||
commonMessageInterface.onOpenMessageActionsDialog(message)
|
||||
true
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val showThreadButton = chatActivity.conversationThreadId == null && message.isThread
|
||||
if (showThreadButton) {
|
||||
binding.reactions.threadButton.visibility = View.VISIBLE
|
||||
binding.reactions.threadButton.setContent {
|
||||
ThreadButtonComposable(
|
||||
onButtonClick = { openThread(message) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.reactions.threadButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedMessageAuthor
|
||||
.setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = IncomingLinkPreviewMessageViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.core.net.toUri
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.ChatMessageUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.UriUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URLEncoder
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) :
|
||||
MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
|
||||
private val binding: ItemCustomIncomingLocationMessageBinding =
|
||||
ItemCustomIncomingLocationMessageBinding.bind(itemView)
|
||||
|
||||
var locationLon: String? = ""
|
||||
var locationLat: String? = ""
|
||||
var locationName: String? = ""
|
||||
var locationGeoLink: String? = ""
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
val textSize = context.resources!!.getDimension(R.dimen.chat_text_size)
|
||||
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
binding.messageText.text = message.text
|
||||
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
// geo-location
|
||||
setLocationDataOnMessageItem(message)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageText.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedMessageAuthor
|
||||
.setTextColor(context.resources.getColor(R.color.textColorMaxContrast, null))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
|
||||
private fun setLocationDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "geo-location") {
|
||||
locationLon = individualHashMap["longitude"]
|
||||
locationLat = individualHashMap["latitude"]
|
||||
locationName = individualHashMap["name"]
|
||||
locationGeoLink = individualHashMap["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.webview.settings.javaScriptEnabled = true
|
||||
|
||||
binding.webview.webViewClient = object : WebViewClient() {
|
||||
@Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)")
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean =
|
||||
if (url != null && UriUtils.hasHttpProtocolPrefixed(url)) {
|
||||
view?.context?.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html")
|
||||
urlStringBuffer.append(
|
||||
"?mapProviderUrl=" + URLEncoder.encode(context.getString(R.string.osm_tile_server_url))
|
||||
)
|
||||
urlStringBuffer.append(
|
||||
"&mapProviderAttribution=" + URLEncoder.encode(context.getString(R.string.osm_tile_server_attributation))
|
||||
)
|
||||
urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat))
|
||||
urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon))
|
||||
urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName))
|
||||
urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink))
|
||||
|
||||
binding.webview.loadUrl(urlStringBuffer.toString())
|
||||
|
||||
binding.webview.setOnTouchListener(object : View.OnTouchListener {
|
||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||
when (event?.action) {
|
||||
MotionEvent.ACTION_UP -> openGeoLink()
|
||||
}
|
||||
|
||||
return v?.onTouchEvent(event) ?: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun openGeoLink() {
|
||||
if (!locationGeoLink.isNullOrEmpty()) {
|
||||
val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!)
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, geoLinkWithMarker.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
} else {
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
Log.e(TAG, "locationGeoLink was null or empty")
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMarkerToGeoLink(locationGeoLink: String): String = locationGeoLink.replace("geo:", "geo:0,0?q=")
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LocInMessageView"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemCustomIncomingPollMessageBinding
|
||||
import com.nextcloud.talk.polls.ui.PollMainDialogFragment
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.ChatMessageUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class IncomingPollMessageViewHolder(incomingView: View, payload: Any) :
|
||||
MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
|
||||
|
||||
private val binding: ItemCustomIncomingPollMessageBinding = ItemCustomIncomingPollMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
setPollPreview(message)
|
||||
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
Thread().showThreadPreview(
|
||||
chatActivity,
|
||||
message,
|
||||
threadBinding = binding.threadTitleWrapper,
|
||||
reactionsBinding = binding.reactions,
|
||||
openThread = { openThread(message) }
|
||||
)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
private fun setPollPreview(message: ChatMessage) {
|
||||
var pollId: String? = null
|
||||
var pollName: String? = null
|
||||
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "talk-poll") {
|
||||
pollId = individualHashMap["id"]
|
||||
pollName = individualHashMap["name"].toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pollId != null && pollName != null) {
|
||||
binding.messagePollTitle.text = pollName
|
||||
|
||||
val roomToken = (payload as? MessagePayload)!!.roomToken
|
||||
val isOwnerOrModerator = (payload as? MessagePayload)!!.isOwnerOrModerator ?: false
|
||||
|
||||
binding.bubble.setOnClickListener {
|
||||
val pollVoteDialog = PollMainDialogFragment.newInstance(
|
||||
message.activeUser!!,
|
||||
roomToken,
|
||||
isOwnerOrModerator,
|
||||
pollId,
|
||||
pollName
|
||||
)
|
||||
pollVoteDialog.show(
|
||||
(binding.messagePollIcon.context as ChatActivity).supportFragmentManager,
|
||||
TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedMessageAuthor
|
||||
.setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = IncomingPollMessageViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022-2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021-2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.nextcloud.talk.R;
|
||||
import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
|
||||
import com.nextcloud.talk.databinding.ItemThreadTitleBinding;
|
||||
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage;
|
||||
import com.nextcloud.talk.utils.TextMatchers;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.emoji2.widget.EmojiTextView;
|
||||
|
||||
public class IncomingPreviewMessageViewHolder extends PreviewMessageViewHolder {
|
||||
private final ItemCustomIncomingPreviewMessageBinding binding;
|
||||
|
||||
public IncomingPreviewMessageViewHolder(View itemView, Object payload) {
|
||||
super(itemView, payload);
|
||||
binding = ItemCustomIncomingPreviewMessageBinding.bind(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(@NonNull ChatMessage message) {
|
||||
super.onBind(message);
|
||||
if(!message.isVoiceMessage()
|
||||
&& !Objects.equals(message.getMessage(), "{file}")
|
||||
) {
|
||||
Spanned processedMessageText = null;
|
||||
binding.incomingPreviewMessageBubble.setBackgroundResource(R.drawable.shape_grouped_incoming_message);
|
||||
if (viewThemeUtils != null ) {
|
||||
processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageCaption.getContext(),
|
||||
message,
|
||||
true,
|
||||
viewThemeUtils);
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(binding.incomingPreviewMessageBubble, true, false,
|
||||
false);
|
||||
}
|
||||
|
||||
if (processedMessageText != null) {
|
||||
processedMessageText = messageUtils.processMessageParameters(
|
||||
binding.messageCaption.getContext(),
|
||||
viewThemeUtils,
|
||||
processedMessageText,
|
||||
message,
|
||||
binding.incomingPreviewMessageBubble);
|
||||
}
|
||||
binding.incomingPreviewMessageBubble.setOnClickListener(null);
|
||||
|
||||
float textSize = 0;
|
||||
if (context != null) {
|
||||
textSize = context.getResources().getDimension(R.dimen.chat_text_size);
|
||||
}
|
||||
HashMap<String, HashMap<String, String>> messageParameters = message.getMessageParameters();
|
||||
if (
|
||||
(messageParameters == null || messageParameters.size() <= 0) &&
|
||||
TextMatchers.isMessageWithSingleEmoticonOnly(message.getText())
|
||||
) {
|
||||
textSize = (float) (textSize * IncomingTextMessageViewHolder.TEXT_SIZE_MULTIPLIER);
|
||||
itemView.setSelected(true);
|
||||
}
|
||||
binding.messageCaption.setVisibility(View.VISIBLE);
|
||||
binding.messageCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
|
||||
binding.messageCaption.setText(processedMessageText);
|
||||
} else {
|
||||
binding.incomingPreviewMessageBubble.setBackground(null);
|
||||
binding.messageCaption.setVisibility(View.GONE);
|
||||
}
|
||||
binding.messageAuthor.setText(message.getActorDisplayName());
|
||||
binding.messageText.setTextColor(ContextCompat.getColor(binding.messageText.getContext(),
|
||||
R.color.no_emphasis_text));
|
||||
binding.messageTime.setTextColor(ContextCompat.getColor(binding.messageText.getContext(),
|
||||
R.color.no_emphasis_text));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getMessageText() {
|
||||
return binding.messageText;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getMessageCaption() {
|
||||
return binding.messageCaption;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressBar getProgressBar() {
|
||||
return binding.progressBar;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getPreviewContainer() {
|
||||
return binding.previewContainer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MaterialCardView getPreviewContactContainer() {
|
||||
return binding.contactContainer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImageView getPreviewContactPhoto() {
|
||||
return binding.contactPhoto;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getPreviewContactName() {
|
||||
return binding.contactName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressBar getPreviewContactProgressBar() {
|
||||
return binding.contactProgressBar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactionsInsideMessageBinding getReactionsBinding(){ return binding.reactions; }
|
||||
|
||||
@Override
|
||||
public ItemThreadTitleBinding getThreadsBinding(){ return binding.threadTitleWrapper; }
|
||||
}
|
||||
|
|
@ -0,0 +1,428 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021-2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.toSpanned
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.ChatMessageRepository
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
|
||||
import com.nextcloud.talk.utils.ChatMessageUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.SpreedFeatures
|
||||
import com.nextcloud.talk.utils.TextMatchers
|
||||
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
|
||||
MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(itemView, payload) {
|
||||
|
||||
private val binding: ItemCustomIncomingTextMessageBinding = ItemCustomIncomingTextMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var currentUserProvider: CurrentUserProviderNew
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@Inject
|
||||
lateinit var chatRepository: ChatMessageRepository
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
colorizeMessageBubble(message)
|
||||
itemView.isSelected = false
|
||||
val user = currentUserProvider.currentUser.blockingGet()
|
||||
val hasCheckboxes = processCheckboxes(
|
||||
message,
|
||||
user
|
||||
)
|
||||
processMessage(message, hasCheckboxes)
|
||||
}
|
||||
|
||||
private fun processMessage(message: ChatMessage, hasCheckboxes: Boolean) {
|
||||
var textSize = context.resources!!.getDimension(R.dimen.chat_text_size)
|
||||
if (!hasCheckboxes) {
|
||||
binding.messageText.visibility = View.VISIBLE
|
||||
binding.checkboxContainer.visibility = View.GONE
|
||||
var processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageText.context,
|
||||
message,
|
||||
true,
|
||||
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(
|
||||
binding.messageText.context,
|
||||
viewThemeUtils,
|
||||
processedMessageText,
|
||||
message,
|
||||
itemView
|
||||
)
|
||||
val messageParameters = message.messageParameters
|
||||
if (
|
||||
(messageParameters == null || messageParameters.size <= 0) &&
|
||||
TextMatchers.isMessageWithSingleEmoticonOnly(message.text)
|
||||
) {
|
||||
textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat()
|
||||
itemView.isSelected = true
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
binding.messageText.text = processedMessageText
|
||||
// just for debugging:
|
||||
// binding.messageText.text =
|
||||
// SpannableStringBuilder(processedMessageText).append(" (" + message.jsonMessageId + ")")
|
||||
} else {
|
||||
binding.messageText.visibility = View.GONE
|
||||
binding.checkboxContainer.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (message.lastEditTimestamp != 0L && !message.isDeleted) {
|
||||
binding.messageEditIndicator.visibility = View.VISIBLE
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!)
|
||||
} else {
|
||||
binding.messageEditIndicator.visibility = View.GONE
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
}
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
|
||||
// parent message handling
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
processParentMessage(message)
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnLongClickListener { l: View? ->
|
||||
commonMessageInterface.onOpenMessageActionsDialog(message)
|
||||
true
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
Thread().showThreadPreview(
|
||||
chatActivity,
|
||||
message,
|
||||
threadBinding = binding.threadTitleWrapper,
|
||||
reactionsBinding = binding.reactions,
|
||||
openThread = { openThread(message) }
|
||||
)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageText.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun processCheckboxes(chatMessage: ChatMessage, user: User): Boolean {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val message = chatMessage.message!!.toSpanned()
|
||||
val messageTextView = binding.messageText
|
||||
val checkBoxContainer = binding.checkboxContainer
|
||||
val isOlderThanTwentyFourHours = chatMessage
|
||||
.createdAt
|
||||
.before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE))
|
||||
|
||||
val messageIsEditable = hasSpreedFeatureCapability(
|
||||
user.capabilities?.spreedCapability!!,
|
||||
SpreedFeatures.EDIT_MESSAGES
|
||||
) &&
|
||||
!isOlderThanTwentyFourHours
|
||||
|
||||
checkBoxContainer.removeAllViews()
|
||||
val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE)
|
||||
val matches = regex.findAll(message)
|
||||
|
||||
if (matches.none()) return false
|
||||
|
||||
val firstPart = message.toString().substringBefore("\n- [")
|
||||
messageTextView.text = messageUtils.enrichChatMessageText(
|
||||
binding.messageText.context,
|
||||
firstPart,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
val checkboxList = mutableListOf<CheckBox>()
|
||||
|
||||
matches.forEach { matchResult ->
|
||||
val isChecked = matchResult.groupValues[CHECKED_GROUP_INDEX] == "X" ||
|
||||
matchResult.groupValues[CHECKED_GROUP_INDEX] == "x"
|
||||
val taskText = matchResult.groupValues[TASK_TEXT_GROUP_INDEX].trim()
|
||||
|
||||
val checkBox = CheckBox(checkBoxContainer.context).apply {
|
||||
text = taskText
|
||||
this.isChecked = isChecked
|
||||
this.isEnabled = (
|
||||
chatMessage.actorType == "bots" ||
|
||||
chatActivity.userAllowedByPrivilages(chatMessage)
|
||||
) &&
|
||||
messageIsEditable
|
||||
|
||||
setTextColor(ContextCompat.getColor(context, R.color.no_emphasis_text))
|
||||
|
||||
setOnCheckedChangeListener { _, _ ->
|
||||
updateCheckboxStates(chatMessage, user, checkboxList)
|
||||
}
|
||||
}
|
||||
checkBoxContainer.addView(checkBox)
|
||||
checkboxList.add(checkBox)
|
||||
viewThemeUtils.platform.themeCheckbox(checkBox)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateCheckboxStates(chatMessage: ChatMessage, user: User, checkboxes: List<CheckBox>) {
|
||||
job = CoroutineScope(Dispatchers.Main).launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val apiVersion: Int = ApiUtils.getChatApiVersion(
|
||||
user.capabilities?.spreedCapability!!,
|
||||
intArrayOf(1)
|
||||
)
|
||||
val updatedMessage = updateMessageWithCheckboxStates(chatMessage.message!!, checkboxes)
|
||||
chatRepository.editChatMessage(
|
||||
user.getCredentials(),
|
||||
ApiUtils.getUrlForChatMessage(apiVersion, user.baseUrl!!, chatMessage.token!!, chatMessage.id),
|
||||
updatedMessage
|
||||
).collect { result ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (result.isSuccess) {
|
||||
val editedMessage = result.getOrNull()?.ocs?.data!!.parentMessage!!
|
||||
Log.d(TAG, "EditedMessage: $editedMessage")
|
||||
binding.messageEditIndicator.apply {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
binding.messageTime.text =
|
||||
dateUtils.getLocalTimeStringFromTimestamp(editedMessage.lastEditTimestamp!!)
|
||||
} else {
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMessageWithCheckboxStates(originalMessage: String, checkboxes: List<CheckBox>): String {
|
||||
var updatedMessage = originalMessage
|
||||
val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE)
|
||||
|
||||
checkboxes.forEach { _ ->
|
||||
updatedMessage = regex.replace(updatedMessage) { matchResult ->
|
||||
val taskText = matchResult.groupValues[TASK_TEXT_GROUP_INDEX].trim()
|
||||
val checkboxState = if (checkboxes.find { it.text == taskText }?.isChecked == true) "X" else " "
|
||||
"- [$checkboxState] $taskText"
|
||||
}
|
||||
}
|
||||
return updatedMessage
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
private fun processParentMessage(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text =
|
||||
if (parentChatMessage.actorDisplayName.isNullOrEmpty()) {
|
||||
context.getText(R.string.nc_nick_guest)
|
||||
} else {
|
||||
parentChatMessage.actorDisplayName
|
||||
}
|
||||
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView,
|
||||
R.color.high_emphasis_text
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnClickListener {
|
||||
chatActivity.jumpToQuotedMessage(parentChatMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
override fun viewDetached() {
|
||||
super.viewDetached()
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TEXT_SIZE_MULTIPLIER = 2.5
|
||||
private val TAG = IncomingTextMessageViewHolder::class.java.simpleName
|
||||
private const val CHECKED_GROUP_INDEX = 2
|
||||
private const val TASK_TEXT_GROUP_INDEX = 3
|
||||
private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.ChatMessageUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ExecutionException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
|
||||
MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
|
||||
|
||||
private val binding: ItemCustomIncomingVoiceMessageBinding = ItemCustomIncomingVoiceMessageBinding.bind(itemView)
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var context: Context? = null
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var voiceMessageInterface: VoiceMessageInterface
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
private var isBound = false
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
if (isBound) {
|
||||
handleIsPlayingVoiceMessageState(message)
|
||||
return
|
||||
}
|
||||
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
val filename = message.selectedIndividualHashMap!!["name"]
|
||||
val retrieved = appPreferences.getWaveFormFromFile(filename)
|
||||
if (retrieved.isNotEmpty() &&
|
||||
message.voiceMessageFloatArray == null ||
|
||||
message.voiceMessageFloatArray?.isEmpty() == true
|
||||
) {
|
||||
message.voiceMessageFloatArray = retrieved.toFloatArray()
|
||||
binding.seekbar.setWaveData(message.voiceMessageFloatArray!!)
|
||||
}
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
updateDownloadState(message)
|
||||
binding.seekbar.max = MAX
|
||||
viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar)
|
||||
viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT)
|
||||
|
||||
showVoiceMessageDuration(message)
|
||||
if (message.isDownloadingVoiceMessage) {
|
||||
showVoiceMessageLoading()
|
||||
} else {
|
||||
if (message.voiceMessageFloatArray == null || message.voiceMessageFloatArray!!.isEmpty()) {
|
||||
binding.seekbar.setWaveData(FloatArray(0))
|
||||
} else {
|
||||
binding.seekbar.setWaveData(message.voiceMessageFloatArray!!)
|
||||
}
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (message.resetVoiceMessage) {
|
||||
resetVoiceMessage(message)
|
||||
}
|
||||
|
||||
binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
voiceMessageInterface.updateMediaPlayerProgressBySlider(message, progress)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
(voiceMessageInterface as ChatActivity).chatViewModel.voiceMessagePlayBackUIFlow.onEach { speed ->
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.playbackSpeedControlBtn.setSpeed(speed)
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
|
||||
binding.playbackSpeedControlBtn.setSpeed(appPreferences.getPreferredPlayback(message.actorId))
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
isBound = true
|
||||
}
|
||||
|
||||
private fun showVoiceMessageDuration(message: ChatMessage) {
|
||||
if (message.voiceMessageDuration > 0) {
|
||||
binding.voiceMessageDuration.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.voiceMessageDuration.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetVoiceMessage(chatMessage: ChatMessage) {
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||
)
|
||||
binding.seekbar.progress = SEEKBAR_START
|
||||
chatMessage.resetVoiceMessage = false
|
||||
chatMessage.voiceMessagePlayedSeconds = 0
|
||||
showVoiceMessageDuration(message)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun handleIsPlayingVoiceMessageState(message: ChatMessage) {
|
||||
colorizeMessageBubble(message)
|
||||
if (message.isPlayingVoiceMessage) {
|
||||
showPlayButton()
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_pause_voice_message_24
|
||||
)
|
||||
|
||||
val d = message.voiceMessageDuration.toLong()
|
||||
val t = message.voiceMessagePlayedSeconds.toLong()
|
||||
binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t)
|
||||
binding.voiceMessageDuration.visibility = View.VISIBLE
|
||||
binding.seekbar.progress = message.voiceMessageSeekbarProgress
|
||||
} else {
|
||||
showVoiceMessageDuration(message)
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDownloadState(message: ChatMessage) {
|
||||
// check if download worker is already running
|
||||
val fileId = message.selectedIndividualHashMap!!["id"]
|
||||
val workers = WorkManager.getInstance(context!!).getWorkInfosByTag(fileId!!)
|
||||
|
||||
try {
|
||||
for (workInfo in workers.get()) {
|
||||
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
showVoiceMessageLoading()
|
||||
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
|
||||
.observeForever { info: WorkInfo? ->
|
||||
showStatus(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showStatus(info: WorkInfo?) {
|
||||
if (info != null) {
|
||||
when (info.state) {
|
||||
WorkInfo.State.RUNNING -> {
|
||||
Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder")
|
||||
showVoiceMessageLoading()
|
||||
}
|
||||
|
||||
WorkInfo.State.SUCCEEDED -> {
|
||||
Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder")
|
||||
showPlayButton()
|
||||
}
|
||||
|
||||
WorkInfo.State.FAILED -> {
|
||||
Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder")
|
||||
showPlayButton()
|
||||
}
|
||||
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPlayButton() {
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showVoiceMessageLoading() {
|
||||
binding.playPauseBtn.visibility = View.GONE
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(
|
||||
bubble,
|
||||
message.isGrouped,
|
||||
message.isDeleted,
|
||||
message.wasPlayedVoiceMessage
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context!!.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedMessageAuthor
|
||||
.setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun assignVoiceMessageInterface(voiceMessageInterface: VoiceMessageInterface) {
|
||||
this.voiceMessageInterface = voiceMessageInterface
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VoiceInMessageView"
|
||||
private const val SEEKBAR_START: Int = 0
|
||||
private const val MAX: Int = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import coil.load
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ReferenceInsideMessageBinding
|
||||
import com.nextcloud.talk.models.json.opengraph.OpenGraphOverall
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
class LinkPreview {
|
||||
|
||||
fun showLink(message: ChatMessage, ncApi: NcApi, binding: ReferenceInsideMessageBinding, context: Context) {
|
||||
binding.referenceName.text = ""
|
||||
binding.referenceDescription.text = ""
|
||||
binding.referenceLink.text = ""
|
||||
binding.referenceThumbImage.setImageDrawable(null)
|
||||
|
||||
if (!message.extractedUrlToPreview.isNullOrEmpty()) {
|
||||
val credentials: String = ApiUtils.getCredentials(message.activeUser?.username, message.activeUser?.token)!!
|
||||
val openGraphLink = ApiUtils.getUrlForOpenGraph(message.activeUser?.baseUrl!!)
|
||||
ncApi.getOpenGraph(
|
||||
credentials,
|
||||
openGraphLink,
|
||||
message.extractedUrlToPreview
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(object : Observer<OpenGraphOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onNext(openGraphOverall: OpenGraphOverall) {
|
||||
val reference = openGraphOverall.ocs?.data?.references?.entries?.iterator()?.next()?.value
|
||||
|
||||
if (reference != null) {
|
||||
val referenceName = reference.openGraphObject?.name
|
||||
if (!referenceName.isNullOrEmpty()) {
|
||||
binding.referenceName.visibility = View.VISIBLE
|
||||
binding.referenceName.text = referenceName
|
||||
} else {
|
||||
binding.referenceName.visibility = View.GONE
|
||||
}
|
||||
|
||||
val referenceDescription = reference.openGraphObject?.description
|
||||
if (!referenceDescription.isNullOrEmpty()) {
|
||||
binding.referenceDescription.visibility = View.VISIBLE
|
||||
binding.referenceDescription.text = referenceDescription
|
||||
} else {
|
||||
binding.referenceDescription.visibility = View.GONE
|
||||
}
|
||||
|
||||
val referenceLink = reference.openGraphObject?.link
|
||||
if (!referenceLink.isNullOrEmpty()) {
|
||||
binding.referenceLink.visibility = View.VISIBLE
|
||||
binding.referenceLink.text = referenceLink.replace(HTTPS_PROTOCOL, "")
|
||||
} else {
|
||||
binding.referenceLink.visibility = View.GONE
|
||||
}
|
||||
|
||||
val referenceThumbUrl = reference.openGraphObject?.thumb
|
||||
var backgroundId = R.drawable.link_text_background
|
||||
if (!referenceThumbUrl.isNullOrEmpty()) {
|
||||
binding.referenceThumbImage.visibility = View.VISIBLE
|
||||
binding.referenceThumbImage.load(referenceThumbUrl)
|
||||
} else {
|
||||
backgroundId = R.drawable.link_text_no_preview_background
|
||||
binding.referenceThumbImage.visibility = View.GONE
|
||||
}
|
||||
binding.referenceMetadataContainer.background = ContextCompat.getDrawable(
|
||||
binding.referenceMetadataContainer.context,
|
||||
backgroundId
|
||||
)
|
||||
|
||||
binding.referenceWrapper.setOnClickListener {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, referenceLink!!.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "failed to get openGraph data", e)
|
||||
binding.referenceName.visibility = View.GONE
|
||||
binding.referenceDescription.visibility = View.GONE
|
||||
binding.referenceLink.visibility = View.GONE
|
||||
binding.referenceThumbImage.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = LinkPreview::class.java.simpleName
|
||||
private const val HTTPS_PROTOCOL = "https://"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
|
||||
|
||||
data class MessagePayload(
|
||||
var roomToken: String,
|
||||
val isOwnerOrModerator: Boolean?,
|
||||
val profileBottomSheet: ProfileBottomSheet
|
||||
)
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2024 Sowjanya Kota<sowjanya.kch@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingDeckCardMessageBinding
|
||||
import com.nextcloud.talk.models.json.chat.ReadStatus
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class OutcomingDeckCardViewHolder(outcomingView: View) :
|
||||
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingDeckCardMessageBinding = ItemCustomOutcomingDeckCardMessageBinding.bind(
|
||||
itemView
|
||||
)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
var stackName: String? = null
|
||||
var cardName: String? = null
|
||||
var boardName: String? = null
|
||||
var cardLink: String? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
binding.cardView.findViewById<ImageView>(R.id.deckCardImage)?.let {
|
||||
viewThemeUtils.platform.colorImageView(it, ColorRole.SECONDARY)
|
||||
}
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
showDeckCard(message)
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
val readStatusDrawableInt = when (message.readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (message.readStatus) {
|
||||
ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
readStatusDrawableInt?.let { drawableInt ->
|
||||
AppCompatResources.getDrawable(context, drawableInt)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
|
||||
binding.cardView.setOnClickListener {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, cardLink!!.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDeckCard(message: ChatMessage) {
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "deck-card") {
|
||||
cardName = individualHashMap["name"]
|
||||
stackName = individualHashMap["stackname"]
|
||||
boardName = individualHashMap["boardname"]
|
||||
cardLink = individualHashMap["link"]
|
||||
}
|
||||
}
|
||||
}
|
||||
val cardDescription = String.format(
|
||||
context.resources.getString(R.string.deck_card_description),
|
||||
stackName,
|
||||
boardName
|
||||
)
|
||||
|
||||
binding.cardName.visibility = View.VISIBLE
|
||||
binding.cardDescription.visibility = View.VISIBLE
|
||||
binding.cardName.text = cardName
|
||||
binding.cardDescription.text = cardDescription
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedMessageAuthor
|
||||
.setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = OutcomingDeckCardViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingLinkPreviewMessageBinding
|
||||
import com.nextcloud.talk.models.json.chat.ReadStatus
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) :
|
||||
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView, payload),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingLinkPreviewMessageBinding =
|
||||
ItemCustomOutcomingLinkPreviewMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
var processedMessageText =
|
||||
messageUtils.enrichChatMessageText(binding.messageText.context, message, false, viewThemeUtils)
|
||||
processedMessageText = messageUtils.processMessageParameters(
|
||||
binding.messageText.context,
|
||||
viewThemeUtils,
|
||||
processedMessageText!!,
|
||||
message,
|
||||
itemView
|
||||
)
|
||||
|
||||
binding.messageText.text = processedMessageText
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
val readStatusDrawableInt = when (message.readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (message.readStatus) {
|
||||
ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
readStatusDrawableInt?.let { drawableInt ->
|
||||
AppCompatResources.getDrawable(context, drawableInt)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
|
||||
LinkPreview().showLink(
|
||||
message,
|
||||
ncApi,
|
||||
binding.referenceInclude,
|
||||
itemView.context
|
||||
)
|
||||
binding.referenceInclude.referenceWrapper.setOnLongClickListener { l: View? ->
|
||||
commonMessageInterface.onOpenMessageActionsDialog(message)
|
||||
true
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val showThreadButton = chatActivity.conversationThreadId == null && message.isThread
|
||||
if (showThreadButton) {
|
||||
binding.reactions.threadButton.visibility = View.VISIBLE
|
||||
binding.reactions.threadButton.setContent {
|
||||
ThreadButtonComposable(
|
||||
onButtonClick = { openThread(message) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.reactions.threadButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = OutcomingLinkPreviewMessageViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.net.toUri
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding
|
||||
import com.nextcloud.talk.models.json.chat.ReadStatus
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.UriUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URLEncoder
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class OutcomingLocationMessageViewHolder(incomingView: View) :
|
||||
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(incomingView),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingLocationMessageBinding =
|
||||
ItemCustomOutcomingLocationMessageBinding.bind(itemView)
|
||||
private val realView: View = itemView
|
||||
|
||||
var locationLon: String? = ""
|
||||
var locationLat: String? = ""
|
||||
var locationName: String? = ""
|
||||
var locationGeoLink: String? = ""
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
realView.isSelected = false
|
||||
val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams
|
||||
layoutParams.isWrapBefore = false
|
||||
|
||||
val textSize = context.resources.getDimension(R.dimen.chat_text_size)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
binding.messageTime.layoutParams = layoutParams
|
||||
|
||||
binding.messageText.text = message.text
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
val readStatusDrawableInt = when (message.readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (message.readStatus) {
|
||||
ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
readStatusDrawableInt?.let { drawableInt ->
|
||||
AppCompatResources.getDrawable(context, drawableInt)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
|
||||
// geo-location
|
||||
setLocationDataOnMessageItem(message)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageText.context,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
|
||||
private fun setLocationDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "geo-location") {
|
||||
locationLon = individualHashMap["longitude"]
|
||||
locationLat = individualHashMap["latitude"]
|
||||
locationName = individualHashMap["name"]
|
||||
locationGeoLink = individualHashMap["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.webview.settings.javaScriptEnabled = true
|
||||
|
||||
binding.webview.webViewClient = object : WebViewClient() {
|
||||
@Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)")
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean =
|
||||
if (url != null && UriUtils.hasHttpProtocolPrefixed(url)) {
|
||||
view?.context?.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html")
|
||||
urlStringBuffer.append(
|
||||
"?mapProviderUrl=" + URLEncoder.encode(context.getString(R.string.osm_tile_server_url))
|
||||
)
|
||||
urlStringBuffer.append(
|
||||
"&mapProviderAttribution=" + URLEncoder.encode(
|
||||
context.getString(
|
||||
R.string
|
||||
.osm_tile_server_attributation
|
||||
)
|
||||
)
|
||||
)
|
||||
urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat))
|
||||
urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon))
|
||||
urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName))
|
||||
urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink))
|
||||
|
||||
binding.webview.loadUrl(urlStringBuffer.toString())
|
||||
|
||||
binding.webview.setOnTouchListener(object : View.OnTouchListener {
|
||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||
when (event?.action) {
|
||||
MotionEvent.ACTION_UP -> openGeoLink()
|
||||
}
|
||||
|
||||
return v?.onTouchEvent(event) ?: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
private fun openGeoLink() {
|
||||
if (!locationGeoLink.isNullOrEmpty()) {
|
||||
val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!)
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, geoLinkWithMarker.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
} else {
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
Log.e(TAG, "locationGeoLink was null or empty")
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMarkerToGeoLink(locationGeoLink: String): String = locationGeoLink.replace("geo:", "geo:0,0?q=")
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LocOutMessageView"
|
||||
private const val HALF_ALPHA_INT: Int = 255 / 2
|
||||
private val ALPHA_60_INT: Int = (255 * 0.6).roundToInt()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingPollMessageBinding
|
||||
import com.nextcloud.talk.models.json.chat.ReadStatus
|
||||
import com.nextcloud.talk.polls.ui.PollMainDialogFragment
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
|
||||
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView, payload),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingPollMessageBinding = ItemCustomOutcomingPollMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
val readStatusDrawableInt = when (message.readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (message.readStatus) {
|
||||
ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
readStatusDrawableInt?.let { drawableInt ->
|
||||
AppCompatResources.getDrawable(context, drawableInt)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
|
||||
setPollPreview(message)
|
||||
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
Thread().showThreadPreview(
|
||||
chatActivity,
|
||||
message,
|
||||
threadBinding = binding.threadTitleWrapper,
|
||||
reactionsBinding = binding.reactions,
|
||||
openThread = { openThread(message) }
|
||||
)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
private fun setPollPreview(message: ChatMessage) {
|
||||
var pollId: String? = null
|
||||
var pollName: String? = null
|
||||
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "talk-poll") {
|
||||
pollId = individualHashMap["id"]
|
||||
pollName = individualHashMap["name"].toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pollId != null && pollName != null) {
|
||||
binding.messagePollTitle.text = pollName
|
||||
|
||||
val roomToken = (payload as? MessagePayload)!!.roomToken
|
||||
val isOwnerOrModerator = (payload as? MessagePayload)!!.isOwnerOrModerator ?: false
|
||||
|
||||
binding.bubble.setOnClickListener {
|
||||
val pollVoteDialog = PollMainDialogFragment.newInstance(
|
||||
message.activeUser!!,
|
||||
roomToken,
|
||||
isOwnerOrModerator,
|
||||
pollId,
|
||||
pollName
|
||||
)
|
||||
pollVoteDialog.show(
|
||||
(binding.messagePollIcon.context as ChatActivity).supportFragmentManager,
|
||||
TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = OutcomingPollMessageViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.nextcloud.talk.R;
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
|
||||
import com.nextcloud.talk.databinding.ItemThreadTitleBinding;
|
||||
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage;
|
||||
import com.nextcloud.talk.utils.TextMatchers;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.emoji2.widget.EmojiTextView;
|
||||
|
||||
public class OutcomingPreviewMessageViewHolder extends PreviewMessageViewHolder {
|
||||
|
||||
private final ItemCustomOutcomingPreviewMessageBinding binding;
|
||||
|
||||
public OutcomingPreviewMessageViewHolder(View itemView) {
|
||||
super(itemView, null);
|
||||
binding = ItemCustomOutcomingPreviewMessageBinding.bind(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(@NonNull ChatMessage message) {
|
||||
super.onBind(message);
|
||||
if(!message.isVoiceMessage()
|
||||
&& !Objects.equals(message.getMessage(), "{file}")
|
||||
) {
|
||||
Spanned processedMessageText = null;
|
||||
binding.outgoingPreviewMessageBubble.setBackgroundResource(R.drawable.shape_grouped_outcoming_message);
|
||||
if (viewThemeUtils != null) {
|
||||
processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageCaption.getContext(),
|
||||
message,
|
||||
false,
|
||||
viewThemeUtils);
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(binding.outgoingPreviewMessageBubble, true, false,
|
||||
false);
|
||||
}
|
||||
|
||||
if (processedMessageText != null) {
|
||||
processedMessageText = messageUtils.processMessageParameters(
|
||||
binding.messageCaption.getContext(),
|
||||
viewThemeUtils,
|
||||
processedMessageText,
|
||||
message,
|
||||
binding.outgoingPreviewMessageBubble);
|
||||
}
|
||||
binding.outgoingPreviewMessageBubble.setOnClickListener(null);
|
||||
|
||||
float textSize = 0;
|
||||
if (context != null) {
|
||||
textSize = context.getResources().getDimension(R.dimen.chat_text_size);
|
||||
}
|
||||
HashMap<String, HashMap<String, String>> messageParameters = message.getMessageParameters();
|
||||
if (
|
||||
(messageParameters == null || messageParameters.size() <= 0) &&
|
||||
TextMatchers.isMessageWithSingleEmoticonOnly(message.getText())
|
||||
) {
|
||||
textSize = (float)(textSize * IncomingTextMessageViewHolder.TEXT_SIZE_MULTIPLIER);
|
||||
itemView.setSelected(true);
|
||||
}
|
||||
binding.messageCaption.setVisibility(View.VISIBLE);
|
||||
binding.messageCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
|
||||
binding.messageCaption.setText(processedMessageText);
|
||||
} else {
|
||||
binding.outgoingPreviewMessageBubble.setBackground(null);
|
||||
binding.messageCaption.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
binding.messageText.setTextColor(ContextCompat.getColor(binding.messageText.getContext(),
|
||||
R.color.no_emphasis_text));
|
||||
binding.messageTime.setTextColor(ContextCompat.getColor(binding.messageText.getContext(),
|
||||
R.color.no_emphasis_text));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getMessageText() {
|
||||
return binding.messageText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressBar getProgressBar() {
|
||||
return binding.progressBar;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getPreviewContainer() {
|
||||
return binding.previewContainer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MaterialCardView getPreviewContactContainer() {
|
||||
return binding.contactContainer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImageView getPreviewContactPhoto() {
|
||||
return binding.contactPhoto;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getPreviewContactName() {
|
||||
return binding.contactName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressBar getPreviewContactProgressBar() {
|
||||
return binding.contactProgressBar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactionsInsideMessageBinding getReactionsBinding() { return binding.reactions; }
|
||||
|
||||
@Override
|
||||
public ItemThreadTitleBinding getThreadsBinding(){ return binding.threadTitleWrapper; }
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getMessageCaption() { return binding.messageCaption; }
|
||||
}
|
||||
|
|
@ -0,0 +1,452 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.text.toSpanned
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.ChatMessageRepository
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.data.database.model.SendStatus
|
||||
import com.nextcloud.talk.data.network.NetworkMonitor
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
|
||||
import com.nextcloud.talk.models.json.chat.ReadStatus
|
||||
import com.nextcloud.talk.models.json.conversations.ConversationEnums
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.SpreedFeatures
|
||||
import com.nextcloud.talk.utils.TextMatchers
|
||||
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class OutcomingTextMessageViewHolder(itemView: View) :
|
||||
OutcomingTextMessageViewHolder<ChatMessage>(itemView),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingTextMessageBinding = ItemCustomOutcomingTextMessageBinding.bind(itemView)
|
||||
private val realView: View = itemView
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@Inject
|
||||
lateinit var chatRepository: ChatMessageRepository
|
||||
|
||||
@Inject
|
||||
lateinit var currentUserProvider: CurrentUserProviderNew
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
@Suppress("Detekt.LongMethod")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
val user = currentUserProvider.currentUser.blockingGet()
|
||||
val hasCheckboxes = processCheckboxes(
|
||||
message,
|
||||
user
|
||||
)
|
||||
processMessage(message, hasCheckboxes)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.LongMethod")
|
||||
private fun processMessage(message: ChatMessage, hasCheckboxes: Boolean) {
|
||||
var isBubbled = true
|
||||
val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams
|
||||
var textSize = context.resources.getDimension(R.dimen.chat_text_size)
|
||||
if (!hasCheckboxes) {
|
||||
realView.isSelected = false
|
||||
layoutParams.isWrapBefore = false
|
||||
|
||||
binding.messageText.visibility = View.VISIBLE
|
||||
binding.checkboxContainer.visibility = View.GONE
|
||||
|
||||
var processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageText.context,
|
||||
message,
|
||||
false,
|
||||
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(
|
||||
binding.messageText.context,
|
||||
viewThemeUtils,
|
||||
processedMessageText,
|
||||
message,
|
||||
itemView
|
||||
)
|
||||
|
||||
if (
|
||||
(message.messageParameters == null || message.messageParameters!!.size <= 0) &&
|
||||
TextMatchers.isMessageWithSingleEmoticonOnly(message.text)
|
||||
) {
|
||||
textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat()
|
||||
layoutParams.isWrapBefore = true
|
||||
realView.isSelected = true
|
||||
isBubbled = false
|
||||
}
|
||||
|
||||
binding.messageTime.layoutParams = layoutParams
|
||||
viewThemeUtils.platform.colorTextView(binding.messageText, ColorRole.ON_SURFACE_VARIANT)
|
||||
binding.messageText.text = processedMessageText
|
||||
// just for debugging:
|
||||
// binding.messageText.text =
|
||||
// SpannableStringBuilder(processedMessageText).append(" (" + message.jsonMessageId + ")")
|
||||
} else {
|
||||
binding.messageText.visibility = View.GONE
|
||||
binding.checkboxContainer.visibility = View.VISIBLE
|
||||
}
|
||||
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
if (message.lastEditTimestamp != 0L && !message.isDeleted) {
|
||||
binding.messageEditIndicator.visibility = View.VISIBLE
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!)
|
||||
} else {
|
||||
binding.messageEditIndicator.visibility = View.GONE
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
}
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
setBubbleOnChatMessage(message)
|
||||
|
||||
// parent message handling
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
processParentMessage(message)
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnLongClickListener { l: View? ->
|
||||
commonMessageInterface.onOpenMessageActionsDialog(message)
|
||||
true
|
||||
}
|
||||
|
||||
binding.checkMark.visibility = View.INVISIBLE
|
||||
binding.sendingProgress.visibility = View.GONE
|
||||
|
||||
if (message.sendStatus == SendStatus.FAILED) {
|
||||
updateStatus(R.drawable.baseline_error_outline_24, context.resources?.getString(R.string.nc_message_failed))
|
||||
} else if (message.isTemporary) {
|
||||
updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending))
|
||||
} else if (message.readStatus == ReadStatus.READ) {
|
||||
updateStatus(R.drawable.ic_check_all, context.resources?.getString(R.string.nc_message_read))
|
||||
} else if (message.readStatus == ReadStatus.SENT) {
|
||||
updateStatus(R.drawable.ic_check, context.resources?.getString(R.string.nc_message_sent))
|
||||
}
|
||||
|
||||
chatActivity.lifecycleScope.launch {
|
||||
if (message.isTemporary && !networkMonitor.isOnline.value) {
|
||||
updateStatus(
|
||||
R.drawable.ic_signal_wifi_off_white_24dp,
|
||||
context.resources?.getString(R.string.nc_message_offline)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
Thread().showThreadPreview(
|
||||
chatActivity,
|
||||
message,
|
||||
threadBinding = binding.threadTitleWrapper,
|
||||
reactionsBinding = binding.reactions,
|
||||
openThread = { openThread(message) }
|
||||
)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
context,
|
||||
true,
|
||||
viewThemeUtils,
|
||||
isBubbled
|
||||
)
|
||||
}
|
||||
|
||||
private fun processCheckboxes(chatMessage: ChatMessage, user: User): Boolean {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val message = chatMessage.message!!.toSpanned()
|
||||
val messageTextView = binding.messageText
|
||||
val checkBoxContainer = binding.checkboxContainer
|
||||
val isOlderThanTwentyFourHours = chatMessage
|
||||
.createdAt
|
||||
.before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE))
|
||||
val messageIsEditable = hasSpreedFeatureCapability(
|
||||
user.capabilities?.spreedCapability!!,
|
||||
SpreedFeatures.EDIT_MESSAGES
|
||||
) &&
|
||||
!isOlderThanTwentyFourHours
|
||||
|
||||
val isNoTimeLimitOnNoteToSelf = hasSpreedFeatureCapability(
|
||||
user.capabilities?.spreedCapability!!,
|
||||
SpreedFeatures
|
||||
.EDIT_MESSAGES_NOTE_TO_SELF
|
||||
) &&
|
||||
chatActivity.currentConversation?.type == ConversationEnums.ConversationType.NOTE_TO_SELF
|
||||
|
||||
checkBoxContainer.removeAllViews()
|
||||
val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE)
|
||||
val matches = regex.findAll(message)
|
||||
|
||||
if (matches.none()) return false
|
||||
|
||||
val firstPart = message.toString().substringBefore("\n- [")
|
||||
messageTextView.text = messageUtils.enrichChatMessageText(
|
||||
binding.messageText.context,
|
||||
firstPart,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
val checkboxList = mutableListOf<CheckBox>()
|
||||
|
||||
matches.forEach { matchResult ->
|
||||
val isChecked = matchResult.groupValues[CHECKED_GROUP_INDEX] == "X" ||
|
||||
matchResult.groupValues[CHECKED_GROUP_INDEX] == "x"
|
||||
val taskText = matchResult.groupValues[TASK_TEXT_GROUP_INDEX].trim()
|
||||
|
||||
val checkBox = CheckBox(checkBoxContainer.context).apply {
|
||||
text = taskText
|
||||
this.isChecked = isChecked
|
||||
this.isEnabled = messageIsEditable || isNoTimeLimitOnNoteToSelf
|
||||
|
||||
setTextColor(ContextCompat.getColor(context, R.color.no_emphasis_text))
|
||||
|
||||
setOnCheckedChangeListener { _, _ ->
|
||||
updateCheckboxStates(chatMessage, user, checkboxList)
|
||||
}
|
||||
}
|
||||
checkBoxContainer.addView(checkBox)
|
||||
checkboxList.add(checkBox)
|
||||
viewThemeUtils.platform.themeCheckbox(checkBox)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateCheckboxStates(chatMessage: ChatMessage, user: User, checkboxes: List<CheckBox>) {
|
||||
job = CoroutineScope(Dispatchers.Main).launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val apiVersion: Int = ApiUtils.getChatApiVersion(
|
||||
user.capabilities?.spreedCapability!!,
|
||||
intArrayOf(1)
|
||||
)
|
||||
val updatedMessage = updateMessageWithCheckboxStates(chatMessage.message!!, checkboxes)
|
||||
chatRepository.editChatMessage(
|
||||
user.getCredentials(),
|
||||
ApiUtils.getUrlForChatMessage(apiVersion, user.baseUrl!!, chatMessage.token!!, chatMessage.id),
|
||||
updatedMessage
|
||||
).collect { result ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (result.isSuccess) {
|
||||
val editedMessage = result.getOrNull()?.ocs?.data!!.parentMessage!!
|
||||
Log.d(TAG, "EditedMessage: $editedMessage")
|
||||
binding.messageEditIndicator.apply {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
binding.messageTime.text =
|
||||
dateUtils.getLocalTimeStringFromTimestamp(editedMessage.lastEditTimestamp!!)
|
||||
} else {
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMessageWithCheckboxStates(originalMessage: String, checkboxes: List<CheckBox>): String {
|
||||
var updatedMessage = originalMessage
|
||||
val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE)
|
||||
|
||||
checkboxes.forEach { _ ->
|
||||
updatedMessage = regex.replace(updatedMessage) { matchResult ->
|
||||
val taskText = matchResult.groupValues[TASK_TEXT_GROUP_INDEX].trim()
|
||||
val checkboxState = if (checkboxes.find { it.text == taskText }?.isChecked == true) "X" else " "
|
||||
"- [$checkboxState] $taskText"
|
||||
}
|
||||
}
|
||||
return updatedMessage
|
||||
}
|
||||
|
||||
private fun updateStatus(readStatusDrawableInt: Int, description: String?) {
|
||||
binding.sendingProgress.visibility = View.GONE
|
||||
binding.checkMark.visibility = View.VISIBLE
|
||||
readStatusDrawableInt.let { drawableInt ->
|
||||
ResourcesCompat.getDrawable(context.resources, drawableInt, null)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
binding.checkMark.contentDescription = description
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
private fun processParentMessage(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnClickListener {
|
||||
chatActivity.jumpToQuotedMessage(parentChatMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBubbleOnChatMessage(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
override fun viewDetached() {
|
||||
super.viewDetached()
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TEXT_SIZE_MULTIPLIER = 2.5
|
||||
private val TAG = OutcomingTextMessageViewHolder::class.java.simpleName
|
||||
private const val CHECKED_GROUP_INDEX = 2
|
||||
private const val TASK_TEXT_GROUP_INDEX = 3
|
||||
private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,398 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2023 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding
|
||||
import com.nextcloud.talk.models.json.chat.ReadStatus
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ExecutionException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
@Suppress("Detekt.TooManyFunctions")
|
||||
class OutcomingVoiceMessageViewHolder(outcomingView: View) :
|
||||
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingVoiceMessageBinding = ItemCustomOutcomingVoiceMessageBinding.bind(itemView)
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var context: Context? = null
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var handler: Handler
|
||||
|
||||
lateinit var voiceMessageInterface: VoiceMessageInterface
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
private var isBound = false
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
if (isBound) {
|
||||
handleIsPlayingVoiceMessageState(message)
|
||||
return
|
||||
}
|
||||
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
|
||||
val filename = message.selectedIndividualHashMap!!["name"]
|
||||
val retrieved = appPreferences.getWaveFormFromFile(filename)
|
||||
if (retrieved.isNotEmpty() &&
|
||||
message.voiceMessageFloatArray == null ||
|
||||
message.voiceMessageFloatArray?.isEmpty() == true
|
||||
) {
|
||||
message.voiceMessageFloatArray = retrieved.toFloatArray()
|
||||
binding.seekbar.setWaveData(message.voiceMessageFloatArray!!)
|
||||
}
|
||||
|
||||
binding.seekbar.max = MAX
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
updateDownloadState(message)
|
||||
viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar)
|
||||
viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT)
|
||||
|
||||
showVoiceMessageDuration(message)
|
||||
|
||||
handleIsDownloadingVoiceMessageState(message)
|
||||
|
||||
handleResetVoiceMessageState(message)
|
||||
|
||||
binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
voiceMessageInterface.updateMediaPlayerProgressBySlider(message, progress)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setReadStatus(message.readStatus)
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
(voiceMessageInterface as ChatActivity).chatViewModel.voiceMessagePlayBackUIFlow.onEach { speed ->
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.playbackSpeedControlBtn.setSpeed(speed)
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
|
||||
binding.playbackSpeedControlBtn.setSpeed(appPreferences.getPreferredPlayback(message.actorId))
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
isBound = true
|
||||
}
|
||||
|
||||
private fun setReadStatus(readStatus: Enum<ReadStatus>) {
|
||||
val readStatusDrawableInt = when (readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (readStatus) {
|
||||
ReadStatus.READ -> context?.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context?.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
readStatusDrawableInt?.let { drawableInt ->
|
||||
AppCompatResources.getDrawable(context!!, drawableInt)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun handleResetVoiceMessageState(message: ChatMessage) {
|
||||
if (message.resetVoiceMessage) {
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||
)
|
||||
binding.seekbar.progress = SEEKBAR_START
|
||||
message.voiceMessagePlayedSeconds = 0
|
||||
showVoiceMessageDuration(message)
|
||||
message.resetVoiceMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun showVoiceMessageDuration(message: ChatMessage) {
|
||||
if (message.voiceMessageDuration > 0) {
|
||||
binding.voiceMessageDuration.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.voiceMessageDuration.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIsDownloadingVoiceMessageState(message: ChatMessage) {
|
||||
if (message.isDownloadingVoiceMessage) {
|
||||
showVoiceMessageLoading()
|
||||
} else {
|
||||
if (message.voiceMessageFloatArray == null || message.voiceMessageFloatArray!!.isEmpty()) {
|
||||
binding.seekbar.setWaveData(FloatArray(0))
|
||||
} else {
|
||||
binding.seekbar.setWaveData(message.voiceMessageFloatArray!!)
|
||||
}
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIsPlayingVoiceMessageState(message: ChatMessage) {
|
||||
colorizeMessageBubble(message)
|
||||
if (message.isPlayingVoiceMessage) {
|
||||
showPlayButton()
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_pause_voice_message_24
|
||||
)
|
||||
|
||||
val d = message.voiceMessageDuration.toLong()
|
||||
val t = message.voiceMessagePlayedSeconds.toLong()
|
||||
binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t)
|
||||
binding.voiceMessageDuration.visibility = View.VISIBLE
|
||||
binding.seekbar.progress = message.voiceMessageSeekbarProgress
|
||||
} else {
|
||||
showVoiceMessageDuration(message)
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDownloadState(message: ChatMessage) {
|
||||
// check if download worker is already running
|
||||
val fileId = message.selectedIndividualHashMap!!["id"]
|
||||
val workers = WorkManager.getInstance(context!!).getWorkInfosByTag(fileId!!)
|
||||
|
||||
try {
|
||||
for (workInfo in workers.get()) {
|
||||
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
showVoiceMessageLoading()
|
||||
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
|
||||
.observeForever { info: WorkInfo? ->
|
||||
updateDownloadState(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDownloadState(info: WorkInfo?) {
|
||||
if (info != null) {
|
||||
when (info.state) {
|
||||
WorkInfo.State.RUNNING -> {
|
||||
Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder")
|
||||
showVoiceMessageLoading()
|
||||
}
|
||||
|
||||
WorkInfo.State.SUCCEEDED -> {
|
||||
Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder")
|
||||
showPlayButton()
|
||||
}
|
||||
|
||||
WorkInfo.State.FAILED -> {
|
||||
Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder")
|
||||
showPlayButton()
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.d(TAG, "WorkInfo.State unused in ViewHolder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPlayButton() {
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showVoiceMessageLoading() {
|
||||
binding.playPauseBtn.visibility = View.GONE
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context!!.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(
|
||||
bubble,
|
||||
message.isGrouped,
|
||||
message.isDeleted,
|
||||
message.wasPlayedVoiceMessage
|
||||
)
|
||||
}
|
||||
|
||||
fun assignVoiceMessageInterface(voiceMessageInterface: VoiceMessageInterface) {
|
||||
this.voiceMessageInterface = voiceMessageInterface
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VoiceOutMessageView"
|
||||
private const val SEEKBAR_START: Int = 0
|
||||
private const val MAX = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
|
||||
interface PreviewMessageInterface {
|
||||
fun onPreviewMessageLongClick(chatMessage: ChatMessage)
|
||||
}
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Handler
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.emoji2.widget.EmojiTextView
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.ItemThreadTitleBinding
|
||||
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
|
||||
import com.nextcloud.talk.extensions.loadChangelogBotAvatar
|
||||
import com.nextcloud.talk.extensions.loadFederatedUserAvatar
|
||||
import com.nextcloud.talk.filebrowser.models.BrowserFile
|
||||
import com.nextcloud.talk.filebrowser.webdav.ReadFilesystemOperation
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType
|
||||
import com.nextcloud.talk.utils.FileViewerUtils
|
||||
import com.nextcloud.talk.utils.FileViewerUtils.ProgressUi
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.stfalcon.chatkit.messages.MessageHolders.IncomingImageMessageViewHolder
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.SingleObserver
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
|
||||
IncomingImageMessageViewHolder<ChatMessage>(itemView, payload) {
|
||||
@JvmField
|
||||
@Inject
|
||||
var context: Context? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var viewThemeUtils: ViewThemeUtils? = null
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var okHttpClient: OkHttpClient? = null
|
||||
open var progressBar: ProgressBar? = null
|
||||
open var reactionsBinding: ReactionsInsideMessageBinding? = null
|
||||
open var threadsBinding: ItemThreadTitleBinding? = null
|
||||
var fileViewerUtils: FileViewerUtils? = null
|
||||
var clickView: View? = null
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
var previewMessageInterface: PreviewMessageInterface? = null
|
||||
|
||||
private var placeholder: Drawable? = null
|
||||
|
||||
init {
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Suppress("NestedBlockDepth", "ComplexMethod", "LongMethod")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
image.minimumHeight = DisplayUtils.convertDpToPixel(MIN_IMAGE_HEIGHT, context!!).toInt()
|
||||
|
||||
time.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
viewThemeUtils!!.platform.colorCircularProgressBar(progressBar!!, ColorRole.PRIMARY)
|
||||
clickView = image
|
||||
messageText.visibility = View.VISIBLE
|
||||
if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) {
|
||||
fileViewerUtils = FileViewerUtils(context!!, message.activeUser!!)
|
||||
val fileName = message.selectedIndividualHashMap!![KEY_NAME]
|
||||
|
||||
messageText.text = fileName
|
||||
|
||||
if (message.activeUser != null &&
|
||||
message.activeUser!!.username != null &&
|
||||
message.activeUser!!.baseUrl != null
|
||||
) {
|
||||
clickView!!.setOnClickListener { v: View? ->
|
||||
fileViewerUtils!!.openFile(
|
||||
message,
|
||||
ProgressUi(progressBar, messageText, image)
|
||||
)
|
||||
}
|
||||
clickView!!.setOnLongClickListener {
|
||||
previewMessageInterface!!.onPreviewMessageLongClick(message)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "failed to set click listener because activeUser, username or baseUrl were null")
|
||||
}
|
||||
fileViewerUtils!!.resumeToUpdateViewsByProgress(
|
||||
message.selectedIndividualHashMap!![KEY_NAME]!!,
|
||||
message.selectedIndividualHashMap!![KEY_ID]!!,
|
||||
message.selectedIndividualHashMap!![KEY_MIMETYPE],
|
||||
message.openWhenDownloaded,
|
||||
ProgressUi(progressBar, messageText, image)
|
||||
)
|
||||
} else if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE) {
|
||||
messageText.text = "GIPHY"
|
||||
DisplayUtils.setClickableString("GIPHY", "https://giphy.com", messageText)
|
||||
} else if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE) {
|
||||
messageText.text = "Tenor"
|
||||
DisplayUtils.setClickableString("Tenor", "https://tenor.com", messageText)
|
||||
} else {
|
||||
if (message.messageType == ChatMessage.MessageType.SINGLE_LINK_IMAGE_MESSAGE.name) {
|
||||
clickView!!.setOnClickListener {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, message.imageUrl!!.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context!!.startActivity(browserIntent)
|
||||
}
|
||||
} else {
|
||||
clickView!!.setOnClickListener(null)
|
||||
}
|
||||
messageText.text = ""
|
||||
}
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
Thread().showThreadPreview(
|
||||
chatActivity,
|
||||
message,
|
||||
threadBinding = threadsBinding!!,
|
||||
reactionsBinding = reactionsBinding!!,
|
||||
openThread = { openThread(message) }
|
||||
)
|
||||
|
||||
val paddingSide = DisplayUtils.convertDpToPixel(HORIZONTAL_REACTION_PADDING, context!!).toInt()
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
reactionsBinding!!,
|
||||
messageText.context,
|
||||
true,
|
||||
viewThemeUtils!!,
|
||||
hasBubbleBackground(message)
|
||||
)
|
||||
reactionsBinding!!.reactionsEmojiWrapper.setPadding(paddingSide, 0, paddingSide, 0)
|
||||
|
||||
if (userAvatar != null) {
|
||||
if (message.isGrouped || message.isOneToOneConversation) {
|
||||
if (message.isOneToOneConversation) {
|
||||
userAvatar.visibility = View.GONE
|
||||
} else {
|
||||
userAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
} else {
|
||||
userAvatar.visibility = View.VISIBLE
|
||||
userAvatar.setOnClickListener { v: View ->
|
||||
if (payload is MessagePayload) {
|
||||
(payload as MessagePayload).profileBottomSheet.showFor(
|
||||
message,
|
||||
v.context
|
||||
)
|
||||
}
|
||||
}
|
||||
if (ACTOR_TYPE_BOTS == message.actorType && ACTOR_ID_CHANGELOG == message.actorId) {
|
||||
userAvatar.loadChangelogBotAvatar()
|
||||
} else if (message.actorType == "federated_users") {
|
||||
userAvatar.loadFederatedUserAvatar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageCaption.setOnClickListener(null)
|
||||
messageCaption.setOnLongClickListener {
|
||||
previewMessageInterface!!.onPreviewMessageLongClick(message)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
override fun getPayloadForImageLoader(message: ChatMessage?): Any? {
|
||||
if (message!!.selectedIndividualHashMap!!.containsKey(KEY_CONTACT_NAME)) {
|
||||
previewContainer.visibility = View.GONE
|
||||
previewContactContainer.visibility = View.VISIBLE
|
||||
previewContactName.text = message.selectedIndividualHashMap!![KEY_CONTACT_NAME]
|
||||
progressBar = previewContactProgressBar
|
||||
messageText.visibility = View.INVISIBLE
|
||||
clickView = previewContactContainer
|
||||
viewThemeUtils!!.talk.colorContactChatItemBackground(previewContactContainer)
|
||||
viewThemeUtils!!.talk.colorContactChatItemName(previewContactName)
|
||||
viewThemeUtils!!.platform.colorCircularProgressBar(
|
||||
previewContactProgressBar!!,
|
||||
ColorRole.ON_PRIMARY_CONTAINER
|
||||
)
|
||||
|
||||
if (message.selectedIndividualHashMap!!.containsKey(KEY_CONTACT_PHOTO)) {
|
||||
image = previewContactPhoto
|
||||
placeholder = getDrawableFromContactDetails(
|
||||
context,
|
||||
message.selectedIndividualHashMap!![KEY_CONTACT_PHOTO]
|
||||
)
|
||||
} else {
|
||||
image = previewContactPhoto
|
||||
image.setImageDrawable(ContextCompat.getDrawable(context!!, R.drawable.ic_mimetype_text_vcard))
|
||||
}
|
||||
} else {
|
||||
previewContainer.visibility = View.VISIBLE
|
||||
previewContactContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (message.selectedIndividualHashMap!!.containsKey(KEY_MIMETYPE)) {
|
||||
val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE]
|
||||
val drawableResourceId = getDrawableResourceIdForMimeType(mimetype)
|
||||
var drawable = ContextCompat.getDrawable(context!!, drawableResourceId)
|
||||
if (drawable != null &&
|
||||
(
|
||||
drawableResourceId == R.drawable.ic_mimetype_folder ||
|
||||
drawableResourceId == R.drawable.ic_mimetype_package_x_generic
|
||||
)
|
||||
) {
|
||||
drawable = viewThemeUtils?.platform?.tintDrawable(context!!, drawable)
|
||||
}
|
||||
placeholder = drawable
|
||||
} else {
|
||||
fetchFileInformation(
|
||||
"/" + message.selectedIndividualHashMap!![KEY_PATH],
|
||||
message.activeUser
|
||||
)
|
||||
}
|
||||
|
||||
return placeholder
|
||||
}
|
||||
|
||||
private fun getDrawableFromContactDetails(context: Context?, base64: String?): Drawable? {
|
||||
var drawable: Drawable? = null
|
||||
if (base64 != "") {
|
||||
val inputStream = ByteArrayInputStream(
|
||||
Base64.decode(base64!!.toByteArray(), Base64.DEFAULT)
|
||||
)
|
||||
drawable = Drawable.createFromResourceStream(
|
||||
context!!.resources,
|
||||
null,
|
||||
inputStream,
|
||||
null,
|
||||
null
|
||||
)
|
||||
try {
|
||||
inputStream.close()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "failed to close stream in getDrawableFromContactDetails", e)
|
||||
}
|
||||
}
|
||||
if (drawable == null) {
|
||||
drawable = ContextCompat.getDrawable(context!!, R.drawable.ic_mimetype_text_vcard)
|
||||
}
|
||||
return drawable
|
||||
}
|
||||
|
||||
private fun fetchFileInformation(url: String, activeUser: User?) {
|
||||
Single.fromCallable { ReadFilesystemOperation(okHttpClient, activeUser, url, 0) }
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(object : SingleObserver<ReadFilesystemOperation> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onSuccess(readFilesystemOperation: ReadFilesystemOperation) {
|
||||
val davResponse = readFilesystemOperation.readRemotePath()
|
||||
if (davResponse.data != null) {
|
||||
val browserFileList = davResponse.data as List<BrowserFile>
|
||||
if (browserFileList.isNotEmpty()) {
|
||||
Handler(context!!.mainLooper).post {
|
||||
val resourceId = getDrawableResourceIdForMimeType(browserFileList[0].mimeType)
|
||||
placeholder = ContextCompat.getDrawable(context!!, resourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "Error reading file information", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
fun assignPreviewMessageInterface(previewMessageInterface: PreviewMessageInterface?) {
|
||||
this.previewMessageInterface = previewMessageInterface
|
||||
}
|
||||
|
||||
fun hasBubbleBackground(message: ChatMessage): Boolean = !message.isVoiceMessage && message.message != "{file}"
|
||||
|
||||
abstract val messageText: EmojiTextView
|
||||
abstract val messageCaption: EmojiTextView
|
||||
abstract val previewContainer: View
|
||||
abstract val previewContactContainer: MaterialCardView
|
||||
abstract val previewContactPhoto: ImageView
|
||||
abstract val previewContactName: EmojiTextView
|
||||
abstract val previewContactProgressBar: ProgressBar?
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PreviewMsgViewHolder"
|
||||
const val KEY_CONTACT_NAME = "contact-name"
|
||||
const val KEY_CONTACT_PHOTO = "contact-photo"
|
||||
const val KEY_MIMETYPE = "mimetype"
|
||||
const val KEY_ID = "id"
|
||||
const val KEY_PATH = "path"
|
||||
const val ACTOR_TYPE_BOTS = "bots"
|
||||
const val ACTOR_ID_CHANGELOG = "changelog"
|
||||
const val KEY_NAME = "name"
|
||||
const val MIN_IMAGE_HEIGHT = 100F
|
||||
const val HORIZONTAL_REACTION_PADDING = 8.0F
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.vanniktech.emoji.EmojiTextView
|
||||
|
||||
class Reaction {
|
||||
|
||||
fun showReactions(
|
||||
message: ChatMessage,
|
||||
clickOnReaction: (message: ChatMessage, emoji: String) -> Unit,
|
||||
longClickOnReaction: (message: ChatMessage) -> Unit,
|
||||
binding: ReactionsInsideMessageBinding,
|
||||
context: Context,
|
||||
isOutgoingMessage: Boolean,
|
||||
viewThemeUtils: ViewThemeUtils,
|
||||
isBubbled: Boolean = true
|
||||
) {
|
||||
binding.reactionsEmojiWrapper.removeAllViews()
|
||||
|
||||
if (message.reactions != null && message.reactions!!.isNotEmpty()) {
|
||||
binding.reactionsEmojiWrapper.visibility = View.VISIBLE
|
||||
|
||||
binding.reactionsEmojiWrapper.setOnLongClickListener {
|
||||
longClickOnReaction(message)
|
||||
true
|
||||
}
|
||||
|
||||
val amountParams = getAmountLayoutParams(context)
|
||||
val wrapperParams = getWrapperLayoutParams(context)
|
||||
|
||||
val paddingSide = DisplayUtils.convertDpToPixel(EMOJI_AND_AMOUNT_PADDING_SIDE, context).toInt()
|
||||
val paddingTop = DisplayUtils.convertDpToPixel(WRAPPER_PADDING_TOP, context).toInt()
|
||||
val paddingBottom = DisplayUtils.convertDpToPixel(WRAPPER_PADDING_BOTTOM, context).toInt()
|
||||
|
||||
for ((emoji, amount) in message.reactions!!) {
|
||||
val isSelfReaction = message.reactionsSelf != null &&
|
||||
message.reactionsSelf!!.isNotEmpty() &&
|
||||
message.reactionsSelf!!.contains(emoji)
|
||||
val textColor = viewThemeUtils.talk.getTextColor(isOutgoingMessage, isSelfReaction, binding)
|
||||
val emojiWithAmountWrapper = getEmojiWithAmountWrapperLayout(
|
||||
binding.reactionsEmojiWrapper.context,
|
||||
emoji,
|
||||
amount,
|
||||
EmojiWithAmountWrapperLayoutInfo(
|
||||
textColor,
|
||||
amountParams,
|
||||
wrapperParams,
|
||||
paddingSide,
|
||||
paddingTop,
|
||||
paddingBottom,
|
||||
viewThemeUtils,
|
||||
isOutgoingMessage,
|
||||
isSelfReaction
|
||||
),
|
||||
isBubbled
|
||||
)
|
||||
|
||||
emojiWithAmountWrapper.setOnClickListener {
|
||||
clickOnReaction(message, emoji)
|
||||
}
|
||||
emojiWithAmountWrapper.setOnLongClickListener {
|
||||
longClickOnReaction(message)
|
||||
false
|
||||
}
|
||||
|
||||
binding.reactionsEmojiWrapper.addView(emojiWithAmountWrapper)
|
||||
}
|
||||
} else {
|
||||
binding.reactionsEmojiWrapper.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEmojiWithAmountWrapperLayout(
|
||||
context: Context,
|
||||
emoji: String,
|
||||
amount: Int,
|
||||
layoutInfo: EmojiWithAmountWrapperLayoutInfo,
|
||||
isBubbled: Boolean
|
||||
): LinearLayout {
|
||||
val emojiWithAmountWrapper = LinearLayout(context)
|
||||
emojiWithAmountWrapper.orientation = LinearLayout.HORIZONTAL
|
||||
|
||||
emojiWithAmountWrapper.addView(getEmojiTextView(context, emoji))
|
||||
emojiWithAmountWrapper.addView(getReactionCount(context, layoutInfo.textColor, amount, layoutInfo.amountParams))
|
||||
emojiWithAmountWrapper.layoutParams = layoutInfo.wrapperParams
|
||||
|
||||
if (layoutInfo.isSelfReaction) {
|
||||
layoutInfo.viewThemeUtils.talk.setCheckedBackground(
|
||||
emojiWithAmountWrapper,
|
||||
layoutInfo.isOutgoingMessage,
|
||||
isBubbled
|
||||
)
|
||||
|
||||
emojiWithAmountWrapper.setPaddingRelative(
|
||||
layoutInfo.paddingSide,
|
||||
layoutInfo.paddingTop,
|
||||
layoutInfo.paddingSide,
|
||||
layoutInfo.paddingBottom
|
||||
)
|
||||
} else {
|
||||
emojiWithAmountWrapper.setPaddingRelative(
|
||||
0,
|
||||
layoutInfo.paddingTop,
|
||||
layoutInfo.paddingSide,
|
||||
layoutInfo.paddingBottom
|
||||
)
|
||||
}
|
||||
return emojiWithAmountWrapper
|
||||
}
|
||||
|
||||
private fun getEmojiTextView(context: Context, emoji: String): EmojiTextView {
|
||||
val reactionEmoji = EmojiTextView(context)
|
||||
reactionEmoji.text = emoji
|
||||
return reactionEmoji
|
||||
}
|
||||
|
||||
private fun getReactionCount(
|
||||
context: Context,
|
||||
textColor: Int,
|
||||
amount: Int,
|
||||
amountParams: LinearLayout.LayoutParams
|
||||
): TextView {
|
||||
val reactionAmount = TextView(context)
|
||||
reactionAmount.setTextColor(textColor)
|
||||
reactionAmount.text = amount.toString()
|
||||
reactionAmount.layoutParams = amountParams
|
||||
return reactionAmount
|
||||
}
|
||||
|
||||
private fun getWrapperLayoutParams(context: Context): LinearLayout.LayoutParams {
|
||||
val wrapperParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
wrapperParams.marginEnd = DisplayUtils.convertDpToPixel(EMOJI_END_MARGIN, context).toInt()
|
||||
return wrapperParams
|
||||
}
|
||||
|
||||
private fun getAmountLayoutParams(context: Context): LinearLayout.LayoutParams {
|
||||
val amountParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
amountParams.marginStart = DisplayUtils.convertDpToPixel(AMOUNT_START_MARGIN, context).toInt()
|
||||
return amountParams
|
||||
}
|
||||
|
||||
private data class EmojiWithAmountWrapperLayoutInfo(
|
||||
val textColor: Int,
|
||||
val amountParams: LinearLayout.LayoutParams,
|
||||
val wrapperParams: LinearLayout.LayoutParams,
|
||||
val paddingSide: Int,
|
||||
val paddingTop: Int,
|
||||
val paddingBottom: Int,
|
||||
val viewThemeUtils: ViewThemeUtils,
|
||||
val isOutgoingMessage: Boolean,
|
||||
val isSelfReaction: Boolean
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val AMOUNT_START_MARGIN: Float = 2F
|
||||
const val EMOJI_END_MARGIN: Float = 6F
|
||||
const val EMOJI_AND_AMOUNT_PADDING_SIDE: Float = 4F
|
||||
const val WRAPPER_PADDING_TOP: Float = 2F
|
||||
const val WRAPPER_PADDING_BOTTOM: Float = 3F
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
|
||||
interface SystemMessageInterface {
|
||||
fun expandSystemMessage(chatMessage: ChatMessage)
|
||||
fun collapseSystemMessages()
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextPaint
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.ViewCompat
|
||||
import autodagger.AutoInjector
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemSystemMessageBinding
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class SystemMessageViewHolder(itemView: View) :
|
||||
MessageHolders
|
||||
.IncomingTextMessageViewHolder<ChatMessage>(itemView) {
|
||||
|
||||
private val binding: ItemSystemMessageBinding = ItemSystemMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var currentUserProvider: CurrentUserProviderNew
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var appPreferences: AppPreferences? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var context: Context? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var dateUtils: DateUtils? = null
|
||||
protected var background: ViewGroup
|
||||
|
||||
lateinit var systemMessageInterface: SystemMessageInterface
|
||||
|
||||
init {
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
background = itemView.findViewById(R.id.container)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
val user = currentUserProvider.currentUser.blockingGet()
|
||||
val resources = itemView.resources
|
||||
val pressedColor: Int = resources.getColor(R.color.bg_message_list_incoming_bubble)
|
||||
val mentionColor: Int = resources.getColor(R.color.textColorMaxContrast)
|
||||
val bubbleDrawable = DisplayUtils.getMessageSelector(
|
||||
resources.getColor(R.color.transparent),
|
||||
resources.getColor(R.color.transparent),
|
||||
pressedColor,
|
||||
R.drawable.shape_grouped_incoming_message
|
||||
)
|
||||
ViewCompat.setBackground(background, bubbleDrawable)
|
||||
binding.messageText.setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX,
|
||||
resources.getDimension(R.dimen.chat_system_message_text_size)
|
||||
)
|
||||
var messageString: Spannable = SpannableString(message.text)
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualMap: Map<String?, String?>? = message.messageParameters!![key]
|
||||
if (individualMap != null && individualMap.containsKey("name")) {
|
||||
var searchText: String? = if ("user" == individualMap["type"] ||
|
||||
"guest" == individualMap["type"] ||
|
||||
"call" == individualMap["type"]
|
||||
) {
|
||||
"@" + individualMap["name"]
|
||||
} else {
|
||||
individualMap["name"]
|
||||
}
|
||||
messageString =
|
||||
DisplayUtils.searchAndColor(
|
||||
messageString,
|
||||
searchText!!,
|
||||
mentionColor,
|
||||
resources.getDimensionPixelSize(R.dimen.chat_system_message_text_size)
|
||||
)
|
||||
if (individualMap["link"] != null) {
|
||||
val displayName = individualMap["name"] ?: ""
|
||||
val link = (user.baseUrl + individualMap["link"])
|
||||
val newStartIndex = messageString.indexOf(displayName)
|
||||
if (newStartIndex != -1) {
|
||||
val clickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context?.startActivity(browserIntent)
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
super.updateDrawState(ds)
|
||||
ds.color = mentionColor
|
||||
ds.isUnderlineText = false
|
||||
}
|
||||
}
|
||||
|
||||
messageString.setSpan(
|
||||
clickableSpan,
|
||||
newStartIndex,
|
||||
newStartIndex + displayName.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.systemMessageLayout.visibility = View.VISIBLE
|
||||
binding.similarMessagesHint.visibility = View.GONE
|
||||
if (message.expandableParent) {
|
||||
processExpandableParent(message, messageString)
|
||||
} else if (message.hiddenByCollapse) {
|
||||
binding.systemMessageLayout.visibility = View.GONE
|
||||
} else {
|
||||
binding.expandCollapseIcon.visibility = View.GONE
|
||||
binding.messageText.text = messageString
|
||||
binding.expandCollapseIcon.setImageDrawable(null)
|
||||
binding.systemMessageLayout.setOnClickListener(null)
|
||||
}
|
||||
|
||||
if (!message.expandableParent && message.lastItemOfExpandableGroup != 0) {
|
||||
binding.systemMessageLayout.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
|
||||
binding.messageText.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
|
||||
}
|
||||
|
||||
binding.messageTime.text = dateUtils!!.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun processExpandableParent(message: ChatMessage, messageString: Spannable) {
|
||||
binding.expandCollapseIcon.visibility = View.VISIBLE
|
||||
|
||||
if (!message.isExpanded) {
|
||||
val similarMessages = sharedApplication!!.resources.getQuantityString(
|
||||
R.plurals.see_similar_system_messages,
|
||||
message.expandableChildrenAmount,
|
||||
message.expandableChildrenAmount
|
||||
)
|
||||
|
||||
binding.messageText.text = messageString
|
||||
binding.messageText.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.similarMessagesHint.visibility = View.VISIBLE
|
||||
binding.similarMessagesHint.text = similarMessages
|
||||
|
||||
binding.expandCollapseIcon.setImageDrawable(
|
||||
ContextCompat.getDrawable(context!!, R.drawable.baseline_unfold_more_24)
|
||||
)
|
||||
binding.systemMessageLayout.setOnClickListener { systemMessageInterface.expandSystemMessage(message) }
|
||||
binding.messageText.setOnClickListener { systemMessageInterface.expandSystemMessage(message) }
|
||||
} else {
|
||||
binding.messageText.text = messageString
|
||||
binding.similarMessagesHint.visibility = View.GONE
|
||||
binding.similarMessagesHint.text = ""
|
||||
|
||||
binding.expandCollapseIcon.setImageDrawable(
|
||||
ContextCompat.getDrawable(context!!, R.drawable.baseline_unfold_less_24)
|
||||
)
|
||||
binding.systemMessageLayout.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
|
||||
binding.messageText.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
|
||||
}
|
||||
}
|
||||
|
||||
fun assignSystemMessageInterface(systemMessageInterface: SystemMessageInterface) {
|
||||
this.systemMessageInterface = systemMessageInterface
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue