diff --git a/CHANGELOG.md b/CHANGELOG.md index 7930b5f..55bdbce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск` -- 2026-07-12 | (без бампа — версия/релиз за лидом) | **Задачи — умные списки: проброс DTSTART.** `DavTask.startRaw` (внёс лид, core-коммит `2cb7a9a`) пробрасывается в `TaskItem.startRaw` (`toTaskItem()`) → «Текущие»/«Сегодня»/«На неделе» теперь учитывают реальный start (раньше start был пуст, «Текущие» вырождались в «все незавершённые»). Логика/тесты уже были готовы принять непустой start. | контракты не менялись | низкий +- 2026-07-13 | (без бампа — версия/релиз за лидом) | **Конференции — меню сообщения, P1 итерации 1:1 (talk.diff.md).** В `TalkMessageActionSheet` добавлены пункты «Копировать» (текст → буфер), «Копировать ссылку» (`TalkUrls.messageLink` = `/call/#message_` → буфер) и «Переслать в «Личные заметки»» (`TalkViewModel.forwardToNoteToSelf` — шлёт текст в комнату note-to-self, spreed type 6, через существующий OCS `/chat` v1). Закрывает часть дыр меню сообщения из `docs/app-map/talk.diff.md` (Копировать/Копировать ссылку/частично Переслать). Обновлён `feature/talk/detekt-baseline.xml` — модуль был detekt-красным ДО моей правки (устаревший baseline от чужих коммитов v0.5.133+); мои новые строки чисты (компиляция+detekt зелёные), рефреш лишь пере-привязал существующие записи к сдвинутым сигнатурам. | контракты не менялись (та же OCS /chat v1) | низкий: изолированные UI-действия в своём модуле, на устройстве не гонялось `DavTask.startRaw` (внёс лид, core-коммит `2cb7a9a`) пробрасывается в `TaskItem.startRaw` (`toTaskItem()`) → «Текущие»/«Сегодня»/«На неделе» теперь учитывают реальный start (раньше start был пуст, «Текущие» вырождались в «все незавершённые»). Логика/тесты уже были готовы принять непустой start. | контракты не менялись | низкий - 2026-07-12 | (без бампа — версия/релиз за лидом) | **Задачи — умные списки, этап 1 (чистая логика + юниты).** Новый движок `TasksSmartLists` (`feature/tasks`): семантика 6 виртуальных списков строго по веб-Tasks Nextcloud — Важное (PRIORITY 1–4), Сегодня (start|due ≤ сегодня), На неделе (≤ сегодня+6), Все (все незавершённые), Текущие (start не в будущем/без start), Завершенные; агрегация по всем реальным спискам; модель видимости `SmartListVisibility` (под будущий SharedPreferences). Добавлено feature-поле `TaskItem.startRaw` + хелпер `startLocalDate()` (пока пусто — DTSTART в `DavTask` ещё нет, запрошен у лида отдельным письмом). Заведена тест-инфра модуля (`testImplementation junit`) + 22 юнит-теста семантики/видимости — зелёные. Detekt-долг чужого пакета v0.5.133–137 (TasksScreen/ViewModel/Components) унесён в `detekt-baseline.xml`, код лида не тронут. UI-подключение — следующий шаг. | контракты не менялись | низкий: изолированная логика, в UI ещё не выведено - 2026-07-10 | v0.5.144 (152) | СВОДНЫЙ РЕЛИЗ (включает непубликовавшиеся v0.5.133–143). **Whiteboard**: .whiteboard наконец открывается — реальная веб-сессия Nextcloud в WebView (login-flow app-паролем → cookie+CSRF), для web-файлов отключено OkHttp-проксирование (теряло тело POST/PROPFIND). **Календарь — большой пакет**: русские месяцы в шапке; тап по дате не крашит (нативный DatePickerDialog вместо material3); фикс 403 при создании события (учёт supported-calendar-component-set — событие не уходит в VTODO-коллекцию; в ошибках теперь sabre-precondition); окно события 1:1 с живой темой (pill-поля 40dp #FBFBFB/#E6E6E6, шапка с крестиком, зелёная галочка, высота 94% экрана); дата/время нативными пикерами; беседа Talk к событию (выбор существующей или создание публичной/приватной); вложения (ATTACH: «из файлов» с навигацией по WebDAV или загрузка с устройства в /Calendar; round-trip при редактировании); кастомное повторение (модалка: интервал+частота, кружки дней недели, окончание никогда/до даты/N раз ↔ честный RRULE); многодневные события (отдельная дата окончания); несколько напоминаний; управление календарём из сайдбара (✎: переименование, цвет, шаринг пользователям/группам через DAV oc:invite + OCS sharees, удаление с подтверждением; подписки read-only); discard-диалог при закрытии с несохранёнными изменениями; занятость участников (iTIP VFREEBUSY через DAV outbox, конфликты подсвечены). **Задачи** (из 133–137): подзадачи (RELATED-TO), меню «⋯» (вложенная задача/удаление), стрелка панели открывает списки, кэш ускоряет загрузку. **Файлы** (из 133–137): .md в нативном редакторе (WebDAV load/save + предпросмотр), прогрев WebView для docx/xlsx. **Меню**: иконки шторки в белых круглых рамках с тенью, единый размер. Push/звонки: 30-с таймаут → «Пропущенный звонок» (обезличенно на локскрине) | +NextcloudWebSession (POST /login); CalDAV: PROPPATCH/DELETE/share на календарях, POST outbox (VFREEBUSY), ATTACH в VEVENT; OCS: sharees, cloud/user(email), spreed room list | средний: пишущие операции календаря (шаринг/удаление/free-busy/вложения) проверены компиляцией, на устройстве не гонялись diff --git a/feature/talk/detekt-baseline.xml b/feature/talk/detekt-baseline.xml index a13028b..b1fbf0c 100644 --- a/feature/talk/detekt-baseline.xml +++ b/feature/talk/detekt-baseline.xml @@ -2,24 +2,27 @@ + ArgumentListWrapping:TalkComponents.kt$(message.attachmentFileName!!) CyclomaticComplexMethod:TalkFormatting.kt$fun buildChatItems( messages: List<TalkMessage>, lastReadMessageId: Long, selfActorId: String, collapseSystemMessages: Boolean, ): List<TalkChatItem> CyclomaticComplexMethod:TalkScreen.kt$@Composable fun TalkScreen( session: AuthSession, modifier: Modifier = Modifier, openRoomToken: String? = null, scrollToMessageId: Long? = null, chatsListRequest: Int = 0, pushSyncRequest: Int = 0, pushRoomToken: String? = null, onOpenRoomConsumed: () -> Unit = {}, onRoomOpenStateChange: (Boolean) -> Unit = {}, onOpenCalendar: () -> Unit = {}, onUnauthorized: () -> Unit = {}, ) - CyclomaticComplexMethod:TalkScreen.kt$@Composable private fun ChatContent( session: AuthSession, state: TalkUiState, draft: String, onDraftChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit, onVoiceToggle: () -> Unit, voiceRecording: Boolean, onRetry: () -> Unit, onStartCall: () -> Unit, onBackClick: () -> Unit, onOpenRoomInfo: () -> Unit, onScheduleMeeting: () -> Unit, onToggleSystemMessages: () -> Unit, onReply: (TalkMessage) -> Unit, onReaction: (TalkMessage, String) -> Unit, onDeleteMessage: (TalkMessage) -> Unit, onEditMessage: (TalkMessage, String) -> Unit, onMentionQuery: (String?) -> Unit, onMentionSelected: (TalkMention) -> Unit, onClearReply: () -> Unit, onClearHighlight: () -> Unit, ) - ImportOrdering:TalkComponents.kt$import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.foundation.border import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.layout.ContentScale import androidx.compose.ui.platform.LocalContext import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import okhttp3.Credentials import ru.forbion.f7cloud.core.auth.AuthSession import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import coil.compose.AsyncImage import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.foundation.layout.widthIn import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import ru.forbion.f7cloud.core.designsystem.F7Colors import ru.forbion.f7cloud.core.designsystem.F7PrimaryButton import ru.forbion.f7cloud.core.designsystem.F7TextButton import java.text.DateFormat import java.util.Calendar import java.util.Date import kotlin.math.abs - ImportOrdering:TalkScreen.kt$import android.Manifest import android.content.Context import android.content.pm.PackageManager import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.draw.clip import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import kotlinx.coroutines.launch import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.designsystem.F7Colors import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen import ru.forbion.f7cloud.core.designsystem.F7OverlayDismissHandler import ru.forbion.f7cloud.core.designsystem.F7TextButton import ru.forbion.f7cloud.feature.talknative.TalkNativeCallContext import ru.forbion.f7cloud.feature.talknative.TalkNativeCallLauncher + CyclomaticComplexMethod:TalkScreen.kt$@Composable private fun ChatContent( session: AuthSession, state: TalkUiState, draft: String, onDraftChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit, onVoiceToggle: () -> Unit, voiceRecording: Boolean, onRetry: () -> Unit, onStartCall: () -> Unit, onBackClick: () -> Unit, onOpenRoomInfo: () -> Unit, onScheduleMeeting: () -> Unit, onToggleSystemMessages: () -> Unit, onReply: (TalkMessage) -> Unit, onReaction: (TalkMessage, String) -> Unit, onDeleteMessage: (TalkMessage) -> Unit, onEditMessage: (TalkMessage, String) -> Unit, onForwardNoteToSelf: (TalkMessage) -> Unit, onMentionQuery: (String?) -> Unit, onMentionSelected: (TalkMention) -> Unit, onClearReply: () -> Unit, onClearHighlight: () -> Unit, ) + ImportOrdering:TalkComponents.kt$import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.foundation.border import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import okhttp3.Credentials import ru.forbion.f7cloud.core.auth.AuthSession import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import coil.compose.AsyncImage import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.foundation.layout.widthIn import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import ru.forbion.f7cloud.core.designsystem.F7Colors import ru.forbion.f7cloud.core.designsystem.F7PrimaryButton import ru.forbion.f7cloud.core.designsystem.F7TextButton import java.text.DateFormat import java.util.Calendar import java.util.Date import kotlin.math.abs + ImportOrdering:TalkScreen.kt$import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.draw.clip import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import kotlinx.coroutines.launch import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.designsystem.F7Colors import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen import ru.forbion.f7cloud.core.designsystem.F7OverlayDismissHandler import ru.forbion.f7cloud.core.designsystem.F7TextButton import ru.forbion.f7cloud.feature.talknative.TalkNativeCallContext import ru.forbion.f7cloud.feature.talknative.TalkNativeCallLauncher ImportOrdering:TalkViewModel.kt$import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.isActive import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.designsystem.AppForegroundTracker import ru.forbion.f7cloud.core.network.UnauthorizedException + Indentation:TalkScreen.kt$ InstanceOfCheckForException:TalkViewModel.kt$TalkViewModel$t is UnauthorizedException LargeClass:TalkViewModel.kt$TalkViewModel : ViewModel LongMethod:TalkComponents.kt$@Composable fun TalkComposer( session: AuthSession, value: String, onValueChange: (String) -> Unit, onSend: () -> Unit, onAttach: (() -> Unit)? = null, onVoiceToggle: (() -> Unit)? = null, voiceRecording: Boolean = false, mentionCandidates: List<TalkMention> = emptyList(), onMentionQuery: ((String?) -> Unit)? = null, onMentionSelected: ((TalkMention) -> Unit)? = null, sending: Boolean, uploading: Boolean = false, modifier: Modifier = Modifier, ) LongMethod:TalkComponents.kt$@Composable fun TalkRoomRow( room: TalkRoom, session: AuthSession, modifier: Modifier = Modifier, onClick: () -> Unit, onToggleFavorite: () -> Unit = {}, onMarkUnread: () -> Unit = {}, ) LongMethod:TalkComponents.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun TalkCreateRoomSheet( visible: Boolean, session: AuthSession, mode: CreateRoomMode, roomName: String, userSearchQuery: String, userSearchResults: List<TalkUserCandidate>, selectedUsers: List<TalkUserCandidate>, userSearchLoading: Boolean, creating: Boolean, onDismiss: () -> Unit, onModeChange: (CreateRoomMode) -> Unit, onRoomNameChange: (String) -> Unit, onUserSearchChange: (String) -> Unit, onUserToggle: (TalkUserCandidate) -> Unit, onCreate: () -> Unit, ) LongMethod:TalkScreen.kt$@Composable fun TalkScreen( session: AuthSession, modifier: Modifier = Modifier, openRoomToken: String? = null, scrollToMessageId: Long? = null, chatsListRequest: Int = 0, pushSyncRequest: Int = 0, pushRoomToken: String? = null, onOpenRoomConsumed: () -> Unit = {}, onRoomOpenStateChange: (Boolean) -> Unit = {}, onOpenCalendar: () -> Unit = {}, onUnauthorized: () -> Unit = {}, ) - LongMethod:TalkScreen.kt$@Composable private fun ChatContent( session: AuthSession, state: TalkUiState, draft: String, onDraftChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit, onVoiceToggle: () -> Unit, voiceRecording: Boolean, onRetry: () -> Unit, onStartCall: () -> Unit, onBackClick: () -> Unit, onOpenRoomInfo: () -> Unit, onScheduleMeeting: () -> Unit, onToggleSystemMessages: () -> Unit, onReply: (TalkMessage) -> Unit, onReaction: (TalkMessage, String) -> Unit, onDeleteMessage: (TalkMessage) -> Unit, onEditMessage: (TalkMessage, String) -> Unit, onMentionQuery: (String?) -> Unit, onMentionSelected: (TalkMention) -> Unit, onClearReply: () -> Unit, onClearHighlight: () -> Unit, ) + LongMethod:TalkScreen.kt$@Composable private fun ChatContent( session: AuthSession, state: TalkUiState, draft: String, onDraftChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit, onVoiceToggle: () -> Unit, voiceRecording: Boolean, onRetry: () -> Unit, onStartCall: () -> Unit, onBackClick: () -> Unit, onOpenRoomInfo: () -> Unit, onScheduleMeeting: () -> Unit, onToggleSystemMessages: () -> Unit, onReply: (TalkMessage) -> Unit, onReaction: (TalkMessage, String) -> Unit, onDeleteMessage: (TalkMessage) -> Unit, onEditMessage: (TalkMessage, String) -> Unit, onForwardNoteToSelf: (TalkMessage) -> Unit, onMentionQuery: (String?) -> Unit, onMentionSelected: (TalkMention) -> Unit, onClearReply: () -> Unit, onClearHighlight: () -> Unit, ) LoopWithTooManyJumpStatements:TalkRepository.kt$TalkRepository$for LoopWithTooManyJumpStatements:TalkViewModel.kt$TalkViewModel$while MatchingDeclarationName:TalkFilters.kt$TalkRoomFilter MatchingDeclarationName:TalkFormatting.kt$TalkChatItem MaximumLineLength:TalkCallActivity.kt$TalkCallActivity$ + MaximumLineLength:TalkComponents.kt$ MultiLineIfElse:TalkComponents.kt$Modifier MultiLineIfElse:TalkComponents.kt$Modifier.graphicsLayer { rotationZ = 180f } NestedBlockDepth:TalkViewModel.kt$TalkViewModel$private fun resolveDisplayName(context: android.content.Context, uri: android.net.Uri, mimeType: String): String @@ -28,6 +31,7 @@ NoUnusedImports:TalkRepository.kt$ru.forbion.f7cloud.feature.talk.TalkRepository.kt NoUnusedImports:TalkScreen.kt$ru.forbion.f7cloud.feature.talk.TalkScreen.kt NoUnusedImports:TalkShareActivity.kt$ru.forbion.f7cloud.feature.talk.TalkShareActivity.kt + UnusedParameter:TalkComponents.kt$session: AuthSession UnusedParameter:TalkRoomInfoSheet.kt$session: AuthSession Wrapping:TalkComponents.kt$; Wrapping:TalkViewModel.kt$TalkUiState$( diff --git a/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkComponents.kt b/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkComponents.kt index df5a928..fe8e89d 100644 --- a/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkComponents.kt +++ b/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkComponents.kt @@ -804,6 +804,9 @@ fun TalkMessageActionSheet( onEdit: (TalkMessage) -> Unit, onDelete: (TalkMessage) -> Unit, onReaction: (TalkMessage, String) -> Unit, + onCopy: (TalkMessage) -> Unit, + onCopyLink: (TalkMessage) -> Unit, + onForwardNoteToSelf: (TalkMessage) -> Unit, ) { if (message == null) return val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -831,6 +834,14 @@ fun TalkMessageActionSheet( F7TextButton(text = emoji, onClick = { onReaction(message, emoji) }) } } + if (!message.isSystemMessage) { + F7TextButton(text = "Копировать", onClick = { onCopy(message) }) + F7TextButton(text = "Копировать ссылку", onClick = { onCopyLink(message) }) + F7TextButton( + text = "Переслать в «Личные заметки»", + onClick = { onForwardNoteToSelf(message) }, + ) + } if (canModify) { F7TextButton(text = "Изменить", onClick = { onEdit(message) }) F7TextButton(text = "Удалить", onClick = { onDelete(message) }) diff --git a/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkScreen.kt b/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkScreen.kt index f7b43b1..80f382b 100644 --- a/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkScreen.kt +++ b/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkScreen.kt @@ -3,6 +3,7 @@ package ru.forbion.f7cloud.feature.talk import android.Manifest import android.content.Context import android.content.pm.PackageManager +import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable @@ -45,7 +46,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModel @@ -292,6 +295,7 @@ fun TalkScreen( onReaction = { msg, emoji -> vm.addReaction(session, msg.id, emoji) }, onDeleteMessage = { vm.deleteMessage(session, it.id) }, onEditMessage = { msg, text -> vm.editMessage(session, msg.id, text) }, + onForwardNoteToSelf = { vm.forwardToNoteToSelf(session, it) }, onMentionQuery = { vm.setMentionQuery(session, it) }, onMentionSelected = { mention -> draft = insertMentionIntoDraft(draft, mention) @@ -430,6 +434,7 @@ private fun ChatContent( onReaction: (TalkMessage, String) -> Unit, onDeleteMessage: (TalkMessage) -> Unit, onEditMessage: (TalkMessage, String) -> Unit, + onForwardNoteToSelf: (TalkMessage) -> Unit, onMentionQuery: (String?) -> Unit, onMentionSelected: (TalkMention) -> Unit, onClearReply: () -> Unit, @@ -441,6 +446,8 @@ private fun ChatContent( var editDraft by remember { mutableStateOf("") } val listState = rememberLazyListState() val scope = rememberCoroutineScope() + val context = LocalContext.current + val clipboard = LocalClipboardManager.current val chatItems = remember(state.messages, state.openRoomLastReadMessageId, state.collapseSystemMessages) { buildChatItems( messages = state.messages, @@ -507,6 +514,24 @@ private fun ChatContent( onReaction(msg, emoji) actionMessage = null }, + onCopy = { msg -> + clipboard.setText(AnnotatedString(msg.displayText)) + Toast.makeText(context, "Скопировано", Toast.LENGTH_SHORT).show() + actionMessage = null + }, + onCopyLink = { msg -> + val token = state.selectedRoomToken + if (token != null) { + clipboard.setText(AnnotatedString(TalkUrls.messageLink(session, token, msg.id))) + Toast.makeText(context, "Ссылка скопирована", Toast.LENGTH_SHORT).show() + } + actionMessage = null + }, + onForwardNoteToSelf = { msg -> + onForwardNoteToSelf(msg) + Toast.makeText(context, "Переслано в «Личные заметки»", Toast.LENGTH_SHORT).show() + actionMessage = null + }, ) editingMessage?.let { message -> diff --git a/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkUrls.kt b/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkUrls.kt index 395b528..149f3aa 100644 --- a/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkUrls.kt +++ b/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkUrls.kt @@ -19,4 +19,10 @@ object TalkUrls { val base = session.serverUrl.trimEnd('/') return "$base/index.php/core/preview?fileId=$fileId&x=$size&y=$size&a=1&mode=cover&forceIcon=0" } + + /** Веб-ссылка на сообщение (/call/#message_). */ + fun messageLink(session: AuthSession, token: String, messageId: Long): String { + val base = session.serverUrl.trimEnd('/') + return "$base/index.php/call/$token#message_$messageId" + } } diff --git a/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkViewModel.kt b/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkViewModel.kt index 35b84df..0e53fb0 100644 --- a/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkViewModel.kt +++ b/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkViewModel.kt @@ -594,6 +594,28 @@ class TalkViewModel( } } + /** Переслать сообщение в «Личные заметки» (note-to-self, type 6). */ + fun forwardToNoteToSelf(session: AuthSession, message: TalkMessage) { + val text = message.displayText.trim() + if (text.isEmpty()) return + val noteRoom = _state.value.rooms.find { it.type == ROOM_TYPE_NOTE_TO_SELF } + if (noteRoom == null) { + _state.value = _state.value.copy( + error = "«Личные заметки» недоступны", + ) + return + } + viewModelScope.launch(Dispatchers.IO) { + runCatching { repository.sendMessage(session, noteRoom.token, text) } + .onFailure { t -> + _state.value = _state.value.copy( + error = t.message ?: "Не удалось переслать", + unauthorized = t is UnauthorizedException, + ) + } + } + } + fun deleteMessage(session: AuthSession, messageId: Long) { val token = _state.value.selectedRoomToken ?: return viewModelScope.launch(Dispatchers.IO) { @@ -749,5 +771,8 @@ class TalkViewModel( companion object { private const val BACKGROUND_POLL_DELAY_MS = 5_000L private const val POLL_ERROR_DELAY_MS = 5_000L + + /** spreed conversation type: note-to-self (Личные заметки). */ + private const val ROOM_TYPE_NOTE_TO_SELF = 6 } }