feat(talk): меню сообщения — Копировать/Копировать ссылку/Переслать в «Личные заметки» (P1 talk.diff)

This commit is contained in:
b-mob-conf
2026-07-13 06:42:00 +00:00
parent 056e4dd8ba
commit 59543a7028
6 changed files with 76 additions and 5 deletions
+1 -1
View File
@@ -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/<token>#message_<id>` → буфер) и «Переслать в «Личные заметки»» (`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 14), Сегодня (start|due ≤ сегодня), На неделе (≤ сегодня+6), Все (все незавершённые), Текущие (start не в будущем/без start), Завершенные; агрегация по всем реальным спискам; модель видимости `SmartListVisibility` (под будущий SharedPreferences). Добавлено feature-поле `TaskItem.startRaw` + хелпер `startLocalDate()` (пока пусто — DTSTART в `DavTask` ещё нет, запрошен у лида отдельным письмом). Заведена тест-инфра модуля (`testImplementation junit`) + 22 юнит-теста семантики/видимости — зелёные. Detekt-долг чужого пакета v0.5.133137 (TasksScreen/ViewModel/Components) унесён в `detekt-baseline.xml`, код лида не тронут. UI-подключение — следующий шаг. | контракты не менялись | низкий: изолированная логика, в UI ещё не выведено - 2026-07-12 | (без бампа — версия/релиз за лидом) | **Задачи — умные списки, этап 1 (чистая логика + юниты).** Новый движок `TasksSmartLists` (`feature/tasks`): семантика 6 виртуальных списков строго по веб-Tasks Nextcloud — Важное (PRIORITY 14), Сегодня (start|due ≤ сегодня), На неделе (≤ сегодня+6), Все (все незавершённые), Текущие (start не в будущем/без start), Завершенные; агрегация по всем реальным спискам; модель видимости `SmartListVisibility` (под будущий SharedPreferences). Добавлено feature-поле `TaskItem.startRaw` + хелпер `startLocalDate()` (пока пусто — DTSTART в `DavTask` ещё нет, запрошен у лида отдельным письмом). Заведена тест-инфра модуля (`testImplementation junit`) + 22 юнит-теста семантики/видимости — зелёные. Detekt-долг чужого пакета v0.5.133137 (TasksScreen/ViewModel/Components) унесён в `detekt-baseline.xml`, код лида не тронут. UI-подключение — следующий шаг. | контракты не менялись | низкий: изолированная логика, в UI ещё не выведено
- 2026-07-10 | v0.5.144 (152) | СВОДНЫЙ РЕЛИЗ (включает непубликовавшиеся v0.5.133143). **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), меню «⋯» (вложенная задача/удаление), стрелка панели открывает списки, кэш ускоряет загрузку. **Файлы** (из 133137): .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/вложения) проверены компиляцией, на устройстве не гонялись - 2026-07-10 | v0.5.144 (152) | СВОДНЫЙ РЕЛИЗ (включает непубликовавшиеся v0.5.133143). **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), меню «⋯» (вложенная задача/удаление), стрелка панели открывает списки, кэш ускоряет загрузку. **Файлы** (из 133137): .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/вложения) проверены компиляцией, на устройстве не гонялись
+8 -4
View File
@@ -2,24 +2,27 @@
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues/> <ManuallySuppressedIssues/>
<CurrentIssues> <CurrentIssues>
<ID>ArgumentListWrapping:TalkComponents.kt$(message.attachmentFileName!!)</ID>
<ID>CyclomaticComplexMethod:TalkFormatting.kt$fun buildChatItems( messages: List&lt;TalkMessage&gt;, lastReadMessageId: Long, selfActorId: String, collapseSystemMessages: Boolean, ): List&lt;TalkChatItem&gt;</ID> <ID>CyclomaticComplexMethod:TalkFormatting.kt$fun buildChatItems( messages: List&lt;TalkMessage&gt;, lastReadMessageId: Long, selfActorId: String, collapseSystemMessages: Boolean, ): List&lt;TalkChatItem&gt;</ID>
<ID>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: () -&gt; Unit = {}, onRoomOpenStateChange: (Boolean) -&gt; Unit = {}, onOpenCalendar: () -&gt; Unit = {}, onUnauthorized: () -&gt; Unit = {}, )</ID> <ID>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: () -&gt; Unit = {}, onRoomOpenStateChange: (Boolean) -&gt; Unit = {}, onOpenCalendar: () -&gt; Unit = {}, onUnauthorized: () -&gt; Unit = {}, )</ID>
<ID>CyclomaticComplexMethod:TalkScreen.kt$@Composable private fun ChatContent( session: AuthSession, state: TalkUiState, draft: String, onDraftChange: (String) -&gt; Unit, onSend: () -&gt; Unit, onAttach: () -&gt; Unit, onVoiceToggle: () -&gt; Unit, voiceRecording: Boolean, onRetry: () -&gt; Unit, onStartCall: () -&gt; Unit, onBackClick: () -&gt; Unit, onOpenRoomInfo: () -&gt; Unit, onScheduleMeeting: () -&gt; Unit, onToggleSystemMessages: () -&gt; Unit, onReply: (TalkMessage) -&gt; Unit, onReaction: (TalkMessage, String) -&gt; Unit, onDeleteMessage: (TalkMessage) -&gt; Unit, onEditMessage: (TalkMessage, String) -&gt; Unit, onMentionQuery: (String?) -&gt; Unit, onMentionSelected: (TalkMention) -&gt; Unit, onClearReply: () -&gt; Unit, onClearHighlight: () -&gt; Unit, )</ID> <ID>CyclomaticComplexMethod:TalkScreen.kt$@Composable private fun ChatContent( session: AuthSession, state: TalkUiState, draft: String, onDraftChange: (String) -&gt; Unit, onSend: () -&gt; Unit, onAttach: () -&gt; Unit, onVoiceToggle: () -&gt; Unit, voiceRecording: Boolean, onRetry: () -&gt; Unit, onStartCall: () -&gt; Unit, onBackClick: () -&gt; Unit, onOpenRoomInfo: () -&gt; Unit, onScheduleMeeting: () -&gt; Unit, onToggleSystemMessages: () -&gt; Unit, onReply: (TalkMessage) -&gt; Unit, onReaction: (TalkMessage, String) -&gt; Unit, onDeleteMessage: (TalkMessage) -&gt; Unit, onEditMessage: (TalkMessage, String) -&gt; Unit, onForwardNoteToSelf: (TalkMessage) -&gt; Unit, onMentionQuery: (String?) -&gt; Unit, onMentionSelected: (TalkMention) -&gt; Unit, onClearReply: () -&gt; Unit, onClearHighlight: () -&gt; Unit, )</ID>
<ID>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</ID> <ID>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</ID>
<ID>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</ID> <ID>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</ID>
<ID>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</ID> <ID>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</ID>
<ID>Indentation:TalkScreen.kt$ </ID>
<ID>InstanceOfCheckForException:TalkViewModel.kt$TalkViewModel$t is UnauthorizedException</ID> <ID>InstanceOfCheckForException:TalkViewModel.kt$TalkViewModel$t is UnauthorizedException</ID>
<ID>LargeClass:TalkViewModel.kt$TalkViewModel : ViewModel</ID> <ID>LargeClass:TalkViewModel.kt$TalkViewModel : ViewModel</ID>
<ID>LongMethod:TalkComponents.kt$@Composable fun TalkComposer( session: AuthSession, value: String, onValueChange: (String) -&gt; Unit, onSend: () -&gt; Unit, onAttach: (() -&gt; Unit)? = null, onVoiceToggle: (() -&gt; Unit)? = null, voiceRecording: Boolean = false, mentionCandidates: List&lt;TalkMention&gt; = emptyList(), onMentionQuery: ((String?) -&gt; Unit)? = null, onMentionSelected: ((TalkMention) -&gt; Unit)? = null, sending: Boolean, uploading: Boolean = false, modifier: Modifier = Modifier, )</ID> <ID>LongMethod:TalkComponents.kt$@Composable fun TalkComposer( session: AuthSession, value: String, onValueChange: (String) -&gt; Unit, onSend: () -&gt; Unit, onAttach: (() -&gt; Unit)? = null, onVoiceToggle: (() -&gt; Unit)? = null, voiceRecording: Boolean = false, mentionCandidates: List&lt;TalkMention&gt; = emptyList(), onMentionQuery: ((String?) -&gt; Unit)? = null, onMentionSelected: ((TalkMention) -&gt; Unit)? = null, sending: Boolean, uploading: Boolean = false, modifier: Modifier = Modifier, )</ID>
<ID>LongMethod:TalkComponents.kt$@Composable fun TalkRoomRow( room: TalkRoom, session: AuthSession, modifier: Modifier = Modifier, onClick: () -&gt; Unit, onToggleFavorite: () -&gt; Unit = {}, onMarkUnread: () -&gt; Unit = {}, )</ID> <ID>LongMethod:TalkComponents.kt$@Composable fun TalkRoomRow( room: TalkRoom, session: AuthSession, modifier: Modifier = Modifier, onClick: () -&gt; Unit, onToggleFavorite: () -&gt; Unit = {}, onMarkUnread: () -&gt; Unit = {}, )</ID>
<ID>LongMethod:TalkComponents.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun TalkCreateRoomSheet( visible: Boolean, session: AuthSession, mode: CreateRoomMode, roomName: String, userSearchQuery: String, userSearchResults: List&lt;TalkUserCandidate&gt;, selectedUsers: List&lt;TalkUserCandidate&gt;, userSearchLoading: Boolean, creating: Boolean, onDismiss: () -&gt; Unit, onModeChange: (CreateRoomMode) -&gt; Unit, onRoomNameChange: (String) -&gt; Unit, onUserSearchChange: (String) -&gt; Unit, onUserToggle: (TalkUserCandidate) -&gt; Unit, onCreate: () -&gt; Unit, )</ID> <ID>LongMethod:TalkComponents.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun TalkCreateRoomSheet( visible: Boolean, session: AuthSession, mode: CreateRoomMode, roomName: String, userSearchQuery: String, userSearchResults: List&lt;TalkUserCandidate&gt;, selectedUsers: List&lt;TalkUserCandidate&gt;, userSearchLoading: Boolean, creating: Boolean, onDismiss: () -&gt; Unit, onModeChange: (CreateRoomMode) -&gt; Unit, onRoomNameChange: (String) -&gt; Unit, onUserSearchChange: (String) -&gt; Unit, onUserToggle: (TalkUserCandidate) -&gt; Unit, onCreate: () -&gt; Unit, )</ID>
<ID>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: () -&gt; Unit = {}, onRoomOpenStateChange: (Boolean) -&gt; Unit = {}, onOpenCalendar: () -&gt; Unit = {}, onUnauthorized: () -&gt; Unit = {}, )</ID> <ID>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: () -&gt; Unit = {}, onRoomOpenStateChange: (Boolean) -&gt; Unit = {}, onOpenCalendar: () -&gt; Unit = {}, onUnauthorized: () -&gt; Unit = {}, )</ID>
<ID>LongMethod:TalkScreen.kt$@Composable private fun ChatContent( session: AuthSession, state: TalkUiState, draft: String, onDraftChange: (String) -&gt; Unit, onSend: () -&gt; Unit, onAttach: () -&gt; Unit, onVoiceToggle: () -&gt; Unit, voiceRecording: Boolean, onRetry: () -&gt; Unit, onStartCall: () -&gt; Unit, onBackClick: () -&gt; Unit, onOpenRoomInfo: () -&gt; Unit, onScheduleMeeting: () -&gt; Unit, onToggleSystemMessages: () -&gt; Unit, onReply: (TalkMessage) -&gt; Unit, onReaction: (TalkMessage, String) -&gt; Unit, onDeleteMessage: (TalkMessage) -&gt; Unit, onEditMessage: (TalkMessage, String) -&gt; Unit, onMentionQuery: (String?) -&gt; Unit, onMentionSelected: (TalkMention) -&gt; Unit, onClearReply: () -&gt; Unit, onClearHighlight: () -&gt; Unit, )</ID> <ID>LongMethod:TalkScreen.kt$@Composable private fun ChatContent( session: AuthSession, state: TalkUiState, draft: String, onDraftChange: (String) -&gt; Unit, onSend: () -&gt; Unit, onAttach: () -&gt; Unit, onVoiceToggle: () -&gt; Unit, voiceRecording: Boolean, onRetry: () -&gt; Unit, onStartCall: () -&gt; Unit, onBackClick: () -&gt; Unit, onOpenRoomInfo: () -&gt; Unit, onScheduleMeeting: () -&gt; Unit, onToggleSystemMessages: () -&gt; Unit, onReply: (TalkMessage) -&gt; Unit, onReaction: (TalkMessage, String) -&gt; Unit, onDeleteMessage: (TalkMessage) -&gt; Unit, onEditMessage: (TalkMessage, String) -&gt; Unit, onForwardNoteToSelf: (TalkMessage) -&gt; Unit, onMentionQuery: (String?) -&gt; Unit, onMentionSelected: (TalkMention) -&gt; Unit, onClearReply: () -&gt; Unit, onClearHighlight: () -&gt; Unit, )</ID>
<ID>LoopWithTooManyJumpStatements:TalkRepository.kt$TalkRepository$for</ID> <ID>LoopWithTooManyJumpStatements:TalkRepository.kt$TalkRepository$for</ID>
<ID>LoopWithTooManyJumpStatements:TalkViewModel.kt$TalkViewModel$while</ID> <ID>LoopWithTooManyJumpStatements:TalkViewModel.kt$TalkViewModel$while</ID>
<ID>MatchingDeclarationName:TalkFilters.kt$TalkRoomFilter</ID> <ID>MatchingDeclarationName:TalkFilters.kt$TalkRoomFilter</ID>
<ID>MatchingDeclarationName:TalkFormatting.kt$TalkChatItem</ID> <ID>MatchingDeclarationName:TalkFormatting.kt$TalkChatItem</ID>
<ID>MaximumLineLength:TalkCallActivity.kt$TalkCallActivity$ </ID> <ID>MaximumLineLength:TalkCallActivity.kt$TalkCallActivity$ </ID>
<ID>MaximumLineLength:TalkComponents.kt$ </ID>
<ID>MultiLineIfElse:TalkComponents.kt$Modifier</ID> <ID>MultiLineIfElse:TalkComponents.kt$Modifier</ID>
<ID>MultiLineIfElse:TalkComponents.kt$Modifier.graphicsLayer { rotationZ = 180f }</ID> <ID>MultiLineIfElse:TalkComponents.kt$Modifier.graphicsLayer { rotationZ = 180f }</ID>
<ID>NestedBlockDepth:TalkViewModel.kt$TalkViewModel$private fun resolveDisplayName(context: android.content.Context, uri: android.net.Uri, mimeType: String): String</ID> <ID>NestedBlockDepth:TalkViewModel.kt$TalkViewModel$private fun resolveDisplayName(context: android.content.Context, uri: android.net.Uri, mimeType: String): String</ID>
@@ -28,6 +31,7 @@
<ID>NoUnusedImports:TalkRepository.kt$ru.forbion.f7cloud.feature.talk.TalkRepository.kt</ID> <ID>NoUnusedImports:TalkRepository.kt$ru.forbion.f7cloud.feature.talk.TalkRepository.kt</ID>
<ID>NoUnusedImports:TalkScreen.kt$ru.forbion.f7cloud.feature.talk.TalkScreen.kt</ID> <ID>NoUnusedImports:TalkScreen.kt$ru.forbion.f7cloud.feature.talk.TalkScreen.kt</ID>
<ID>NoUnusedImports:TalkShareActivity.kt$ru.forbion.f7cloud.feature.talk.TalkShareActivity.kt</ID> <ID>NoUnusedImports:TalkShareActivity.kt$ru.forbion.f7cloud.feature.talk.TalkShareActivity.kt</ID>
<ID>UnusedParameter:TalkComponents.kt$session: AuthSession</ID>
<ID>UnusedParameter:TalkRoomInfoSheet.kt$session: AuthSession</ID> <ID>UnusedParameter:TalkRoomInfoSheet.kt$session: AuthSession</ID>
<ID>Wrapping:TalkComponents.kt$;</ID> <ID>Wrapping:TalkComponents.kt$;</ID>
<ID>Wrapping:TalkViewModel.kt$TalkUiState$(</ID> <ID>Wrapping:TalkViewModel.kt$TalkUiState$(</ID>
@@ -804,6 +804,9 @@ fun TalkMessageActionSheet(
onEdit: (TalkMessage) -> Unit, onEdit: (TalkMessage) -> Unit,
onDelete: (TalkMessage) -> Unit, onDelete: (TalkMessage) -> Unit,
onReaction: (TalkMessage, String) -> Unit, onReaction: (TalkMessage, String) -> Unit,
onCopy: (TalkMessage) -> Unit,
onCopyLink: (TalkMessage) -> Unit,
onForwardNoteToSelf: (TalkMessage) -> Unit,
) { ) {
if (message == null) return if (message == null) return
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
@@ -831,6 +834,14 @@ fun TalkMessageActionSheet(
F7TextButton(text = emoji, onClick = { onReaction(message, emoji) }) 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) { if (canModify) {
F7TextButton(text = "Изменить", onClick = { onEdit(message) }) F7TextButton(text = "Изменить", onClick = { onEdit(message) })
F7TextButton(text = "Удалить", onClick = { onDelete(message) }) F7TextButton(text = "Удалить", onClick = { onDelete(message) })
@@ -3,6 +3,7 @@ package ru.forbion.f7cloud.feature.talk
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -45,7 +46,9 @@ 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.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -292,6 +295,7 @@ fun TalkScreen(
onReaction = { msg, emoji -> vm.addReaction(session, msg.id, emoji) }, onReaction = { msg, emoji -> vm.addReaction(session, msg.id, emoji) },
onDeleteMessage = { vm.deleteMessage(session, it.id) }, onDeleteMessage = { vm.deleteMessage(session, it.id) },
onEditMessage = { msg, text -> vm.editMessage(session, msg.id, text) }, onEditMessage = { msg, text -> vm.editMessage(session, msg.id, text) },
onForwardNoteToSelf = { vm.forwardToNoteToSelf(session, it) },
onMentionQuery = { vm.setMentionQuery(session, it) }, onMentionQuery = { vm.setMentionQuery(session, it) },
onMentionSelected = { mention -> onMentionSelected = { mention ->
draft = insertMentionIntoDraft(draft, mention) draft = insertMentionIntoDraft(draft, mention)
@@ -430,6 +434,7 @@ private fun ChatContent(
onReaction: (TalkMessage, String) -> Unit, onReaction: (TalkMessage, String) -> Unit,
onDeleteMessage: (TalkMessage) -> Unit, onDeleteMessage: (TalkMessage) -> Unit,
onEditMessage: (TalkMessage, String) -> Unit, onEditMessage: (TalkMessage, String) -> Unit,
onForwardNoteToSelf: (TalkMessage) -> Unit,
onMentionQuery: (String?) -> Unit, onMentionQuery: (String?) -> Unit,
onMentionSelected: (TalkMention) -> Unit, onMentionSelected: (TalkMention) -> Unit,
onClearReply: () -> Unit, onClearReply: () -> Unit,
@@ -441,6 +446,8 @@ private fun ChatContent(
var editDraft by remember { mutableStateOf("") } var editDraft by remember { mutableStateOf("") }
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current
val clipboard = LocalClipboardManager.current
val chatItems = remember(state.messages, state.openRoomLastReadMessageId, state.collapseSystemMessages) { val chatItems = remember(state.messages, state.openRoomLastReadMessageId, state.collapseSystemMessages) {
buildChatItems( buildChatItems(
messages = state.messages, messages = state.messages,
@@ -507,6 +514,24 @@ private fun ChatContent(
onReaction(msg, emoji) onReaction(msg, emoji)
actionMessage = null 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 -> editingMessage?.let { message ->
@@ -19,4 +19,10 @@ object TalkUrls {
val base = session.serverUrl.trimEnd('/') val base = session.serverUrl.trimEnd('/')
return "$base/index.php/core/preview?fileId=$fileId&x=$size&y=$size&a=1&mode=cover&forceIcon=0" return "$base/index.php/core/preview?fileId=$fileId&x=$size&y=$size&a=1&mode=cover&forceIcon=0"
} }
/** Веб-ссылка на сообщение (/call/<token>#message_<id>). */
fun messageLink(session: AuthSession, token: String, messageId: Long): String {
val base = session.serverUrl.trimEnd('/')
return "$base/index.php/call/$token#message_$messageId"
}
} }
@@ -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) { fun deleteMessage(session: AuthSession, messageId: Long) {
val token = _state.value.selectedRoomToken ?: return val token = _state.value.selectedRoomToken ?: return
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -749,5 +771,8 @@ class TalkViewModel(
companion object { companion object {
private const val BACKGROUND_POLL_DELAY_MS = 5_000L private const val BACKGROUND_POLL_DELAY_MS = 5_000L
private const val POLL_ERROR_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
} }
} }