Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82fb78e9ea |
+12
-12
@@ -2,9 +2,9 @@ name: Android CI Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "master", "hotfix/*", "feature/*" ]
|
branches: [ "dev", "release/*", "hotfix/*" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "master", "hotfix/*", "feature/*" ]
|
branches: [ "dev", "release/*", "hotfix/*" ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
@@ -12,7 +12,7 @@ env:
|
|||||||
RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}
|
RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_apks:
|
build_apk_aab:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
name: Build artifacts
|
name: Build artifacts
|
||||||
steps:
|
steps:
|
||||||
@@ -29,15 +29,6 @@ jobs:
|
|||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
- name: Build and sign release APK
|
|
||||||
run: ./gradlew assembleRelease
|
|
||||||
|
|
||||||
- name: Upload release APK
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: app-release.apk
|
|
||||||
path: app/build/outputs/apk/release/app-release.apk
|
|
||||||
|
|
||||||
- name: Build and sign debug APK
|
- name: Build and sign debug APK
|
||||||
run: ./gradlew assembleDebug
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
@@ -46,3 +37,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: app-debug.apk
|
name: app-debug.apk
|
||||||
path: app/build/outputs/apk/debug/app-debug.apk
|
path: app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
|
||||||
|
- name: Build and sign release APK
|
||||||
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
|
- name: Upload release APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app-release.apk
|
||||||
|
path: app/build/outputs/apk/release/app-release.apk
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
name: Android CI Release
|
name: Android CI Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
branches: [ "release/*"]
|
branches: [ "master" ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
@@ -27,6 +27,15 @@ jobs:
|
|||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Build and sign debug APK
|
||||||
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
|
- name: Upload debug APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app-debug.apk
|
||||||
|
path: app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
|
||||||
- name: Build and sign release APK
|
- name: Build and sign release APK
|
||||||
run: ./gradlew assembleRelease
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
@@ -36,6 +45,15 @@ jobs:
|
|||||||
name: app-release.apk
|
name: app-release.apk
|
||||||
path: app/build/outputs/apk/release/app-release.apk
|
path: app/build/outputs/apk/release/app-release.apk
|
||||||
|
|
||||||
|
- name: Build and sign debug Bundle
|
||||||
|
run: ./gradlew bundleDebug
|
||||||
|
|
||||||
|
- name: Upload debug Bundle
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app-debug.aab
|
||||||
|
path: app/build/outputs/bundle/debug/app-debug.aab
|
||||||
|
|
||||||
- name: Build and sign release Bundle
|
- name: Build and sign release Bundle
|
||||||
run: ./gradlew bundleRelease
|
run: ./gradlew bundleRelease
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package dev.meloda.fast.presentation
|
package dev.meloda.fast.presentation
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -38,7 +37,6 @@ import dev.chrisbanes.haze.hazeEffect
|
|||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
import dev.meloda.fast.conversations.navigation.Conversations
|
import dev.meloda.fast.conversations.navigation.Conversations
|
||||||
import dev.meloda.fast.conversations.navigation.ConversationsGraph
|
|
||||||
import dev.meloda.fast.conversations.navigation.conversationsGraph
|
import dev.meloda.fast.conversations.navigation.conversationsGraph
|
||||||
import dev.meloda.fast.friends.navigation.Friends
|
import dev.meloda.fast.friends.navigation.Friends
|
||||||
import dev.meloda.fast.friends.navigation.friendsScreen
|
import dev.meloda.fast.friends.navigation.friendsScreen
|
||||||
@@ -73,18 +71,6 @@ fun MainScreen(
|
|||||||
mutableIntStateOf(1)
|
mutableIntStateOf(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(enabled = selectedItemIndex != 1) {
|
|
||||||
val index = 1
|
|
||||||
val currentRoute = navigationItems[selectedItemIndex].route
|
|
||||||
|
|
||||||
selectedItemIndex = 1
|
|
||||||
navController.navigate(navigationItems[index].route) {
|
|
||||||
popUpTo(route = currentRoute) {
|
|
||||||
inclusive = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val user = LocalUser.current
|
val user = LocalUser.current
|
||||||
val profileImageUrl by remember(user) {
|
val profileImageUrl by remember(user) {
|
||||||
derivedStateOf { user?.photo100 }
|
derivedStateOf { user?.photo100 }
|
||||||
@@ -209,7 +195,7 @@ fun MainScreen(
|
|||||||
onNavigateToCreateChat = onNavigateToCreateChat,
|
onNavigateToCreateChat = onNavigateToCreateChat,
|
||||||
onScrolledToTop = {
|
onScrolledToTop = {
|
||||||
tabReselected = tabReselected.toMutableMap().also {
|
tabReselected = tabReselected.toMutableMap().also {
|
||||||
it[ConversationsGraph] = false
|
it[Conversations] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,20 +23,6 @@ fun <T> MutableList<T>.addIf(element: T, condition: () -> Boolean) {
|
|||||||
if (condition.invoke()) add(element)
|
if (condition.invoke()) add(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> MutableList<T>.removeIfCompat(condition: (T) -> Boolean): Boolean {
|
|
||||||
var removed = false
|
|
||||||
|
|
||||||
val each = iterator()
|
|
||||||
while (each.hasNext()) {
|
|
||||||
if (condition(each.next())) {
|
|
||||||
each.remove()
|
|
||||||
removed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> Flow<T>.listenValue(
|
fun <T> Flow<T>.listenValue(
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
action: suspend (T) -> Unit
|
action: suspend (T) -> Unit
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ interface MessagesRepository {
|
|||||||
randomId: Long,
|
randomId: Long,
|
||||||
message: String?,
|
message: String?,
|
||||||
replyTo: Long?,
|
replyTo: Long?,
|
||||||
attachments: List<VkAttachment>?,
|
attachments: List<VkAttachment>?
|
||||||
formatData: VkMessage.FormatData?
|
|
||||||
): ApiResult<MessagesSendResponse, RestApiErrorDomain>
|
): ApiResult<MessagesSendResponse, RestApiErrorDomain>
|
||||||
|
|
||||||
suspend fun markAsRead(
|
suspend fun markAsRead(
|
||||||
|
|||||||
+2
-4
@@ -184,16 +184,14 @@ class MessagesRepositoryImpl(
|
|||||||
randomId: Long,
|
randomId: Long,
|
||||||
message: String?,
|
message: String?,
|
||||||
replyTo: Long?,
|
replyTo: Long?,
|
||||||
attachments: List<VkAttachment>?,
|
attachments: List<VkAttachment>?
|
||||||
formatData: VkMessage.FormatData?
|
|
||||||
): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||||
val requestModel = MessagesSendRequest(
|
val requestModel = MessagesSendRequest(
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
randomId = randomId,
|
randomId = randomId,
|
||||||
message = message,
|
message = message,
|
||||||
replyTo = replyTo,
|
replyTo = replyTo,
|
||||||
attachments = attachments,
|
attachments = attachments
|
||||||
formatData = formatData
|
|
||||||
)
|
)
|
||||||
|
|
||||||
messagesService.send(requestModel.map).mapApiDefault()
|
messagesService.send(requestModel.map).mapApiDefault()
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 3,
|
|
||||||
"identityHash": "ca007bca2ab4a9b901662792042770ad",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "accounts",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, `exchangeToken` TEXT, PRIMARY KEY(`userId`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "userId",
|
|
||||||
"columnName": "userId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "accessToken",
|
|
||||||
"columnName": "accessToken",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "fastToken",
|
|
||||||
"columnName": "fastToken",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "trustedHash",
|
|
||||||
"columnName": "trustedHash",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "exchangeToken",
|
|
||||||
"columnName": "exchangeToken",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"userId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"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, 'ca007bca2ab4a9b901662792042770ad')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,454 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 10,
|
|
||||||
"identityHash": "fa307a5eb2e1f7d601bd1374174635cd",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "users",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `isOnline` INTEGER NOT NULL, `isOnlineMobile` INTEGER NOT NULL, `onlineAppId` INTEGER, `lastSeen` INTEGER, `lastSeenStatus` TEXT, `birthday` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `photo400Orig` TEXT, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "firstName",
|
|
||||||
"columnName": "firstName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastName",
|
|
||||||
"columnName": "lastName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isOnline",
|
|
||||||
"columnName": "isOnline",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isOnlineMobile",
|
|
||||||
"columnName": "isOnlineMobile",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "onlineAppId",
|
|
||||||
"columnName": "onlineAppId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastSeen",
|
|
||||||
"columnName": "lastSeen",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastSeenStatus",
|
|
||||||
"columnName": "lastSeenStatus",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "birthday",
|
|
||||||
"columnName": "birthday",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo50",
|
|
||||||
"columnName": "photo50",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo100",
|
|
||||||
"columnName": "photo100",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo200",
|
|
||||||
"columnName": "photo200",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo400Orig",
|
|
||||||
"columnName": "photo400Orig",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "groups",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "screenName",
|
|
||||||
"columnName": "screenName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo50",
|
|
||||||
"columnName": "photo50",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo100",
|
|
||||||
"columnName": "photo100",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo200",
|
|
||||||
"columnName": "photo200",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "membersCount",
|
|
||||||
"columnName": "membersCount",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "messages",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `conversationMessageId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionConversationMessageId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "conversationMessageId",
|
|
||||||
"columnName": "conversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "text",
|
|
||||||
"columnName": "text",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isOut",
|
|
||||||
"columnName": "isOut",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "peerId",
|
|
||||||
"columnName": "peerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "fromId",
|
|
||||||
"columnName": "fromId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "date",
|
|
||||||
"columnName": "date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "randomId",
|
|
||||||
"columnName": "randomId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "action",
|
|
||||||
"columnName": "action",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionMemberId",
|
|
||||||
"columnName": "actionMemberId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionText",
|
|
||||||
"columnName": "actionText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionConversationMessageId",
|
|
||||||
"columnName": "actionConversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "actionMessage",
|
|
||||||
"columnName": "actionMessage",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "updateTime",
|
|
||||||
"columnName": "updateTime",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "important",
|
|
||||||
"columnName": "important",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "forwardIds",
|
|
||||||
"columnName": "forwardIds",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "attachments",
|
|
||||||
"columnName": "attachments",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "replyMessageId",
|
|
||||||
"columnName": "replyMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "geoType",
|
|
||||||
"columnName": "geoType",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedAt",
|
|
||||||
"columnName": "pinnedAt",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPinned",
|
|
||||||
"columnName": "isPinned",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "conversations",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "localId",
|
|
||||||
"columnName": "localId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ownerId",
|
|
||||||
"columnName": "ownerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo50",
|
|
||||||
"columnName": "photo50",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo100",
|
|
||||||
"columnName": "photo100",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photo200",
|
|
||||||
"columnName": "photo200",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPhantom",
|
|
||||||
"columnName": "isPhantom",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastConversationMessageId",
|
|
||||||
"columnName": "lastConversationMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "inReadCmId",
|
|
||||||
"columnName": "inReadCmId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "outReadCmId",
|
|
||||||
"columnName": "outReadCmId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "inRead",
|
|
||||||
"columnName": "inRead",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "outRead",
|
|
||||||
"columnName": "outRead",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastMessageId",
|
|
||||||
"columnName": "lastMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "unreadCount",
|
|
||||||
"columnName": "unreadCount",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "membersCount",
|
|
||||||
"columnName": "membersCount",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "canChangePin",
|
|
||||||
"columnName": "canChangePin",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "canChangeInfo",
|
|
||||||
"columnName": "canChangeInfo",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "majorId",
|
|
||||||
"columnName": "majorId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "minorId",
|
|
||||||
"columnName": "minorId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "pinnedMessageId",
|
|
||||||
"columnName": "pinnedMessageId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "peerType",
|
|
||||||
"columnName": "peerType",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isArchived",
|
|
||||||
"columnName": "isArchived",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"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, 'fa307a5eb2e1f7d601bd1374174635cd')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,8 +33,7 @@ interface MessagesUseCase : BaseUseCase {
|
|||||||
randomId: Long,
|
randomId: Long,
|
||||||
message: String?,
|
message: String?,
|
||||||
replyTo: Long?,
|
replyTo: Long?,
|
||||||
attachments: List<VkAttachment>?,
|
attachments: List<VkAttachment>?
|
||||||
formatData: VkMessage.FormatData?
|
|
||||||
): Flow<State<MessagesSendResponse>>
|
): Flow<State<MessagesSendResponse>>
|
||||||
|
|
||||||
fun markAsRead(
|
fun markAsRead(
|
||||||
|
|||||||
@@ -57,16 +57,14 @@ class MessagesUseCaseImpl(
|
|||||||
randomId: Long,
|
randomId: Long,
|
||||||
message: String?,
|
message: String?,
|
||||||
replyTo: Long?,
|
replyTo: Long?,
|
||||||
attachments: List<VkAttachment>?,
|
attachments: List<VkAttachment>?
|
||||||
formatData: VkMessage.FormatData?
|
|
||||||
): Flow<State<MessagesSendResponse>> = flowNewState {
|
): Flow<State<MessagesSendResponse>> = flowNewState {
|
||||||
repository.send(
|
repository.send(
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
randomId = randomId,
|
randomId = randomId,
|
||||||
message = message,
|
message = message,
|
||||||
replyTo = replyTo,
|
replyTo = replyTo,
|
||||||
attachments = attachments,
|
attachments = attachments
|
||||||
formatData = formatData
|
|
||||||
).mapToState()
|
).mapToState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package dev.meloda.fast.model
|
|
||||||
|
|
||||||
data class PhotoSize(
|
|
||||||
val height: Int,
|
|
||||||
val width: Int,
|
|
||||||
val type: String,
|
|
||||||
val url: String
|
|
||||||
)
|
|
||||||
@@ -6,8 +6,8 @@ enum class AttachmentType(var value: String) {
|
|||||||
UNKNOWN("unknown"),
|
UNKNOWN("unknown"),
|
||||||
PHOTO("photo"),
|
PHOTO("photo"),
|
||||||
VIDEO("video"),
|
VIDEO("video"),
|
||||||
FILE("doc"),
|
|
||||||
AUDIO("audio"),
|
AUDIO("audio"),
|
||||||
|
FILE("doc"),
|
||||||
LINK("link"),
|
LINK("link"),
|
||||||
AUDIO_MESSAGE("audio_message"),
|
AUDIO_MESSAGE("audio_message"),
|
||||||
MINI_APP("mini_app"),
|
MINI_APP("mini_app"),
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ data class VkFileData(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Photo(
|
data class Photo(val sizes: List<Size>) {
|
||||||
val sizes: List<Size>
|
|
||||||
) {
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Size(
|
data class Size(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package dev.meloda.fast.model.api.data
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import dev.meloda.fast.model.PhotoSize
|
|
||||||
import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
@@ -36,14 +35,7 @@ data class VkPhotoData(
|
|||||||
ownerId = ownerId,
|
ownerId = ownerId,
|
||||||
hasTags = hasTags == true,
|
hasTags = hasTags == true,
|
||||||
accessKey = accessKey,
|
accessKey = accessKey,
|
||||||
sizes = sizes.map { size ->
|
sizes = sizes,
|
||||||
PhotoSize(
|
|
||||||
height = size.height,
|
|
||||||
width = size.width,
|
|
||||||
type = size.type,
|
|
||||||
url = size.url
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = text,
|
text = text,
|
||||||
userId = userId
|
userId = userId
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ data class VkVideoData(
|
|||||||
@Json(name = "is_favorite") val isFavorite: Boolean?,
|
@Json(name = "is_favorite") val isFavorite: Boolean?,
|
||||||
@Json(name = "image") val image: List<Image>?,
|
@Json(name = "image") val image: List<Image>?,
|
||||||
@Json(name = "first_frame") val firstFrame: List<FirstFrame>?,
|
@Json(name = "first_frame") val firstFrame: List<FirstFrame>?,
|
||||||
@Json(name = "files") val files: File?,
|
@Json(name = "files") val files: File?
|
||||||
) : VkAttachmentData {
|
) : VkAttachmentData {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
@@ -73,7 +73,6 @@ data class VkVideoData(
|
|||||||
accessKey = accessKey,
|
accessKey = accessKey,
|
||||||
title = title,
|
title = title,
|
||||||
views = views,
|
views = views,
|
||||||
duration = duration,
|
duration = duration
|
||||||
isShortVideo = type == "short_video"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ data class VkWallReplyData(
|
|||||||
val from_id: Long,
|
val from_id: Long,
|
||||||
val date: Int,
|
val date: Int,
|
||||||
val text: String,
|
val text: String,
|
||||||
val post_id: Long?,
|
val post_id: Long,
|
||||||
val owner_id: Long?,
|
val owner_id: Long,
|
||||||
val parents_stack: List<Int>?,
|
val parents_stack: List<Int>,
|
||||||
val likes: Likes?,
|
val likes: Likes,
|
||||||
val reply_to_user: Int?,
|
val reply_to_user: Int?,
|
||||||
val reply_to_comment: Int?
|
val reply_to_comment: Int?
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ package dev.meloda.fast.model.api.domain
|
|||||||
enum class FormatDataType {
|
enum class FormatDataType {
|
||||||
BOLD, ITALIC, UNDERLINE, URL;
|
BOLD, ITALIC, UNDERLINE, URL;
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return super.toString().lowercase()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(value: String): FormatDataType? =
|
fun parse(value: String): FormatDataType? =
|
||||||
entries.firstOrNull { it.name.lowercase() == value }
|
entries.firstOrNull { it.name.lowercase() == value }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.meloda.fast.model.api.domain
|
package dev.meloda.fast.model.api.domain
|
||||||
|
|
||||||
import dev.meloda.fast.model.PhotoSize
|
|
||||||
import dev.meloda.fast.model.api.data.AttachmentType
|
import dev.meloda.fast.model.api.data.AttachmentType
|
||||||
|
import dev.meloda.fast.model.api.data.VkPhotoData
|
||||||
import java.util.Stack
|
import java.util.Stack
|
||||||
|
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ data class VkPhotoDomain(
|
|||||||
val ownerId: Long,
|
val ownerId: Long,
|
||||||
val hasTags: Boolean,
|
val hasTags: Boolean,
|
||||||
val accessKey: String?,
|
val accessKey: String?,
|
||||||
val sizes: List<PhotoSize>,
|
val sizes: List<VkPhotoData.Size>,
|
||||||
val text: String?,
|
val text: String?,
|
||||||
val userId: Long?
|
val userId: Long?
|
||||||
) : VkAttachment {
|
) : VkAttachment {
|
||||||
@@ -35,15 +35,11 @@ data class VkPhotoDomain(
|
|||||||
sizesChars.push(SIZE_TYPE_2560_2048)
|
sizesChars.push(SIZE_TYPE_2560_2048)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMaxSize(): PhotoSize? {
|
fun getMaxSize(): VkPhotoData.Size? {
|
||||||
return getSizeOrSmaller(sizesChars.peek())
|
return getSizeOrSmaller(sizesChars.peek())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDefault(): PhotoSize? {
|
fun getSizeOrNull(type: Char): VkPhotoData.Size? {
|
||||||
return getSizeOrSmaller(SIZE_TYPE_1080_1024)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSizeOrNull(type: Char): PhotoSize? {
|
|
||||||
for (size in sizes) {
|
for (size in sizes) {
|
||||||
if (size.type == type.toString()) return size
|
if (size.type == type.toString()) return size
|
||||||
}
|
}
|
||||||
@@ -51,7 +47,7 @@ data class VkPhotoDomain(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSizeOrSmaller(type: Char): PhotoSize? {
|
fun getSizeOrSmaller(type: Char): VkPhotoData.Size? {
|
||||||
val photoStack = sizesChars.clone() as Stack<*>
|
val photoStack = sizesChars.clone() as Stack<*>
|
||||||
|
|
||||||
val sizeIndex = photoStack.search(type)
|
val sizeIndex = photoStack.search(type)
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ data class VkVideoDomain(
|
|||||||
val accessKey: String?,
|
val accessKey: String?,
|
||||||
val title: String,
|
val title: String,
|
||||||
val views: Int,
|
val views: Int,
|
||||||
val duration: Int,
|
val duration: Int
|
||||||
val isShortVideo: Boolean
|
|
||||||
) : VkAttachment {
|
) : VkAttachment {
|
||||||
|
|
||||||
override val type: AttachmentType = AttachmentType.VIDEO
|
override val type: AttachmentType = AttachmentType.VIDEO
|
||||||
@@ -23,10 +22,6 @@ data class VkVideoDomain(
|
|||||||
return images.find { it.width == width }
|
return images.find { it.width == width }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDefault(): VideoImage? {
|
|
||||||
return imageForWidthAtLeast(720)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun imageForWidthAtLeast(width: Int): VideoImage? {
|
fun imageForWidthAtLeast(width: Int): VideoImage? {
|
||||||
var certainImages = images.sortedByDescending { it.width }
|
var certainImages = images.sortedByDescending { it.width }
|
||||||
var containsVertical = false
|
var containsVertical = false
|
||||||
@@ -41,11 +36,9 @@ data class VkVideoDomain(
|
|||||||
certainImages = certainImages.filter { it.shapeKind == ShapeKind.Vertical }
|
certainImages = certainImages.filter { it.shapeKind == ShapeKind.Vertical }
|
||||||
}
|
}
|
||||||
|
|
||||||
val filteredCertainImages = certainImages.filter { it.width >= width }
|
certainImages = certainImages.filter { it.width >= width }
|
||||||
|
|
||||||
return filteredCertainImages
|
return certainImages.firstOrNull()
|
||||||
.ifEmpty { certainImages }
|
|
||||||
.firstOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package dev.meloda.fast.model.api.requests
|
|||||||
|
|
||||||
import dev.meloda.fast.model.api.asInt
|
import dev.meloda.fast.model.api.asInt
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
|
||||||
|
|
||||||
data class MessagesGetHistoryRequest(
|
data class MessagesGetHistoryRequest(
|
||||||
val count: Int? = null,
|
val count: Int? = null,
|
||||||
@@ -39,8 +38,7 @@ data class MessagesSendRequest(
|
|||||||
val disableMentions: Boolean? = null,
|
val disableMentions: Boolean? = null,
|
||||||
val doNotParseLinks: Boolean? = null,
|
val doNotParseLinks: Boolean? = null,
|
||||||
val silent: Boolean? = null,
|
val silent: Boolean? = null,
|
||||||
val attachments: List<VkAttachment>? = null,
|
val attachments: List<VkAttachment>? = null
|
||||||
val formatData: VkMessage.FormatData? = null
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val map: Map<String, String>
|
val map: Map<String, String>
|
||||||
@@ -56,13 +54,6 @@ data class MessagesSendRequest(
|
|||||||
disableMentions?.let { this["disable_mentions"] = it.asInt().toString() }
|
disableMentions?.let { this["disable_mentions"] = it.asInt().toString() }
|
||||||
doNotParseLinks?.let { this["dont_parse_links"] = it.asInt().toString() }
|
doNotParseLinks?.let { this["dont_parse_links"] = it.asInt().toString() }
|
||||||
silent?.let { this["silent"] = it.toString() }
|
silent?.let { this["silent"] = it.toString() }
|
||||||
formatData?.let {
|
|
||||||
this["format_data"] = "{\"version\":\"${formatData.version}\",\"items\":[" +
|
|
||||||
formatData.items.joinToString(separator = ", ") { item ->
|
|
||||||
"{\"type\":\"${item.type}\",\"offset\":${item.offset},\"length\":${item.length}}"
|
|
||||||
} +
|
|
||||||
"]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 05/05/2024, Danil Nikolaev: add attachments
|
// TODO: 05/05/2024, Danil Nikolaev: add attachments
|
||||||
// attachments?.let {
|
// attachments?.let {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package dev.meloda.fast.ui.util
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class ImmutableList<T>(val values: List<T>) : Collection<T> {
|
class ImmutableList<T>(val values: List<T>) : Iterable<T> {
|
||||||
|
|
||||||
constructor(size: Int, init: (index: Int) -> T) : this(MutableList(size, init))
|
constructor(size: Int, init: (index: Int) -> T) : this(MutableList(size, init))
|
||||||
|
|
||||||
@@ -25,18 +25,30 @@ class ImmutableList<T>(val values: List<T>) : Collection<T> {
|
|||||||
return values.mapIndexed(transform).toImmutableList()
|
return values.mapIndexed(transform).toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isEmpty(): Boolean = values.isEmpty()
|
fun singleOrNull(): T? {
|
||||||
|
return if (values.size == 1) this[0] else null
|
||||||
override val size: Int get() = values.size
|
|
||||||
|
|
||||||
override fun containsAll(elements: Collection<T>): Boolean {
|
|
||||||
return values.containsAll(elements)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun contains(element: T): Boolean {
|
fun isEmpty(): Boolean = values.isEmpty()
|
||||||
return values.contains(element)
|
|
||||||
|
fun isNotEmpty(): Boolean = !isEmpty()
|
||||||
|
|
||||||
|
inline fun singleOrNull(predicate: (T) -> Boolean): T? {
|
||||||
|
var single: T? = null
|
||||||
|
var found = false
|
||||||
|
for (element in this) {
|
||||||
|
if (predicate(element)) {
|
||||||
|
if (found) return null
|
||||||
|
single = element
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) return null
|
||||||
|
return single
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val size: Int get() = values.size
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun <T> copyOf(collection: Collection<T>): ImmutableList<T> =
|
fun <T> copyOf(collection: Collection<T>): ImmutableList<T> =
|
||||||
ImmutableList(collection.toList())
|
ImmutableList(collection.toList())
|
||||||
@@ -55,7 +67,3 @@ class ImmutableList<T>(val values: List<T>) : Collection<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun <T> emptyImmutableList(): ImmutableList<T> = ImmutableList(emptyList())
|
fun <T> emptyImmutableList(): ImmutableList<T> = ImmutableList(emptyList())
|
||||||
|
|
||||||
fun <T> immutableListOf(vararg elements: T) = ImmutableList(listOf(elements = elements))
|
|
||||||
|
|
||||||
fun <T> ImmutableList<T>?.orEmpty(): ImmutableList<T> = this ?: emptyImmutableList()
|
|
||||||
|
|||||||
@@ -107,7 +107,6 @@
|
|||||||
<string name="message_attachments_files_few">%1$d файла</string>
|
<string name="message_attachments_files_few">%1$d файла</string>
|
||||||
<string name="message_attachments_files_many">%1$d файлов</string>
|
<string name="message_attachments_files_many">%1$d файлов</string>
|
||||||
<string name="message_attachments_files_other">%1$d файлов</string>
|
<string name="message_attachments_files_other">%1$d файлов</string>
|
||||||
<string name="message_attachments_clip">Клип</string>
|
|
||||||
<string name="message_attachments_audio_message">Голосовое сообщение</string>
|
<string name="message_attachments_audio_message">Голосовое сообщение</string>
|
||||||
<string name="message_attachments_link">Ссылка</string>
|
<string name="message_attachments_link">Ссылка</string>
|
||||||
<string name="message_attachments_mini_app">Мини-приложение</string>
|
<string name="message_attachments_mini_app">Мини-приложение</string>
|
||||||
@@ -264,9 +263,4 @@
|
|||||||
<string name="conversation_context_action_archive">В архив</string>
|
<string name="conversation_context_action_archive">В архив</string>
|
||||||
<string name="confirm_archive_conversation">Архивировать чат?</string>
|
<string name="confirm_archive_conversation">Архивировать чат?</string>
|
||||||
<string name="action_archive">В архив</string>
|
<string name="action_archive">В архив</string>
|
||||||
<string name="autofill">Автозаполнение</string>
|
|
||||||
<string name="bold">Жирный</string>
|
|
||||||
<string name="italic">Курсив</string>
|
|
||||||
<string name="underline">Подчёркнутый</string>
|
|
||||||
<string name="link">Ссылка</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -83,7 +83,6 @@
|
|||||||
<string name="message_attachments_files_many">%1$d files</string>
|
<string name="message_attachments_files_many">%1$d files</string>
|
||||||
<string name="message_attachments_files_other">%1$d files</string>
|
<string name="message_attachments_files_other">%1$d files</string>
|
||||||
|
|
||||||
<string name="message_attachments_clip">Clip</string>
|
|
||||||
<string name="message_attachments_audio_message">Voice message</string>
|
<string name="message_attachments_audio_message">Voice message</string>
|
||||||
<string name="message_attachments_link">Link</string>
|
<string name="message_attachments_link">Link</string>
|
||||||
<string name="message_attachments_mini_app">Mini App</string>
|
<string name="message_attachments_mini_app">Mini App</string>
|
||||||
@@ -339,10 +338,4 @@
|
|||||||
<string name="unspam_message_title">Unmark as spam</string>
|
<string name="unspam_message_title">Unmark as spam</string>
|
||||||
<string name="unspam_message_text">Are you sure you want to unmark this message as spam?</string>
|
<string name="unspam_message_text">Are you sure you want to unmark this message as spam?</string>
|
||||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||||
|
|
||||||
<string name="autofill">Autofill</string>
|
|
||||||
<string name="bold">Bold</string>
|
|
||||||
<string name="italic">Italic</string>
|
|
||||||
<string name="underline">Underline</string>
|
|
||||||
<string name="link">Link</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ plugins {
|
|||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
onVariants { variant ->
|
onVariants { variant ->
|
||||||
variant.buildConfigFields?.apply {
|
variant.buildConfigFields.apply {
|
||||||
put(
|
put(
|
||||||
"sdkPackage",
|
"sdkPackage",
|
||||||
BuildConfigField(
|
BuildConfigField(
|
||||||
@@ -46,6 +46,13 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 29-Mar-25, Danil Nikolaev: remove when autofill changes will be in release
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy {
|
||||||
|
force(libs.compose.ui)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "dev.meloda.fast.auth"
|
namespace = "dev.meloda.fast.auth"
|
||||||
|
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ import androidx.compose.ui.platform.testTag
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.contentType
|
import androidx.compose.ui.semantics.contentType
|
||||||
import androidx.compose.ui.semantics.password
|
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.meloda.fast.auth.login.LoginViewModel
|
import dev.meloda.fast.auth.login.LoginViewModel
|
||||||
import dev.meloda.fast.auth.login.LoginViewModelImpl
|
import dev.meloda.fast.auth.login.LoginViewModelImpl
|
||||||
@@ -57,12 +57,14 @@ import dev.meloda.fast.auth.login.model.LoginDialog
|
|||||||
import dev.meloda.fast.auth.login.model.LoginScreenState
|
import dev.meloda.fast.auth.login.model.LoginScreenState
|
||||||
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||||
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
||||||
|
import dev.meloda.fast.datastore.UserSettings
|
||||||
import dev.meloda.fast.ui.components.MaterialDialog
|
import dev.meloda.fast.ui.components.MaterialDialog
|
||||||
import dev.meloda.fast.ui.components.TextFieldErrorText
|
import dev.meloda.fast.ui.components.TextFieldErrorText
|
||||||
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
||||||
import dev.meloda.fast.ui.util.handleEnterKey
|
import dev.meloda.fast.ui.util.handleEnterKey
|
||||||
import dev.meloda.fast.ui.util.handleTabKey
|
import dev.meloda.fast.ui.util.handleTabKey
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import org.koin.compose.koinInject
|
||||||
import dev.meloda.fast.ui.R as UiR
|
import dev.meloda.fast.ui.R as UiR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -37,7 +36,7 @@ fun Logo(modifier: Modifier = Modifier) {
|
|||||||
val size = LocalSizeConfig.current
|
val size = LocalSizeConfig.current
|
||||||
|
|
||||||
val iconWidth by animateDpAsState(if (size.isWidthSmall) 110.dp else 134.dp)
|
val iconWidth by animateDpAsState(if (size.isWidthSmall) 110.dp else 134.dp)
|
||||||
val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 32 else 38)
|
val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 32 else 40)
|
||||||
val bottomAdditionalPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp)
|
val bottomAdditionalPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp)
|
||||||
|
|
||||||
val userSettings: UserSettings = koinInject()
|
val userSettings: UserSettings = koinInject()
|
||||||
@@ -79,8 +78,7 @@ fun Logo(modifier: Modifier = Modifier) {
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.fast_messenger),
|
text = stringResource(id = R.string.fast_messenger),
|
||||||
style = MaterialTheme.typography.displayMedium.copy(fontSize = appNameFontSize.sp),
|
style = MaterialTheme.typography.displayMedium.copy(fontSize = appNameFontSize.sp),
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
package dev.meloda.fast.chatmaterials.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatMaterialItem(
|
||||||
|
item: UiChatMaterial,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
when (item) {
|
||||||
|
is UiChatMaterial.Photo -> {
|
||||||
|
AsyncImage(
|
||||||
|
model = item.previewUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is UiChatMaterial.Video -> {
|
||||||
|
AsyncImage(
|
||||||
|
model = item.previewUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is UiChatMaterial.Audio -> {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = item.title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
Text(text = item.artist)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(text = item.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is UiChatMaterial.File -> {}
|
||||||
|
|
||||||
|
is UiChatMaterial.Link -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-2
@@ -63,7 +63,6 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||||
import dev.meloda.fast.conversations.navigation.Conversations
|
import dev.meloda.fast.conversations.navigation.Conversations
|
||||||
import dev.meloda.fast.conversations.navigation.ConversationsGraph
|
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
import dev.meloda.fast.ui.components.NoItemsView
|
||||||
@@ -117,7 +116,7 @@ fun ConversationsScreen(
|
|||||||
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
|
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
|
||||||
)
|
)
|
||||||
|
|
||||||
val currentTabReselected = LocalReselectedTab.current[ConversationsGraph] ?: false
|
val currentTabReselected = LocalReselectedTab.current[Conversations] ?: false
|
||||||
LaunchedEffect(currentTabReselected) {
|
LaunchedEffect(currentTabReselected) {
|
||||||
if (currentTabReselected) {
|
if (currentTabReselected) {
|
||||||
if (screenState.isArchive) {
|
if (screenState.isArchive) {
|
||||||
|
|||||||
-7
@@ -22,7 +22,6 @@ import dev.meloda.fast.model.api.data.AttachmentType
|
|||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
import dev.meloda.fast.model.api.domain.VkConversation
|
import dev.meloda.fast.model.api.domain.VkConversation
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
import dev.meloda.fast.model.api.domain.VkVideoDomain
|
|
||||||
import dev.meloda.fast.ui.model.api.ActionState
|
import dev.meloda.fast.ui.model.api.ActionState
|
||||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||||
import dev.meloda.fast.ui.model.api.UiConversation
|
import dev.meloda.fast.ui.model.api.UiConversation
|
||||||
@@ -729,12 +728,6 @@ private fun getAttachmentUiText(
|
|||||||
attachment: VkAttachment,
|
attachment: VkAttachment,
|
||||||
size: Int = 1,
|
size: Int = 1,
|
||||||
): UiText {
|
): UiText {
|
||||||
if (attachment.type == AttachmentType.VIDEO &&
|
|
||||||
(attachment as? VkVideoDomain)?.isShortVideo == true
|
|
||||||
) {
|
|
||||||
return UiText.Resource(UiR.string.message_attachments_clip)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attachment.type.isMultiple()) {
|
if (attachment.type.isMultiple()) {
|
||||||
return when (attachment.type) {
|
return when (attachment.type) {
|
||||||
AttachmentType.PHOTO -> UiR.plurals.attachment_photos
|
AttachmentType.PHOTO -> UiR.plurals.attachment_photos
|
||||||
|
|||||||
+24
-21
@@ -47,27 +47,30 @@ fun FriendItem(
|
|||||||
val friendAvatar = friend.avatar?.extractUrl()
|
val friendAvatar = friend.avatar?.extractUrl()
|
||||||
|
|
||||||
Box(modifier = Modifier.size(56.dp)) {
|
Box(modifier = Modifier.size(56.dp)) {
|
||||||
Image(
|
if (friendAvatar == null) {
|
||||||
modifier = Modifier
|
Image(
|
||||||
.fillMaxSize()
|
modifier = Modifier
|
||||||
.clip(CircleShape),
|
.fillMaxSize()
|
||||||
painter = painterResource(id = R.drawable.ic_account_circle_cut),
|
.clip(CircleShape),
|
||||||
contentDescription = "Avatar",
|
painter = painterResource(id = R.drawable.ic_account_circle_cut),
|
||||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
|
contentDescription = "Avatar",
|
||||||
)
|
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
|
||||||
AsyncImage(
|
)
|
||||||
model = friendAvatar,
|
} else {
|
||||||
contentDescription = null,
|
AsyncImage(
|
||||||
modifier = Modifier
|
model = friendAvatar,
|
||||||
.fillMaxSize()
|
contentDescription = null,
|
||||||
.clip(CircleShape)
|
modifier = Modifier
|
||||||
.clickable {
|
.fillMaxSize()
|
||||||
friend.photo400Orig
|
.clip(CircleShape)
|
||||||
?.extractUrl()
|
.clickable {
|
||||||
?.let(onPhotoClicked)
|
friend.photo400Orig
|
||||||
},
|
?.extractUrl()
|
||||||
placeholder = painterResource(id = R.drawable.ic_account_circle_cut)
|
?.let(onPhotoClicked)
|
||||||
)
|
},
|
||||||
|
placeholder = painterResource(id = R.drawable.ic_account_circle_cut)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (friend.onlineStatus.isOnline()) {
|
if (friend.onlineStatus.isOnline()) {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
+4
-130
@@ -7,15 +7,9 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.StringAnnotation
|
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -25,7 +19,6 @@ import com.conena.nanokt.text.isNotEmptyOrBlank
|
|||||||
import dev.meloda.fast.common.VkConstants
|
import dev.meloda.fast.common.VkConstants
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
import dev.meloda.fast.common.extensions.listenValue
|
||||||
import dev.meloda.fast.common.extensions.orDots
|
import dev.meloda.fast.common.extensions.orDots
|
||||||
import dev.meloda.fast.common.extensions.removeIfCompat
|
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
import dev.meloda.fast.common.provider.ResourceProvider
|
import dev.meloda.fast.common.provider.ResourceProvider
|
||||||
import dev.meloda.fast.data.State
|
import dev.meloda.fast.data.State
|
||||||
@@ -50,7 +43,6 @@ import dev.meloda.fast.messageshistory.util.extractAvatar
|
|||||||
import dev.meloda.fast.messageshistory.util.extractTitle
|
import dev.meloda.fast.messageshistory.util.extractTitle
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.model.LongPollParsedEvent
|
import dev.meloda.fast.model.LongPollParsedEvent
|
||||||
import dev.meloda.fast.model.api.domain.FormatDataType
|
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
import dev.meloda.fast.network.VkErrorCode
|
import dev.meloda.fast.network.VkErrorCode
|
||||||
@@ -106,11 +98,6 @@ interface MessagesHistoryViewModel {
|
|||||||
fun onUnpinMessageClicked()
|
fun onUnpinMessageClicked()
|
||||||
|
|
||||||
fun onDeleteSelectedMessagesClicked()
|
fun onDeleteSelectedMessagesClicked()
|
||||||
|
|
||||||
fun onBoldClicked()
|
|
||||||
fun onItalicClicked()
|
|
||||||
fun onUnderlineClicked()
|
|
||||||
fun onLinkClicked()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessagesHistoryViewModelImpl(
|
class MessagesHistoryViewModelImpl(
|
||||||
@@ -359,7 +346,8 @@ class MessagesHistoryViewModelImpl(
|
|||||||
else ActionMode.Send
|
else ActionMode.Send
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
updateStyles()
|
|
||||||
|
screenState.setValue { old -> old.copy(message = newText) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEmojiButtonLongClicked() {
|
override fun onEmojiButtonLongClicked() {
|
||||||
@@ -459,118 +447,6 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var formatData = VkMessage.FormatData("1", emptyList())
|
|
||||||
|
|
||||||
private fun updateStyles() {
|
|
||||||
val annotations =
|
|
||||||
mutableListOf<AnnotatedString.Range<out AnnotatedString.Annotation>>()
|
|
||||||
|
|
||||||
formatData.items.forEachIndexed { index, item ->
|
|
||||||
val spanStyle = when (item.type) {
|
|
||||||
FormatDataType.BOLD -> {
|
|
||||||
SpanStyle(fontWeight = FontWeight.SemiBold)
|
|
||||||
}
|
|
||||||
|
|
||||||
FormatDataType.ITALIC -> {
|
|
||||||
SpanStyle(fontStyle = FontStyle.Italic)
|
|
||||||
}
|
|
||||||
|
|
||||||
FormatDataType.UNDERLINE -> {
|
|
||||||
SpanStyle(textDecoration = TextDecoration.Underline)
|
|
||||||
}
|
|
||||||
|
|
||||||
FormatDataType.URL -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
spanStyle?.let {
|
|
||||||
annotations += AnnotatedString.Range(
|
|
||||||
item = spanStyle,
|
|
||||||
start = item.offset,
|
|
||||||
end = item.offset + item.length
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val newText = AnnotatedString(
|
|
||||||
text = screenState.value.message.text,
|
|
||||||
annotations = annotations
|
|
||||||
)
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(message = old.message.copy(annotatedString = newText))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBoldClicked() {
|
|
||||||
val selectionRange = screenState.value.message.selection
|
|
||||||
val newItems = formatData.items.toMutableList()
|
|
||||||
val wasRemoved = newItems.removeIfCompat {
|
|
||||||
it.type == FormatDataType.BOLD &&
|
|
||||||
it.offset == selectionRange.start &&
|
|
||||||
it.offset + it.length == selectionRange.end
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wasRemoved) {
|
|
||||||
newItems += VkMessage.FormatData.Item(
|
|
||||||
offset = selectionRange.start,
|
|
||||||
length = selectionRange.end - selectionRange.start,
|
|
||||||
type = FormatDataType.BOLD,
|
|
||||||
url = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatData = formatData.copy(items = newItems)
|
|
||||||
updateStyles()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItalicClicked() {
|
|
||||||
val selectionRange = screenState.value.message.selection
|
|
||||||
val newItems = formatData.items.toMutableList()
|
|
||||||
val wasRemoved = newItems.removeIfCompat {
|
|
||||||
it.type == FormatDataType.ITALIC &&
|
|
||||||
it.offset == selectionRange.start &&
|
|
||||||
it.offset + it.length == selectionRange.end
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wasRemoved) {
|
|
||||||
newItems += VkMessage.FormatData.Item(
|
|
||||||
offset = selectionRange.start,
|
|
||||||
length = selectionRange.end - selectionRange.start,
|
|
||||||
type = FormatDataType.ITALIC,
|
|
||||||
url = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatData = formatData.copy(items = newItems)
|
|
||||||
updateStyles()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnderlineClicked() {
|
|
||||||
val selectionRange = screenState.value.message.selection
|
|
||||||
val newItems = formatData.items.toMutableList()
|
|
||||||
val wasRemoved = newItems.removeIfCompat {
|
|
||||||
it.type == FormatDataType.UNDERLINE &&
|
|
||||||
it.offset == selectionRange.start &&
|
|
||||||
it.offset + it.length == selectionRange.end
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wasRemoved) {
|
|
||||||
newItems += VkMessage.FormatData.Item(
|
|
||||||
offset = selectionRange.start,
|
|
||||||
length = selectionRange.end - selectionRange.start,
|
|
||||||
type = FormatDataType.UNDERLINE,
|
|
||||||
url = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatData = formatData.copy(items = newItems)
|
|
||||||
updateStyles()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLinkClicked() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||||
val message = event.message
|
val message = event.message
|
||||||
|
|
||||||
@@ -924,9 +800,8 @@ class MessagesHistoryViewModelImpl(
|
|||||||
pinnedAt = null,
|
pinnedAt = null,
|
||||||
|
|
||||||
// TODO: 04-Apr-25, Danil Nikolaev: implement
|
// TODO: 04-Apr-25, Danil Nikolaev: implement
|
||||||
formatData = formatData,
|
formatData = null,
|
||||||
)
|
)
|
||||||
formatData = formatData.copy(items = emptyList())
|
|
||||||
sendingMessages += newMessage
|
sendingMessages += newMessage
|
||||||
messages.setValue { old -> listOf(newMessage).plus(old) }
|
messages.setValue { old -> listOf(newMessage).plus(old) }
|
||||||
syncUiMessages()
|
syncUiMessages()
|
||||||
@@ -943,8 +818,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
randomId = newMessage.randomId,
|
randomId = newMessage.randomId,
|
||||||
message = newMessage.text,
|
message = newMessage.text,
|
||||||
replyTo = null,
|
replyTo = null,
|
||||||
attachments = null,
|
attachments = null
|
||||||
formatData = newMessage.formatData
|
|
||||||
).listenValue(viewModelScope) { state ->
|
).listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
any = { sendingMessages.remove(newMessage) },
|
any = { sendingMessages.remove(newMessage) },
|
||||||
|
|||||||
+6
-8
@@ -2,16 +2,15 @@ package dev.meloda.fast.messageshistory.model
|
|||||||
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import dev.meloda.fast.common.model.UiImage
|
import dev.meloda.fast.common.model.UiImage
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
|
||||||
|
|
||||||
sealed class UiItem(
|
sealed class UiItem(
|
||||||
open val id: Long,
|
open val id: Long,
|
||||||
open val cmId: Long
|
val cmId: Long
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class Message(
|
data class Message(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val cmId: Long,
|
val conversationMessageId: Long,
|
||||||
val text: AnnotatedString?,
|
val text: AnnotatedString?,
|
||||||
val isOut: Boolean,
|
val isOut: Boolean,
|
||||||
val fromId: Long,
|
val fromId: Long,
|
||||||
@@ -28,14 +27,13 @@ sealed class UiItem(
|
|||||||
val sendingStatus: SendingStatus,
|
val sendingStatus: SendingStatus,
|
||||||
val isSelected: Boolean,
|
val isSelected: Boolean,
|
||||||
val isPinned: Boolean,
|
val isPinned: Boolean,
|
||||||
val isImportant: Boolean,
|
val isImportant: Boolean
|
||||||
val attachments: List<VkAttachment>?
|
) : UiItem(id, conversationMessageId)
|
||||||
) : UiItem(id, cmId)
|
|
||||||
|
|
||||||
data class ActionMessage(
|
data class ActionMessage(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val cmId: Long,
|
val conversationMessageId: Long,
|
||||||
val text: AnnotatedString,
|
val text: AnnotatedString,
|
||||||
val actionCmId: Long?
|
val actionCmId: Long?
|
||||||
) : UiItem(id, cmId)
|
) : UiItem(id, conversationMessageId)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ fun ActionMessageItemPreview() {
|
|||||||
append("You pinned message \"wow hello there\"")
|
append("You pinned message \"wow hello there\"")
|
||||||
},
|
},
|
||||||
actionCmId = null,
|
actionCmId = null,
|
||||||
cmId = 2135
|
conversationMessageId = 2135
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
-103
@@ -1,103 +0,0 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation
|
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Create
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import dev.meloda.fast.messageshistory.model.SendingStatus
|
|
||||||
import dev.meloda.fast.ui.R
|
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BoxScope.DateStatus(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
dateContainerWidth: Dp,
|
|
||||||
date: String,
|
|
||||||
sendingStatus: SendingStatus,
|
|
||||||
isImportant: Boolean,
|
|
||||||
isPinned: Boolean,
|
|
||||||
isEdited: Boolean,
|
|
||||||
isOut: Boolean,
|
|
||||||
isRead: Boolean
|
|
||||||
) {
|
|
||||||
val theme = LocalThemeConfig.current
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = modifier.then(
|
|
||||||
if (theme.enableAnimations) Modifier.animateContentSize()
|
|
||||||
else Modifier
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
if (isImportant) {
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.round_star_24),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(14.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
if (isPinned) {
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_round_push_pin_24),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(14.dp)
|
|
||||||
.rotate(45f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (isEdited) {
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Create,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(14.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = date,
|
|
||||||
style = MaterialTheme.typography.labelSmall
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
|
|
||||||
if (isOut) {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.size(14.dp),
|
|
||||||
painter = painterResource(
|
|
||||||
when (sendingStatus) {
|
|
||||||
SendingStatus.SENDING -> R.drawable.round_access_time_24
|
|
||||||
SendingStatus.SENT -> {
|
|
||||||
if (isRead) R.drawable.round_done_all_24
|
|
||||||
else R.drawable.ic_round_done_24
|
|
||||||
}
|
|
||||||
|
|
||||||
SendingStatus.FAILED -> R.drawable.round_error_outline_24
|
|
||||||
}
|
|
||||||
),
|
|
||||||
tint = if (sendingStatus == SendingStatus.FAILED) MaterialTheme.colorScheme.error
|
|
||||||
else LocalContentColor.current,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-11
@@ -27,16 +27,12 @@ import androidx.compose.ui.unit.dp
|
|||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IncomingMessageBubble(
|
fun IncomingMessageBubble(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
message: UiItem.Message,
|
message: UiItem.Message,
|
||||||
onClick: (VkAttachment) -> Unit = {},
|
|
||||||
onLongClick: (VkAttachment) -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -91,15 +87,12 @@ fun IncomingMessageBubble(
|
|||||||
text = message.text,
|
text = message.text,
|
||||||
isOut = false,
|
isOut = false,
|
||||||
date = message.date,
|
date = message.date,
|
||||||
isEdited = message.isEdited,
|
edited = message.isEdited,
|
||||||
isRead = message.isRead,
|
isRead = message.isRead,
|
||||||
sendingStatus = message.sendingStatus,
|
sendingStatus = message.sendingStatus,
|
||||||
isPinned = message.isPinned,
|
pinned = message.isPinned,
|
||||||
isImportant = message.isImportant,
|
important = message.isImportant,
|
||||||
isSelected = message.isSelected,
|
isSelected = message.isSelected
|
||||||
attachments = message.attachments?.toImmutableList(),
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+150
-161
@@ -4,51 +4,51 @@ import androidx.compose.animation.animateContentSize
|
|||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Create
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.meloda.fast.messageshistory.model.SendingStatus
|
import dev.meloda.fast.messageshistory.model.SendingStatus
|
||||||
import dev.meloda.fast.messageshistory.presentation.attachments.Attachments
|
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.R as UiR
|
||||||
import dev.meloda.fast.ui.util.emptyImmutableList
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageBubble(
|
fun MessageBubble(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: AnnotatedString?,
|
text: AnnotatedString?,
|
||||||
isOut: Boolean,
|
isOut: Boolean,
|
||||||
date: String,
|
date: String?,
|
||||||
isEdited: Boolean,
|
edited: Boolean,
|
||||||
isRead: Boolean,
|
isRead: Boolean,
|
||||||
sendingStatus: SendingStatus,
|
sendingStatus: SendingStatus,
|
||||||
isPinned: Boolean,
|
pinned: Boolean,
|
||||||
isImportant: Boolean,
|
important: Boolean,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean
|
||||||
attachments: ImmutableList<VkAttachment>?,
|
|
||||||
onClick: (VkAttachment) -> Unit = {},
|
|
||||||
onLongClick: (VkAttachment) -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
val theme = LocalThemeConfig.current
|
val theme = LocalThemeConfig.current
|
||||||
val backgroundColor = if (!isOut) {
|
val backgroundColor = if (!isOut) {
|
||||||
@@ -63,162 +63,151 @@ fun MessageBubble(
|
|||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
val minDateContainerWidth by remember(isEdited, isOut, isPinned, isImportant) {
|
|
||||||
derivedStateOf {
|
|
||||||
val mainPart = if (isEdited) 50.dp else 30.dp
|
|
||||||
val readIndicatorPart = if (isOut) 14.dp else 0.dp
|
|
||||||
val pinnedIndicatorPart = if (isPinned) 14.dp else 0.dp
|
|
||||||
val importantIndicatorPart = if (isImportant) 14.dp else 0.dp
|
|
||||||
|
|
||||||
mainPart + readIndicatorPart + pinnedIndicatorPart + importantIndicatorPart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dateContainerWidth by animateDpAsState(
|
|
||||||
targetValue = minDateContainerWidth,
|
|
||||||
label = "dateContainerWidth"
|
|
||||||
)
|
|
||||||
|
|
||||||
val shouldShowBubble by remember(text) {
|
|
||||||
derivedStateOf { text != null }
|
|
||||||
}
|
|
||||||
|
|
||||||
var bubbleContainerWidth by remember {
|
|
||||||
mutableIntStateOf(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var attachmentsContainerWidth by remember {
|
|
||||||
mutableIntStateOf(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val shouldFill by remember(bubbleContainerWidth, attachmentsContainerWidth) {
|
|
||||||
derivedStateOf {
|
|
||||||
attachmentsContainerWidth >= bubbleContainerWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||||
Column {
|
Box(
|
||||||
if (shouldShowBubble) {
|
modifier = modifier
|
||||||
Box(
|
.widthIn(min = 56.dp)
|
||||||
modifier = modifier
|
.clip(RoundedCornerShape(24.dp))
|
||||||
.onGloballyPositioned {
|
.background(backgroundColor)
|
||||||
bubbleContainerWidth = it.size.width
|
.padding(
|
||||||
}
|
horizontal = 8.dp,
|
||||||
.widthIn(min = if (shouldFill) attachmentsContainerWidth.dp else 56.dp)
|
vertical = 6.dp
|
||||||
.clip(
|
)
|
||||||
if (attachments == null) RoundedCornerShape(24.dp)
|
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
|
||||||
else RoundedCornerShape(
|
) {
|
||||||
topStart = 24.dp,
|
val minDateContainerWidth by remember(edited, isOut, pinned, important) {
|
||||||
topEnd = 24.dp,
|
derivedStateOf {
|
||||||
bottomStart = 0.dp,
|
val mainPart = if (edited) 50.dp else 30.dp
|
||||||
bottomEnd = 0.dp
|
val readIndicatorPart = if (isOut) 14.dp else 0.dp
|
||||||
)
|
val pinnedIndicatorPart = if (pinned) 14.dp else 0.dp
|
||||||
)
|
val importantIndicatorPart = if (important) 14.dp else 0.dp
|
||||||
.background(backgroundColor)
|
|
||||||
.padding(
|
|
||||||
horizontal = 8.dp,
|
|
||||||
vertical = 6.dp
|
|
||||||
)
|
|
||||||
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
|
|
||||||
) {
|
|
||||||
MessageTextContainer(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(2.dp)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
.padding(end = dateContainerWidth)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
|
|
||||||
text = text,
|
|
||||||
isOut = isOut,
|
|
||||||
isSelected = isSelected,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (attachments == null) {
|
mainPart + readIndicatorPart + pinnedIndicatorPart + importantIndicatorPart
|
||||||
DateStatus(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 3.dp)
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.defaultMinSize(minWidth = dateContainerWidth),
|
|
||||||
dateContainerWidth = dateContainerWidth,
|
|
||||||
date = date,
|
|
||||||
sendingStatus = sendingStatus,
|
|
||||||
isImportant = isImportant,
|
|
||||||
isPinned = isPinned,
|
|
||||||
isEdited = isEdited,
|
|
||||||
isOut = isOut,
|
|
||||||
isRead = isRead
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachments != null) {
|
val dateContainerWidth by animateDpAsState(
|
||||||
Box(
|
targetValue = minDateContainerWidth,
|
||||||
modifier = Modifier
|
label = "dateContainerWidth"
|
||||||
.onGloballyPositioned {
|
)
|
||||||
attachmentsContainerWidth = it.size.width
|
|
||||||
}
|
|
||||||
.clip(
|
|
||||||
if (!shouldShowBubble) RoundedCornerShape(24.dp)
|
|
||||||
else RoundedCornerShape(
|
|
||||||
bottomEnd = 24.dp,
|
|
||||||
bottomStart = 24.dp,
|
|
||||||
topStart = 0.dp,
|
|
||||||
topEnd = 0.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.background(backgroundColor)
|
|
||||||
) {
|
|
||||||
Attachments(
|
|
||||||
modifier = Modifier,
|
|
||||||
attachments = attachments,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick
|
|
||||||
)
|
|
||||||
|
|
||||||
val dateStatusBackground = if (theme.darkMode) Color.Black.copy(alpha = 0.5f)
|
if (text != null) {
|
||||||
else Color.White.copy(alpha = 0.5f)
|
val textLambda: @Composable () -> Unit = remember(text, theme, dateContainerWidth) {
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = kotlin.run {
|
||||||
|
val builder = AnnotatedString.Builder(text)
|
||||||
|
|
||||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
text.spanStyles.map { spanStyleRange ->
|
||||||
DateStatus(
|
val updatedSpanStyle =
|
||||||
|
if (spanStyleRange.item.color == Color.Red) {
|
||||||
|
spanStyleRange.item.copy(color =
|
||||||
|
if (isOut) {
|
||||||
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
spanStyleRange.item
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.addStyle(
|
||||||
|
style = updatedSpanStyle,
|
||||||
|
start = spanStyleRange.start,
|
||||||
|
end = spanStyleRange.end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
text.paragraphStyles.forEach { style ->
|
||||||
|
builder.addStyle(
|
||||||
|
style = style.item,
|
||||||
|
start = style.start,
|
||||||
|
end = style.end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.toAnnotatedString()
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.padding(2.dp)
|
||||||
.padding(bottom = 6.dp, end = 6.dp)
|
.align(Alignment.Center)
|
||||||
.widthIn(min = 42.dp)
|
.padding(end = 4.dp)
|
||||||
.clip(RoundedCornerShape(24.dp))
|
.padding(end = dateContainerWidth)
|
||||||
.background(dateStatusBackground)
|
.padding(end = 4.dp)
|
||||||
.padding(4.dp),
|
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier)
|
||||||
dateContainerWidth = dateContainerWidth,
|
|
||||||
date = date,
|
|
||||||
sendingStatus = sendingStatus,
|
|
||||||
isImportant = isImportant,
|
|
||||||
isPinned = isPinned,
|
|
||||||
isEdited = isEdited,
|
|
||||||
isOut = isOut,
|
|
||||||
isRead = isRead
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
SelectionContainer {
|
||||||
|
textLambda.invoke()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
textLambda.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.defaultMinSize(minWidth = dateContainerWidth)
|
||||||
|
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
|
||||||
|
) {
|
||||||
|
if (important) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(UiR.drawable.round_star_24),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(14.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
if (pinned) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(UiR.drawable.ic_round_push_pin_24),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(14.dp)
|
||||||
|
.rotate(45f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
if (edited) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Create,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(14.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = date.orEmpty(),
|
||||||
|
style = MaterialTheme.typography.labelSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
|
if (isOut) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
painter = painterResource(
|
||||||
|
when (sendingStatus) {
|
||||||
|
SendingStatus.SENDING -> UiR.drawable.round_access_time_24
|
||||||
|
SendingStatus.SENT -> {
|
||||||
|
if (isRead) UiR.drawable.round_done_all_24
|
||||||
|
else UiR.drawable.ic_round_done_24
|
||||||
|
}
|
||||||
|
|
||||||
|
SendingStatus.FAILED -> UiR.drawable.round_error_outline_24
|
||||||
|
}
|
||||||
|
),
|
||||||
|
tint = if (sendingStatus == SendingStatus.FAILED) MaterialTheme.colorScheme.error
|
||||||
|
else LocalContentColor.current,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun Bubble() {
|
|
||||||
MessageBubble(
|
|
||||||
modifier = Modifier,
|
|
||||||
text = AnnotatedString("Some cool text"),
|
|
||||||
isOut = true,
|
|
||||||
date = "19:01",
|
|
||||||
isEdited = true,
|
|
||||||
isRead = true,
|
|
||||||
sendingStatus = SendingStatus.SENT,
|
|
||||||
isPinned = true,
|
|
||||||
isImportant = true,
|
|
||||||
isSelected = false,
|
|
||||||
attachments = emptyImmutableList()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
-95
@@ -1,95 +0,0 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation
|
|
||||||
|
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageTextContainer(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
text: AnnotatedString?,
|
|
||||||
isOut: Boolean,
|
|
||||||
isSelected: Boolean,
|
|
||||||
) {
|
|
||||||
if (text != null) {
|
|
||||||
if (isSelected) {
|
|
||||||
SelectionContainer {
|
|
||||||
MessageText(
|
|
||||||
modifier = modifier,
|
|
||||||
text = text,
|
|
||||||
isOut = isOut,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MessageText(
|
|
||||||
modifier = modifier,
|
|
||||||
text = text,
|
|
||||||
isOut = isOut,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageText(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
text: AnnotatedString,
|
|
||||||
isOut: Boolean,
|
|
||||||
) {
|
|
||||||
val replacedColor = if (isOut) {
|
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
val newText = remember(text) {
|
|
||||||
val builder = AnnotatedString.Builder(text)
|
|
||||||
|
|
||||||
text.spanStyles.map { spanStyleRange ->
|
|
||||||
val updatedSpanStyle =
|
|
||||||
if (spanStyleRange.item.color == Color.Red) {
|
|
||||||
spanStyleRange.item.copy(color = replacedColor)
|
|
||||||
} else {
|
|
||||||
spanStyleRange.item
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.addStyle(
|
|
||||||
style = updatedSpanStyle,
|
|
||||||
start = spanStyleRange.start,
|
|
||||||
end = spanStyleRange.end
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
text.paragraphStyles.forEach { style ->
|
|
||||||
builder.addStyle(
|
|
||||||
style = style.item,
|
|
||||||
start = style.start,
|
|
||||||
end = style.end
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.toAnnotatedString()
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = newText,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun MessageTextPreview() {
|
|
||||||
MessageTextContainer(
|
|
||||||
modifier = Modifier,
|
|
||||||
text = AnnotatedString("Some cool text"),
|
|
||||||
isOut = true,
|
|
||||||
isSelected = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+1
-4
@@ -70,10 +70,7 @@ fun MessagesHistoryRoute(
|
|||||||
onMessageLongClicked = viewModel::onMessageLongClicked,
|
onMessageLongClicked = viewModel::onMessageLongClicked,
|
||||||
onPinnedMessageClicked = viewModel::onPinnedMessageClicked,
|
onPinnedMessageClicked = viewModel::onPinnedMessageClicked,
|
||||||
onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked,
|
onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked,
|
||||||
onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked,
|
onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked
|
||||||
onBoldRequested = viewModel::onBoldClicked,
|
|
||||||
onItalicRequested = viewModel::onItalicClicked,
|
|
||||||
onUnderlineRequested = viewModel::onUnderlineClicked
|
|
||||||
)
|
)
|
||||||
|
|
||||||
HandleDialogs(
|
HandleDialogs(
|
||||||
|
|||||||
+28
-297
@@ -1,13 +1,7 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation
|
package dev.meloda.fast.messageshistory.presentation
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
@@ -62,7 +56,6 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -74,15 +67,11 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.geometry.Rect
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalTextToolbar
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.platform.TextToolbar
|
|
||||||
import androidx.compose.ui.platform.TextToolbarStatus
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
@@ -144,10 +133,7 @@ fun MessagesHistoryScreen(
|
|||||||
onMessageLongClicked: (Long) -> Unit = {},
|
onMessageLongClicked: (Long) -> Unit = {},
|
||||||
onPinnedMessageClicked: (Long) -> Unit = {},
|
onPinnedMessageClicked: (Long) -> Unit = {},
|
||||||
onUnpinMessageButtonClicked: () -> Unit = {},
|
onUnpinMessageButtonClicked: () -> Unit = {},
|
||||||
onDeleteSelectedButtonClicked: () -> Unit = {},
|
onDeleteSelectedButtonClicked: () -> Unit = {}
|
||||||
onBoldRequested: () -> Unit = {},
|
|
||||||
onItalicRequested: () -> Unit = {},
|
|
||||||
onUnderlineRequested: () -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@@ -303,7 +289,7 @@ fun MessagesHistoryScreen(
|
|||||||
Text(
|
Text(
|
||||||
text = when {
|
text = when {
|
||||||
screenState.isLoading -> stringResource(id = UiR.string.title_loading)
|
screenState.isLoading -> stringResource(id = UiR.string.title_loading)
|
||||||
selectedMessages.isNotEmpty() -> "(${selectedMessages.size})"
|
selectedMessages.size > 0 -> "(${selectedMessages.size})"
|
||||||
else -> screenState.title
|
else -> screenState.title
|
||||||
},
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -319,17 +305,14 @@ fun MessagesHistoryScreen(
|
|||||||
else onClose()
|
else onClose()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Crossfade(targetState = selectedMessages.isEmpty()) { state ->
|
Icon(
|
||||||
Icon(
|
imageVector = if (selectedMessages.isEmpty()) {
|
||||||
imageVector = if (state) {
|
Icons.AutoMirrored.Rounded.ArrowBack
|
||||||
Icons.AutoMirrored.Rounded.ArrowBack
|
} else {
|
||||||
} else {
|
Icons.Rounded.Close
|
||||||
Icons.Rounded.Close
|
},
|
||||||
},
|
contentDescription = "Back button"
|
||||||
contentDescription = if (state) "Close button"
|
)
|
||||||
else "Back button"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
||||||
@@ -574,37 +557,24 @@ fun MessagesHistoryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val view = LocalView.current
|
TextField(
|
||||||
val textToolbar = remember {
|
modifier = Modifier.weight(1f),
|
||||||
CustomTextToolbar(
|
value = screenState.message,
|
||||||
view = view,
|
onValueChange = onMessageInputChanged,
|
||||||
onBoldRequested = onBoldRequested,
|
colors = TextFieldDefaults.colors(
|
||||||
onItalicRequested = onItalicRequested,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
onUnderlineRequested = onUnderlineRequested,
|
focusedContainerColor = Color.Transparent,
|
||||||
onLinkRequested = {}
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
)
|
focusedIndicatorColor = Color.Transparent,
|
||||||
}
|
),
|
||||||
|
placeholder = {
|
||||||
CompositionLocalProvider(LocalTextToolbar provides textToolbar) {
|
Text(
|
||||||
TextField(
|
text = stringResource(id = UiR.string.message_input_hint),
|
||||||
modifier = Modifier.weight(1f),
|
maxLines = 1,
|
||||||
value = screenState.message,
|
overflow = TextOverflow.Ellipsis
|
||||||
onValueChange = onMessageInputChanged,
|
)
|
||||||
colors = TextFieldDefaults.colors(
|
}
|
||||||
unfocusedContainerColor = Color.Transparent,
|
)
|
||||||
focusedContainerColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
),
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = UiR.string.message_input_hint),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val attachmentRotation = remember { Animatable(0f) }
|
val attachmentRotation = remember { Animatable(0f) }
|
||||||
@@ -717,242 +687,3 @@ fun MessagesHistoryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomTextToolbar(
|
|
||||||
private val view: View,
|
|
||||||
private var onBoldRequested: (() -> Unit)? = null,
|
|
||||||
private var onItalicRequested: (() -> Unit)? = null,
|
|
||||||
private var onUnderlineRequested: (() -> Unit)? = null,
|
|
||||||
private var onLinkRequested: (() -> Unit)? = null
|
|
||||||
) : TextToolbar {
|
|
||||||
private var actionMode: android.view.ActionMode? = null
|
|
||||||
private val textActionModeCallback: TextActionModeCallback =
|
|
||||||
TextActionModeCallback(onActionModeDestroy = { actionMode = null })
|
|
||||||
override var status: TextToolbarStatus = TextToolbarStatus.Hidden
|
|
||||||
private set
|
|
||||||
|
|
||||||
override fun showMenu(
|
|
||||||
rect: Rect,
|
|
||||||
onCopyRequested: (() -> Unit)?,
|
|
||||||
onPasteRequested: (() -> Unit)?,
|
|
||||||
onCutRequested: (() -> Unit)?,
|
|
||||||
onSelectAllRequested: (() -> Unit)?,
|
|
||||||
onAutofillRequested: (() -> Unit)?
|
|
||||||
) {
|
|
||||||
textActionModeCallback.rect = rect
|
|
||||||
textActionModeCallback.onCopyRequested = onCopyRequested
|
|
||||||
textActionModeCallback.onCutRequested = onCutRequested
|
|
||||||
textActionModeCallback.onPasteRequested = onPasteRequested
|
|
||||||
textActionModeCallback.onSelectAllRequested = onSelectAllRequested
|
|
||||||
textActionModeCallback.onAutofillRequested = onAutofillRequested
|
|
||||||
textActionModeCallback.onBoldRequested = onBoldRequested
|
|
||||||
textActionModeCallback.onItalicRequested = onItalicRequested
|
|
||||||
textActionModeCallback.onUnderlineRequested = onUnderlineRequested
|
|
||||||
textActionModeCallback.onLinkRequested = onLinkRequested
|
|
||||||
|
|
||||||
if (actionMode == null) {
|
|
||||||
status = TextToolbarStatus.Shown
|
|
||||||
actionMode =
|
|
||||||
TextToolbarHelperMethods.startActionMode(
|
|
||||||
view,
|
|
||||||
FloatingTextActionModeCallback(textActionModeCallback),
|
|
||||||
android.view.ActionMode.TYPE_FLOATING
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
actionMode?.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showMenu(
|
|
||||||
rect: Rect,
|
|
||||||
onCopyRequested: (() -> Unit)?,
|
|
||||||
onPasteRequested: (() -> Unit)?,
|
|
||||||
onCutRequested: (() -> Unit)?,
|
|
||||||
onSelectAllRequested: (() -> Unit)?
|
|
||||||
) {
|
|
||||||
showMenu(
|
|
||||||
rect = rect,
|
|
||||||
onCopyRequested = onCopyRequested,
|
|
||||||
onPasteRequested = onPasteRequested,
|
|
||||||
onCutRequested = onCutRequested,
|
|
||||||
onSelectAllRequested = onSelectAllRequested,
|
|
||||||
onAutofillRequested = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hide() {
|
|
||||||
status = TextToolbarStatus.Hidden
|
|
||||||
actionMode?.finish()
|
|
||||||
actionMode = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is here to ensure that the classes that use this API will get verified and can be AOT
|
|
||||||
* compiled. It is expected that this class will soft-fail verification, but the classes which use
|
|
||||||
* this method will pass.
|
|
||||||
*/
|
|
||||||
internal object TextToolbarHelperMethods {
|
|
||||||
fun startActionMode(
|
|
||||||
view: View,
|
|
||||||
actionModeCallback: android.view.ActionMode.Callback,
|
|
||||||
type: Int
|
|
||||||
): android.view.ActionMode? {
|
|
||||||
return view.startActionMode(actionModeCallback, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateContentRect(actionMode: android.view.ActionMode) {
|
|
||||||
actionMode.invalidateContentRect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class FloatingTextActionModeCallback(private val callback: TextActionModeCallback) :
|
|
||||||
android.view.ActionMode.Callback2() {
|
|
||||||
override fun onActionItemClicked(mode: android.view.ActionMode?, item: MenuItem?): Boolean {
|
|
||||||
return callback.onActionItemClicked(mode, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: android.view.ActionMode?, menu: Menu?): Boolean {
|
|
||||||
return callback.onCreateActionMode(mode, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: android.view.ActionMode?, menu: Menu?): Boolean {
|
|
||||||
return callback.onPrepareActionMode(mode, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyActionMode(mode: android.view.ActionMode?) {
|
|
||||||
callback.onDestroyActionMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGetContentRect(
|
|
||||||
mode: android.view.ActionMode?,
|
|
||||||
view: View?,
|
|
||||||
outRect: android.graphics.Rect?
|
|
||||||
) {
|
|
||||||
val rect = callback.rect
|
|
||||||
outRect?.set(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TextActionModeCallback(
|
|
||||||
val onActionModeDestroy: (() -> Unit)? = null,
|
|
||||||
var rect: Rect = Rect.Zero,
|
|
||||||
var onCopyRequested: (() -> Unit)? = null,
|
|
||||||
var onPasteRequested: (() -> Unit)? = null,
|
|
||||||
var onCutRequested: (() -> Unit)? = null,
|
|
||||||
var onSelectAllRequested: (() -> Unit)? = null,
|
|
||||||
var onAutofillRequested: (() -> Unit)? = null,
|
|
||||||
var onBoldRequested: (() -> Unit)? = null,
|
|
||||||
var onItalicRequested: (() -> Unit)? = null,
|
|
||||||
var onUnderlineRequested: (() -> Unit)? = null,
|
|
||||||
var onLinkRequested: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
fun onCreateActionMode(mode: android.view.ActionMode?, menu: Menu?): Boolean {
|
|
||||||
requireNotNull(menu) { "onCreateActionMode requires a non-null menu" }
|
|
||||||
requireNotNull(mode) { "onCreateActionMode requires a non-null mode" }
|
|
||||||
|
|
||||||
onCopyRequested?.let { addMenuItem(menu, MenuItemOption.Copy) }
|
|
||||||
onPasteRequested?.let { addMenuItem(menu, MenuItemOption.Paste) }
|
|
||||||
onCutRequested?.let { addMenuItem(menu, MenuItemOption.Cut) }
|
|
||||||
onSelectAllRequested?.let { addMenuItem(menu, MenuItemOption.SelectAll) }
|
|
||||||
if (onAutofillRequested != null && Build.VERSION.SDK_INT >= 26) {
|
|
||||||
addMenuItem(menu, MenuItemOption.Autofill)
|
|
||||||
}
|
|
||||||
onBoldRequested?.let { addMenuItem(menu, MenuItemOption.Bold) }
|
|
||||||
onItalicRequested?.let { addMenuItem(menu, MenuItemOption.Italic) }
|
|
||||||
onUnderlineRequested?.let { addMenuItem(menu, MenuItemOption.Underline) }
|
|
||||||
onLinkRequested?.let { addMenuItem(menu, MenuItemOption.Link) }
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// this method is called to populate new menu items when the actionMode was invalidated
|
|
||||||
fun onPrepareActionMode(mode: android.view.ActionMode?, menu: Menu?): Boolean {
|
|
||||||
if (mode == null || menu == null) return false
|
|
||||||
updateMenuItems(menu)
|
|
||||||
// should return true so that new menu items are populated
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onActionItemClicked(mode: android.view.ActionMode?, item: MenuItem?): Boolean {
|
|
||||||
when (item!!.itemId) {
|
|
||||||
MenuItemOption.Copy.ordinal -> onCopyRequested?.invoke()
|
|
||||||
MenuItemOption.Paste.ordinal -> onPasteRequested?.invoke()
|
|
||||||
MenuItemOption.Cut.ordinal -> onCutRequested?.invoke()
|
|
||||||
MenuItemOption.SelectAll.ordinal -> onSelectAllRequested?.invoke()
|
|
||||||
MenuItemOption.Autofill.ordinal -> onAutofillRequested?.invoke()
|
|
||||||
MenuItemOption.Bold.ordinal -> onBoldRequested?.invoke()
|
|
||||||
MenuItemOption.Italic.ordinal -> onItalicRequested?.invoke()
|
|
||||||
MenuItemOption.Underline.ordinal -> onUnderlineRequested?.invoke()
|
|
||||||
MenuItemOption.Link.ordinal -> onLinkRequested?.invoke()
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
mode?.finish()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDestroyActionMode() {
|
|
||||||
onActionModeDestroy?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun updateMenuItems(menu: Menu) {
|
|
||||||
addOrRemoveMenuItem(menu, MenuItemOption.Copy, onCopyRequested)
|
|
||||||
addOrRemoveMenuItem(menu, MenuItemOption.Paste, onPasteRequested)
|
|
||||||
addOrRemoveMenuItem(menu, MenuItemOption.Cut, onCutRequested)
|
|
||||||
addOrRemoveMenuItem(menu, MenuItemOption.SelectAll, onSelectAllRequested)
|
|
||||||
addOrRemoveMenuItem(menu, MenuItemOption.Autofill, onAutofillRequested)
|
|
||||||
addOrRemoveMenuItem(menu, MenuItemOption.Bold, onBoldRequested)
|
|
||||||
addOrRemoveMenuItem(menu, MenuItemOption.Italic, onItalicRequested)
|
|
||||||
addOrRemoveMenuItem(menu, MenuItemOption.Underline, onUnderlineRequested)
|
|
||||||
addOrRemoveMenuItem(menu, MenuItemOption.Link, onLinkRequested)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addMenuItem(menu: Menu, item: MenuItemOption) {
|
|
||||||
menu
|
|
||||||
.add(0, item.ordinal, item.order, item.titleResource)
|
|
||||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addOrRemoveMenuItem(menu: Menu, item: MenuItemOption, callback: (() -> Unit)?) {
|
|
||||||
when {
|
|
||||||
callback != null && menu.findItem(item.ordinal) == null -> addMenuItem(menu, item)
|
|
||||||
callback == null && menu.findItem(item.ordinal) != null -> menu.removeItem(item.ordinal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum class MenuItemOption {
|
|
||||||
Copy,
|
|
||||||
Paste,
|
|
||||||
Cut,
|
|
||||||
SelectAll,
|
|
||||||
Autofill,
|
|
||||||
Bold,
|
|
||||||
Italic,
|
|
||||||
Underline,
|
|
||||||
Link;
|
|
||||||
|
|
||||||
val titleResource: Int
|
|
||||||
get() =
|
|
||||||
when (this) {
|
|
||||||
Copy -> android.R.string.copy
|
|
||||||
Paste -> android.R.string.paste
|
|
||||||
Cut -> android.R.string.cut
|
|
||||||
SelectAll -> android.R.string.selectAll
|
|
||||||
Autofill ->
|
|
||||||
if (Build.VERSION.SDK_INT <= 26) {
|
|
||||||
UiR.string.autofill
|
|
||||||
} else {
|
|
||||||
android.R.string.autofill
|
|
||||||
}
|
|
||||||
|
|
||||||
Bold -> UiR.string.bold
|
|
||||||
Italic -> UiR.string.italic
|
|
||||||
Underline -> UiR.string.underline
|
|
||||||
Link -> UiR.string.link
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This item will be shown before all items that have order greater than this value. */
|
|
||||||
val order = ordinal
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-78
@@ -1,8 +1,5 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation
|
package dev.meloda.fast.messageshistory.presentation
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
@@ -23,13 +20,11 @@ import androidx.compose.material3.CircularProgressIndicator
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -37,13 +32,8 @@ import dev.chrisbanes.haze.HazeState
|
|||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
|
||||||
import dev.meloda.fast.model.api.domain.VkLinkDomain
|
|
||||||
import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import androidx.core.net.toUri
|
|
||||||
import dev.meloda.fast.model.api.domain.VkFileDomain
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -59,63 +49,9 @@ fun MessagesList(
|
|||||||
onMessageClicked: (Long) -> Unit = {},
|
onMessageClicked: (Long) -> Unit = {},
|
||||||
onMessageLongClicked: (Long) -> Unit = {}
|
onMessageLongClicked: (Long) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val theme = LocalThemeConfig.current
|
val theme = LocalThemeConfig.current
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
|
||||||
val isSelectedAtLeastOne by remember(uiMessages) {
|
|
||||||
derivedStateOf {
|
|
||||||
uiMessages.values.any { (it as? UiItem.Message)?.isSelected == true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onAttachmentClick = remember {
|
|
||||||
{ message: UiItem.Message, attachment: VkAttachment ->
|
|
||||||
if (isSelectedAtLeastOne) {
|
|
||||||
onMessageClicked(message.id)
|
|
||||||
} else {
|
|
||||||
when (attachment) {
|
|
||||||
is VkPhotoDomain -> {
|
|
||||||
val maxSize = attachment.getMaxSize()
|
|
||||||
maxSize?.let {
|
|
||||||
context.startActivity(
|
|
||||||
Intent(Intent.ACTION_VIEW, maxSize.url.toUri())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is VkFileDomain -> {
|
|
||||||
context.startActivity(
|
|
||||||
Intent(Intent.ACTION_VIEW, attachment.url.toUri())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is VkLinkDomain -> {
|
|
||||||
context.startActivity(
|
|
||||||
Intent(Intent.ACTION_VIEW, attachment.url.toUri())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onAttachmentLongClick = remember {
|
|
||||||
{ message: UiItem.Message, attachment: VkAttachment ->
|
|
||||||
if (isSelectedAtLeastOne) {
|
|
||||||
onMessageLongClicked(message.id)
|
|
||||||
uiMessages
|
|
||||||
} else {
|
|
||||||
when (attachment) {
|
|
||||||
is VkPhotoDomain -> {
|
|
||||||
val maxSize = attachment.getMaxSize()
|
|
||||||
Log.d("MessagesList", "onPhotoLongClicked. Max size: ${maxSize?.url}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -205,13 +141,7 @@ fun MessagesList(
|
|||||||
)
|
)
|
||||||
else Modifier
|
else Modifier
|
||||||
),
|
),
|
||||||
message = item,
|
message = item
|
||||||
onClick = { attachment ->
|
|
||||||
onAttachmentClick(item, attachment)
|
|
||||||
},
|
|
||||||
onLongClick = { attachment ->
|
|
||||||
onAttachmentLongClick(item, attachment)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
IncomingMessageBubble(
|
IncomingMessageBubble(
|
||||||
@@ -225,13 +155,7 @@ fun MessagesList(
|
|||||||
)
|
)
|
||||||
else Modifier
|
else Modifier
|
||||||
),
|
),
|
||||||
message = item,
|
message = item
|
||||||
onClick = { attachment ->
|
|
||||||
onAttachmentClick(item, attachment)
|
|
||||||
},
|
|
||||||
onLongClick = { attachment ->
|
|
||||||
onAttachmentLongClick(item, attachment)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-11
@@ -10,17 +10,14 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.meloda.fast.common.extensions.orDots
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OutgoingMessageBubble(
|
fun OutgoingMessageBubble(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
message: UiItem.Message,
|
message: UiItem.Message,
|
||||||
onClick: (VkAttachment) -> Unit = {},
|
|
||||||
onLongClick: (VkAttachment) -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -44,15 +41,12 @@ fun OutgoingMessageBubble(
|
|||||||
text = message.text,
|
text = message.text,
|
||||||
isOut = true,
|
isOut = true,
|
||||||
date = message.date,
|
date = message.date,
|
||||||
isEdited = message.isEdited,
|
edited = message.isEdited,
|
||||||
isRead = message.isRead,
|
isRead = message.isRead,
|
||||||
sendingStatus = message.sendingStatus,
|
sendingStatus = message.sendingStatus,
|
||||||
isPinned = message.isPinned,
|
pinned = message.isPinned,
|
||||||
isImportant = message.isImportant,
|
important = message.isImportant,
|
||||||
isSelected = message.isSelected,
|
isSelected = message.isSelected
|
||||||
attachments = message.attachments?.toImmutableList(),
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-9
@@ -21,7 +21,6 @@ import androidx.compose.ui.draw.rotate
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
@@ -63,17 +62,11 @@ fun PinnedMessageContainer(
|
|||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
summary?.let { summary ->
|
summary?.let { summary ->
|
||||||
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
||||||
Text(
|
Text(text = summary)
|
||||||
text = summary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-165
@@ -1,165 +0,0 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation.attachments
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import dev.meloda.fast.model.api.data.AttachmentType
|
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
|
||||||
import dev.meloda.fast.model.api.domain.VkAudioDomain
|
|
||||||
import dev.meloda.fast.model.api.domain.VkFileDomain
|
|
||||||
import dev.meloda.fast.model.api.domain.VkLinkDomain
|
|
||||||
import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
|
||||||
import dev.meloda.fast.model.api.domain.VkVideoDomain
|
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
|
||||||
|
|
||||||
private val previewTypes = listOf(
|
|
||||||
AttachmentType.PHOTO,
|
|
||||||
AttachmentType.VIDEO
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Attachments(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
attachments: ImmutableList<out VkAttachment>,
|
|
||||||
onClick: (VkAttachment) -> Unit = {},
|
|
||||||
onLongClick: (VkAttachment) -> Unit = {}
|
|
||||||
) {
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
if (attachments.isEmpty()) return
|
|
||||||
|
|
||||||
val previewAttachments by remember(attachments) {
|
|
||||||
derivedStateOf {
|
|
||||||
attachments.values.filter { it.type in previewTypes }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val nonPreviewAttachments by remember(attachments) {
|
|
||||||
derivedStateOf {
|
|
||||||
attachments.values.filterNot { it.type in previewTypes }
|
|
||||||
.sortedBy { it.type.ordinal }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previewAttachments.isNotEmpty()) {
|
|
||||||
Previews(
|
|
||||||
modifier = Modifier,
|
|
||||||
photos = previewAttachments
|
|
||||||
.map(VkAttachment::asUiPhoto)
|
|
||||||
.toImmutableList(),
|
|
||||||
onClick = { index ->
|
|
||||||
onClick(previewAttachments[index])
|
|
||||||
},
|
|
||||||
onLongClick = { index ->
|
|
||||||
onLongClick(previewAttachments[index])
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
nonPreviewAttachments.forEach { attachment ->
|
|
||||||
when (attachment.type) {
|
|
||||||
AttachmentType.AUDIO -> {
|
|
||||||
Audio(
|
|
||||||
item = attachment as VkAudioDomain,
|
|
||||||
modifier = Modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AttachmentType.FILE -> {
|
|
||||||
File(
|
|
||||||
item = attachment as VkFileDomain,
|
|
||||||
modifier = Modifier,
|
|
||||||
onClick = { onClick(attachment) },
|
|
||||||
onLongClick = { onLongClick(attachment) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AttachmentType.LINK -> {
|
|
||||||
Link(
|
|
||||||
item = attachment as VkLinkDomain,
|
|
||||||
modifier = Modifier,
|
|
||||||
onClick = { onClick(attachment) },
|
|
||||||
onLongClick = { onLongClick(attachment) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun VkAttachment.asUiPhoto(): UiPreview {
|
|
||||||
return when (this) {
|
|
||||||
is VkPhotoDomain -> {
|
|
||||||
val size = this.getDefault()!!
|
|
||||||
UiPreview(
|
|
||||||
id = this.id,
|
|
||||||
url = size.url,
|
|
||||||
width = size.width,
|
|
||||||
height = size.height,
|
|
||||||
isVideo = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is VkVideoDomain -> {
|
|
||||||
val size = this.getDefault() ?: VkVideoDomain.VideoImage(
|
|
||||||
width = 1280,
|
|
||||||
height = 720,
|
|
||||||
url = "",
|
|
||||||
withPadding = false
|
|
||||||
)
|
|
||||||
|
|
||||||
UiPreview(
|
|
||||||
id = this.id,
|
|
||||||
url = size.url,
|
|
||||||
width = size.width,
|
|
||||||
height = size.height,
|
|
||||||
isVideo = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is VkFileDomain -> {
|
|
||||||
when {
|
|
||||||
this.preview?.video != null -> {
|
|
||||||
val video = this.preview?.video!!
|
|
||||||
|
|
||||||
UiPreview(
|
|
||||||
id = id,
|
|
||||||
url = video.src,
|
|
||||||
width = video.width,
|
|
||||||
height = video.height,
|
|
||||||
isVideo = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.preview?.photo != null -> {
|
|
||||||
val photoSize = this.preview?.photo?.sizes?.first()!!
|
|
||||||
|
|
||||||
UiPreview(
|
|
||||||
id = id,
|
|
||||||
url = photoSize.src,
|
|
||||||
width = photoSize.width,
|
|
||||||
height = photoSize.height,
|
|
||||||
isVideo = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> error("Unsupported type: $this")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> error("Unsupported type: $this")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class UiPreview(
|
|
||||||
val id: Long,
|
|
||||||
val url: String,
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
val isVideo: Boolean
|
|
||||||
)
|
|
||||||
-105
@@ -1,105 +0,0 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation.attachments
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.contentColorFor
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import dev.meloda.fast.model.api.domain.VkAudioDomain
|
|
||||||
import dev.meloda.fast.ui.R
|
|
||||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
|
||||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Audio(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
item: VkAudioDomain
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(min = 56.dp)
|
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.primary)
|
|
||||||
.size(36.dp)
|
|
||||||
.padding(4.dp),
|
|
||||||
painter = painterResource(R.drawable.round_play_arrow_24),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = contentColorFor(MaterialTheme.colorScheme.primary)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = item.title,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
|
||||||
Text(
|
|
||||||
text = item.artist,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 11-Apr-25, Danil Nikolaev: extract to ui model
|
|
||||||
val formattedDuration by remember(item) {
|
|
||||||
derivedStateOf {
|
|
||||||
val duration = item.duration
|
|
||||||
|
|
||||||
val days = duration / (24 * 3600)
|
|
||||||
val hours = (duration % (24 * 3600)) / 3600
|
|
||||||
val minutes = (duration % 3600) / 60
|
|
||||||
val seconds = duration % 60
|
|
||||||
|
|
||||||
val args = mutableListOf<Int>()
|
|
||||||
if (days > 0) args.add(days)
|
|
||||||
if (hours > 0) args.add(hours)
|
|
||||||
args.add(minutes)
|
|
||||||
args.add(seconds)
|
|
||||||
|
|
||||||
val builder = StringBuilder()
|
|
||||||
if (days > 0) builder.append("%02d:")
|
|
||||||
if (hours > 0) builder.append("%02d:")
|
|
||||||
builder.append("%d:%02d")
|
|
||||||
|
|
||||||
builder.toString().format(Locale.getDefault(), *args.toTypedArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = formattedDuration,
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-142
@@ -1,142 +0,0 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation.attachments
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import coil.compose.AsyncImagePainter
|
|
||||||
import coil.compose.rememberAsyncImagePainter
|
|
||||||
import coil.imageLoader
|
|
||||||
import dev.meloda.fast.common.util.AndroidUtils
|
|
||||||
import dev.meloda.fast.model.api.domain.VkFileDomain
|
|
||||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
|
||||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun File(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
item: VkFileDomain,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
onLongClick: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(min = 48.dp)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
var errorLoading by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 11-Apr-25, Danil Nikolaev: extract to ui model
|
|
||||||
val preview by remember(item) {
|
|
||||||
derivedStateOf {
|
|
||||||
when (val preview = item.preview) {
|
|
||||||
null -> null
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
when {
|
|
||||||
preview.photo != null -> {
|
|
||||||
val size = preview.photo?.sizes?.maxByOrNull { it.width }
|
|
||||||
size?.src
|
|
||||||
}
|
|
||||||
|
|
||||||
preview.video != null -> {
|
|
||||||
val size = preview.video?.src
|
|
||||||
size
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val formattedSize by remember(item) {
|
|
||||||
derivedStateOf {
|
|
||||||
AndroidUtils.bytesToHumanReadableSize(item.size.toDouble())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preview != null && !errorLoading) {
|
|
||||||
Image(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
.size(width = 48.dp, height = 36.dp),
|
|
||||||
painter = rememberAsyncImagePainter(
|
|
||||||
model = preview,
|
|
||||||
imageLoader = LocalContext.current.imageLoader,
|
|
||||||
onState = {
|
|
||||||
errorLoading = it is AsyncImagePainter.State.Error
|
|
||||||
}
|
|
||||||
),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(24.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp))
|
|
||||||
.size(width = 48.dp, height = 36.dp),
|
|
||||||
text = item.ext.uppercase(),
|
|
||||||
lineHeight = 36.sp,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = item.title,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
|
||||||
Text(
|
|
||||||
text = formattedSize,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-136
@@ -1,136 +0,0 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation.attachments
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import coil.compose.AsyncImagePainter
|
|
||||||
import coil.compose.rememberAsyncImagePainter
|
|
||||||
import coil.imageLoader
|
|
||||||
import dev.meloda.fast.model.api.domain.VkLinkDomain
|
|
||||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
|
||||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Link(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
item: VkLinkDomain,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
onLongClick: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(min = 56.dp)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
var errorLoading by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 11-Apr-25, Danil Nikolaev: extract to ui model
|
|
||||||
val preview by remember(item) {
|
|
||||||
derivedStateOf { item.photo?.getMaxSize()?.url }
|
|
||||||
}
|
|
||||||
val urlFirstChar by remember(item) {
|
|
||||||
derivedStateOf {
|
|
||||||
item.url
|
|
||||||
.replace("https://", "")
|
|
||||||
.replace("http://", "")
|
|
||||||
.first()
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preview != null && !errorLoading) {
|
|
||||||
Image(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
.size(width = 48.dp, height = 36.dp),
|
|
||||||
painter = rememberAsyncImagePainter(
|
|
||||||
model = preview,
|
|
||||||
imageLoader = LocalContext.current.imageLoader,
|
|
||||||
onState = {
|
|
||||||
errorLoading = it is AsyncImagePainter.State.Error
|
|
||||||
}
|
|
||||||
),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(24.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp))
|
|
||||||
.size(width = 48.dp, height = 36.dp),
|
|
||||||
text = urlFirstChar,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
lineHeight = 36.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
if (item.title != null) {
|
|
||||||
Text(
|
|
||||||
text = item.title!!,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalContentAlpha(
|
|
||||||
alpha = if (item.title != null) ContentAlpha.medium
|
|
||||||
else ContentAlpha.high
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = item.url,
|
|
||||||
style = if (item.title != null) {
|
|
||||||
MaterialTheme.typography.bodyMedium
|
|
||||||
} else {
|
|
||||||
MaterialTheme.typography.bodyLarge
|
|
||||||
},
|
|
||||||
maxLines = if (item.title != null) 1 else 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-146
@@ -1,146 +0,0 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation.attachments
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.PlayArrow
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import dev.meloda.fast.ui.components.IconButton
|
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Previews(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
photos: ImmutableList<UiPreview>,
|
|
||||||
onClick: (index: Int) -> Unit = {},
|
|
||||||
onLongClick: (index: Int) -> Unit = {}
|
|
||||||
) {
|
|
||||||
DynamicPreviewGrid(
|
|
||||||
modifier = modifier,
|
|
||||||
photos = photos,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
|
||||||
@Composable
|
|
||||||
fun DynamicPreviewGrid(
|
|
||||||
photos: ImmutableList<UiPreview>,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onClick: (index: Int) -> Unit = {},
|
|
||||||
onLongClick: (index: Int) -> Unit = {}
|
|
||||||
) {
|
|
||||||
val spacing = 2.dp
|
|
||||||
val shape = RoundedCornerShape(8.dp)
|
|
||||||
|
|
||||||
BoxWithConstraints(modifier = modifier) {
|
|
||||||
val maxWidthPx = with(LocalDensity.current) { maxWidth.toPx() }
|
|
||||||
val spacingPx = with(LocalDensity.current) { spacing.toPx() }
|
|
||||||
|
|
||||||
val rows = photos.chunked(3)
|
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(spacing)) {
|
|
||||||
rows.forEachIndexed { index, row ->
|
|
||||||
val aspectRatios = row.map { it.width.toFloat() / it.height }
|
|
||||||
val totalAspect = aspectRatios.sum()
|
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(spacing)) {
|
|
||||||
row.forEachIndexed { index, preview ->
|
|
||||||
val weight = aspectRatios[index] / totalAspect
|
|
||||||
val photoWidthPx = (maxWidthPx - spacingPx * (row.size - 1)) * weight
|
|
||||||
val height = photoWidthPx / aspectRatios[index]
|
|
||||||
val heightDp = with(LocalDensity.current) { height.toDp() }
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(heightDp)
|
|
||||||
.weight(weight)
|
|
||||||
.clip(shape),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = preview.url,
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.height(heightDp)
|
|
||||||
.clip(shape)
|
|
||||||
.combinedClickable(
|
|
||||||
onLongClick = { onLongClick(index) },
|
|
||||||
onClick = { onClick(index) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (preview.isVideo) {
|
|
||||||
IconButton(
|
|
||||||
onClick = { onClick(index) },
|
|
||||||
modifier = Modifier
|
|
||||||
.size(36.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(Color.Black.copy(alpha = 0.5f))
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier,
|
|
||||||
imageVector = Icons.Rounded.PlayArrow,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = Color.White
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun PreviewDynamicPhotoGrid() {
|
|
||||||
val mockPhotos = listOf(
|
|
||||||
UiPreview(0, "https://picsum.photos/id/1011/600/400", 600, 400, false),
|
|
||||||
UiPreview(0, "https://picsum.photos/id/1012/500/500", 500, 500, false),
|
|
||||||
UiPreview(0, "https://picsum.photos/id/1013/400/600", 400, 600, false),
|
|
||||||
UiPreview(0, "https://picsum.photos/id/1014/600/600", 600, 600, false),
|
|
||||||
UiPreview(0, "https://picsum.photos/id/1015/800/600", 800, 600, false),
|
|
||||||
UiPreview(0, "https://picsum.photos/id/1016/700/500", 700, 500, false),
|
|
||||||
UiPreview(0, "https://picsum.photos/id/1018/600/600", 600, 600, false),
|
|
||||||
UiPreview(0, "https://picsum.photos/id/1020/600/800", 600, 800, false),
|
|
||||||
UiPreview(0, "https://picsum.photos/id/1021/800/800", 800, 800, false),
|
|
||||||
UiPreview(0, "https://picsum.photos/id/1022/500/700", 500, 700, false),
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(8.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
DynamicPreviewGrid(photos = mockPhotos.take(10).toImmutableList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+8
-18
@@ -107,7 +107,7 @@ fun VkMessage.asPresentation(
|
|||||||
): UiItem = when {
|
): UiItem = when {
|
||||||
action != null -> UiItem.ActionMessage(
|
action != null -> UiItem.ActionMessage(
|
||||||
id = id,
|
id = id,
|
||||||
cmId = cmId,
|
conversationMessageId = cmId,
|
||||||
text = extractActionText(
|
text = extractActionText(
|
||||||
resources = resourceProvider.resources,
|
resources = resourceProvider.resources,
|
||||||
youPrefix = resourceProvider.getString(R.string.you_message_prefix),
|
youPrefix = resourceProvider.getString(R.string.you_message_prefix),
|
||||||
@@ -118,7 +118,7 @@ fun VkMessage.asPresentation(
|
|||||||
|
|
||||||
else -> UiItem.Message(
|
else -> UiItem.Message(
|
||||||
id = id,
|
id = id,
|
||||||
cmId = cmId,
|
conversationMessageId = cmId,
|
||||||
text = extractTextWithVisualizedMentions(
|
text = extractTextWithVisualizedMentions(
|
||||||
isOut = isOut,
|
isOut = isOut,
|
||||||
originalText = text,
|
originalText = text,
|
||||||
@@ -143,8 +143,7 @@ fun VkMessage.asPresentation(
|
|||||||
},
|
},
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
isPinned = isPinned,
|
isPinned = isPinned,
|
||||||
isImportant = isImportant,
|
isImportant = isImportant
|
||||||
attachments = attachments?.ifEmpty { null }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,20 +600,11 @@ fun extractTextWithVisualizedMentions(
|
|||||||
val startIndex = mention.indexRange.first
|
val startIndex = mention.indexRange.first
|
||||||
val endIndex = mention.indexRange.last
|
val endIndex = mention.indexRange.last
|
||||||
|
|
||||||
annotations += if (isOut) {
|
annotations += AnnotatedString.Range(
|
||||||
AnnotatedString.Range(
|
item = SpanStyle(color = Color.Red),
|
||||||
item = SpanStyle(textDecoration = TextDecoration.Underline),
|
start = startIndex,
|
||||||
start = startIndex,
|
end = endIndex
|
||||||
end = endIndex
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AnnotatedString.Range(
|
|
||||||
item = SpanStyle(color = Color.Red),
|
|
||||||
start = startIndex,
|
|
||||||
end = endIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
annotations += AnnotatedString.Range(
|
annotations += AnnotatedString.Range(
|
||||||
item = StringAnnotation(mention.id.toString()),
|
item = StringAnnotation(mention.id.toString()),
|
||||||
tag = mention.idPrefix,
|
tag = mention.idPrefix,
|
||||||
|
|||||||
+12
-12
@@ -3,34 +3,34 @@ minSdk = "23"
|
|||||||
targetSdk = "35"
|
targetSdk = "35"
|
||||||
compileSdk = "35"
|
compileSdk = "35"
|
||||||
versionCode = "10"
|
versionCode = "10"
|
||||||
versionName = "0.2.1"
|
versionName = "0.2.0"
|
||||||
|
|
||||||
agp = "8.10.0"
|
agp = "8.9.1"
|
||||||
converterMoshi = "2.11.0"
|
converterMoshi = "2.11.0"
|
||||||
eithernet = "2.0.0"
|
eithernet = "2.0.0"
|
||||||
haze = "1.5.4"
|
haze = "1.5.2"
|
||||||
kotlin = "2.1.20"
|
kotlin = "2.1.20"
|
||||||
ksp = "2.1.20-2.0.1"
|
ksp = "2.1.20-1.0.32"
|
||||||
|
|
||||||
compose-bom = "2025.05.00"
|
compose-bom = "2025.03.01"
|
||||||
koin = "4.0.4"
|
koin = "4.0.4"
|
||||||
|
|
||||||
accompanist = "0.37.2"
|
accompanist = "0.37.2"
|
||||||
coil = "2.7.0"
|
coil = "2.7.0"
|
||||||
coroutines = "1.10.2"
|
coroutines = "1.10.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
chucker = "4.1.0"
|
chucker = "4.1.0"
|
||||||
guava = "33.4.8-jre"
|
guava = "33.4.6-jre"
|
||||||
lifecycle = "2.9.0"
|
lifecycle = "2.8.7"
|
||||||
core-ktx = "1.16.0"
|
core-ktx = "1.15.0"
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
loggingInterceptor = "5.0.0-alpha.14"
|
loggingInterceptor = "5.0.0-alpha.14"
|
||||||
moshi = "1.15.2"
|
moshi = "1.15.2"
|
||||||
retrofit = "2.11.0"
|
retrofit = "2.11.0"
|
||||||
room = "2.7.1"
|
room = "2.6.1"
|
||||||
preference-ktx = "1.2.1"
|
preference-ktx = "1.2.1"
|
||||||
nanokt = "1.2.0"
|
nanokt = "1.2.0"
|
||||||
androidx-navigation = "2.9.0"
|
androidx-navigation = "2.8.9"
|
||||||
serialization = "1.8.1"
|
serialization = "1.8.1"
|
||||||
moduleGraph = "2.8.0"
|
moduleGraph = "2.8.0"
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-j
|
|||||||
|
|
||||||
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
|
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
|
||||||
compose-material3 = { module = "androidx.compose.material3:material3" }
|
compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||||
compose-ui = { module = "androidx.compose.ui:ui" }
|
compose-ui = { module = "androidx.compose.ui:ui", version = "1.8.0-rc02" }
|
||||||
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||||
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||||
compose-material3-windowsize = { module = "androidx.compose.material3:material3-window-size-class" }
|
compose-material3-windowsize = { module = "androidx.compose.material3:material3-window-size-class" }
|
||||||
|
|||||||
Reference in New Issue
Block a user