Skip to content

Commit 8868d94

Browse files
committed
Allow to delete peer
1 parent b3e028d commit 8868d94

File tree

23 files changed

+209
-89
lines changed

23 files changed

+209
-89
lines changed

README.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ PlainApp is an open-source app that lets you securely manage your phone from a w
1111
**Privacy First**
1212
- All data stays on your device — no cloud, no third-party storage
1313
- No Firebase Messaging or Analytics; only crash logs (optional) via Firebase Crashlytics
14-
- Secured with TLS + ChaCha20 encryption
14+
- Secured with TLS + ChaCha20-Poly1305 encryption
1515

1616
**Ad-Free, Always**
1717
- 100% ad-free experience, forever
@@ -34,6 +34,8 @@ Access a self-hosted webpage on the same network to manage your phone:
3434
- RSS reader with clean UI
3535
- Video and audio player (in-app and on the web)
3636
- TV casting for media
37+
- Pomodoro timer
38+
- Peer-to-peer chat and file sharing
3739

3840
**Always Improving**
3941
- More features are on the way
@@ -48,13 +50,6 @@ Discord: https://discord.gg/RQWcS6DEEe
4850

4951
QQ Group: 812409393
5052

51-
## Disclaimer
52-
53-
- ⚠️ The project is under **very active** development.
54-
- ⚠️ Expect bugs and breaking changes.
55-
- ⚠️ It is not perfect, I am always looking for ways to improve. If you find that the app is missing a certain feature, please don't hesitate to submit a feature request.
56-
- ⚠️ I kindly request everyone to ask questions and engage in discussions in a friendly manner.
57-
5853
## Donations :heart:
5954

6055
**This project needs you!** If you would like to support this project's further development, the creator of this project or the continuous maintenance of this project, **feel free to donate**.

app/src/main/java/com/ismartcoding/plain/db/DChat.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,7 @@ interface ChatDao {
196196

197197
@Query("DELETE FROM chats WHERE id in (:ids)")
198198
fun deleteByIds(ids: List<String>)
199+
200+
@Query("DELETE FROM chats WHERE to_id = :peerId OR from_id = :peerId")
201+
fun deleteByPeerId(peerId: String)
199202
}

app/src/main/java/com/ismartcoding/plain/features/ChatHelper.kt

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,57 @@ object ChatHelper {
6060
value: Any?,
6161
) {
6262
AppDatabase.instance.chatDao().delete(id)
63-
if (value is DMessageFiles) {
64-
value.items.forEach {
65-
File(it.uri.getFinalPath(context)).delete()
63+
when (value) {
64+
is DMessageFiles -> {
65+
value.items.forEach {
66+
File(it.uri.getFinalPath(context)).delete()
67+
}
68+
}
69+
70+
is DMessageImages -> {
71+
value.items.forEach {
72+
File(it.uri.getFinalPath(context)).delete()
73+
}
6674
}
67-
} else if (value is DMessageImages) {
68-
value.items.forEach {
69-
File(it.uri.getFinalPath(context)).delete()
75+
76+
is DMessageText -> {
77+
value.linkPreviews.forEach { preview ->
78+
preview.imageLocalPath?.let { path ->
79+
LinkPreviewHelper.deletePreviewImage(context, path)
80+
}
81+
}
7082
}
71-
} else if (value is DMessageText) {
72-
value.linkPreviews.forEach { preview ->
73-
preview.imageLocalPath?.let { path ->
74-
LinkPreviewHelper.deletePreviewImage(context, path)
83+
}
84+
}
85+
86+
suspend fun deleteAllChatsByPeerAsync(context: Context, peerId: String) {
87+
val chatDao = AppDatabase.instance.chatDao()
88+
val chats = chatDao.getByChatId(peerId)
89+
90+
// Delete all associated files first
91+
for (chat in chats) {
92+
when (val value = chat.content.value) {
93+
is DMessageFiles -> {
94+
value.items.forEach {
95+
File(it.uri.getFinalPath(context)).delete()
96+
}
97+
}
98+
is DMessageImages -> {
99+
value.items.forEach {
100+
File(it.uri.getFinalPath(context)).delete()
101+
}
102+
}
103+
is DMessageText -> {
104+
value.linkPreviews.forEach { preview ->
105+
preview.imageLocalPath?.let { path ->
106+
LinkPreviewHelper.deletePreviewImage(context, path)
107+
}
108+
}
75109
}
76110
}
77111
}
112+
113+
// Delete all chat records for this peer using SQL query
114+
chatDao.deleteByPeerId(peerId)
78115
}
79116
}

app/src/main/java/com/ismartcoding/plain/ui/models/ChatListViewModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.ismartcoding.plain.db.DChat
1212
import com.ismartcoding.plain.db.DPeer
1313
import com.ismartcoding.plain.events.HttpApiEvents
1414
import com.ismartcoding.plain.events.NearbyDeviceFoundEvent
15+
import com.ismartcoding.plain.features.ChatHelper
1516
import com.ismartcoding.plain.preferences.NearbyDiscoverablePreference
1617
import com.ismartcoding.plain.web.ChatApiManager
1718
import kotlinx.coroutines.Dispatchers
@@ -125,7 +126,13 @@ class ChatListViewModel : ViewModel() {
125126
fun removePeer(context: Context, peerId: String) {
126127
viewModelScope.launch(Dispatchers.IO) {
127128
try {
129+
// Delete all chat messages and associated files for this peer
130+
ChatHelper.deleteAllChatsByPeerAsync(context, peerId)
131+
132+
// Delete the peer record
128133
AppDatabase.instance.peerDao().delete(peerId)
134+
135+
// Reload key cache and peers list
129136
ChatApiManager.loadKeyCacheAsync()
130137
loadPeers()
131138
} catch (e: Exception) {

app/src/main/java/com/ismartcoding/plain/ui/page/root/components/PeerListItem.kt

Lines changed: 112 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@ package com.ismartcoding.plain.ui.page.root.components
22

33
import androidx.compose.foundation.ExperimentalFoundationApi
44
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.combinedClickable
56
import androidx.compose.foundation.layout.Box
67
import androidx.compose.foundation.layout.Column
78
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.fillMaxSize
810
import androidx.compose.foundation.layout.fillMaxWidth
911
import androidx.compose.foundation.layout.padding
1012
import androidx.compose.foundation.layout.size
13+
import androidx.compose.foundation.layout.wrapContentSize
1114
import androidx.compose.foundation.shape.CircleShape
1215
import androidx.compose.material3.Icon
1316
import androidx.compose.material3.MaterialTheme
1417
import androidx.compose.material3.Surface
1518
import androidx.compose.material3.Text
1619
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.mutableStateOf
21+
import androidx.compose.runtime.remember
1722
import androidx.compose.ui.Alignment
1823
import androidx.compose.ui.Modifier
1924
import androidx.compose.ui.draw.clip
@@ -29,7 +34,10 @@ import com.ismartcoding.plain.db.DMessageImages
2934
import com.ismartcoding.plain.db.DMessageText
3035
import com.ismartcoding.plain.db.DMessageType
3136
import com.ismartcoding.plain.extensions.timeAgo
37+
import com.ismartcoding.plain.ui.base.PDropdownMenu
38+
import com.ismartcoding.plain.ui.base.PDropdownMenuItem
3239
import com.ismartcoding.plain.ui.base.VerticalSpace
40+
import com.ismartcoding.plain.ui.helpers.DialogHelper
3341
import com.ismartcoding.plain.ui.theme.green
3442
import com.ismartcoding.plain.ui.theme.grey
3543
import com.ismartcoding.plain.ui.theme.listItemSubtitle
@@ -45,78 +53,125 @@ fun PeerListItem(
4553
icon: Int,
4654
online: Boolean? = null,
4755
latestChat: DChat? = null,
56+
peerId: String? = null,
57+
onDelete: ((String) -> Unit)? = null,
58+
onClick: () -> Unit = {},
4859
) {
60+
val showContextMenu = remember { mutableStateOf(false) }
61+
val deleteText = stringResource(id = R.string.delete)
62+
val deleteWarningText = stringResource(id = R.string.delete_peer_warning)
63+
val cancelText = stringResource(id = R.string.cancel)
64+
4965
Surface(
50-
modifier =
51-
modifier,
66+
modifier = modifier.combinedClickable(
67+
onClick = onClick,
68+
onLongClick = {
69+
if (peerId != null && onDelete != null) {
70+
showContextMenu.value = true
71+
}
72+
}
73+
),
5274
color = Color.Unspecified,
5375
) {
54-
Row(
55-
modifier =
56-
Modifier
76+
Box {
77+
Row(
78+
modifier = Modifier
5779
.fillMaxWidth()
5880
.padding(4.dp, 8.dp, 8.dp, 8.dp),
59-
verticalAlignment = Alignment.CenterVertically,
60-
) {
61-
Box(
62-
modifier = Modifier
63-
.padding(end = 8.dp)
64-
.size(40.dp)
65-
.padding(2.dp),
66-
contentAlignment = Alignment.Center
81+
verticalAlignment = Alignment.CenterVertically,
6782
) {
68-
Icon(
69-
painter = painterResource(icon),
70-
contentDescription = title,
71-
modifier = Modifier.size(24.dp),
72-
tint = MaterialTheme.colorScheme.primary
73-
)
74-
if (online != null) {
75-
Box(
76-
modifier = Modifier
77-
.size(10.dp)
78-
.clip(CircleShape)
79-
.background(MaterialTheme.colorScheme.surface)
80-
.padding(1.dp)
81-
.clip(CircleShape)
82-
.background(
83-
if (online)
84-
MaterialTheme.colorScheme.green
85-
else
86-
MaterialTheme.colorScheme.grey
87-
)
88-
.align(Alignment.BottomEnd)
83+
Box(
84+
modifier = Modifier
85+
.padding(end = 8.dp)
86+
.size(40.dp)
87+
.padding(2.dp),
88+
contentAlignment = Alignment.Center
89+
) {
90+
Icon(
91+
painter = painterResource(icon),
92+
contentDescription = title,
93+
modifier = Modifier.size(24.dp),
94+
tint = MaterialTheme.colorScheme.primary
8995
)
96+
if (online != null) {
97+
Box(
98+
modifier = Modifier
99+
.size(10.dp)
100+
.clip(CircleShape)
101+
.background(MaterialTheme.colorScheme.surface)
102+
.padding(1.dp)
103+
.clip(CircleShape)
104+
.background(
105+
if (online)
106+
MaterialTheme.colorScheme.green
107+
else
108+
MaterialTheme.colorScheme.grey
109+
)
110+
.align(Alignment.BottomEnd)
111+
)
112+
}
90113
}
91-
}
92-
Column(
93-
modifier = Modifier
94-
.weight(1f)
95-
.padding(vertical = 8.dp)
96-
) {
97-
Row(
98-
modifier = Modifier.fillMaxWidth(),
99-
verticalAlignment = Alignment.CenterVertically
114+
Column(
115+
modifier = Modifier
116+
.weight(1f)
117+
.padding(vertical = 8.dp)
100118
) {
119+
Row(
120+
modifier = Modifier.fillMaxWidth(),
121+
verticalAlignment = Alignment.CenterVertically
122+
) {
123+
Text(
124+
text = title,
125+
style = MaterialTheme.typography.listItemTitle(),
126+
modifier = Modifier.weight(1f)
127+
)
128+
latestChat?.let { chat ->
129+
Text(
130+
text = chat.createdAt.timeAgo(),
131+
style = MaterialTheme.typography.listItemSubtitle(),
132+
)
133+
}
134+
}
135+
VerticalSpace(dp = 8.dp)
101136
Text(
102-
text = title,
103-
style = MaterialTheme.typography.listItemTitle(),
104-
modifier = Modifier.weight(1f)
137+
text = latestChat?.let { getMessagePreview(it) } ?: desc,
138+
style = MaterialTheme.typography.listItemSubtitle(),
139+
maxLines = 1,
140+
overflow = TextOverflow.Ellipsis
105141
)
106-
latestChat?.let { chat ->
107-
Text(
108-
text = chat.createdAt.timeAgo(),
109-
style = MaterialTheme.typography.listItemSubtitle(),
142+
}
143+
}
144+
145+
// Context menu for peer items
146+
if (peerId != null && onDelete != null) {
147+
Box(
148+
modifier = Modifier
149+
.fillMaxSize()
150+
.padding(top = 32.dp)
151+
.wrapContentSize(Alignment.Center),
152+
) {
153+
PDropdownMenu(
154+
expanded = showContextMenu.value,
155+
onDismissRequest = {
156+
showContextMenu.value = false
157+
},
158+
) {
159+
PDropdownMenuItem(
160+
text = { Text(deleteText) },
161+
onClick = {
162+
showContextMenu.value = false
163+
DialogHelper.showConfirmDialog(
164+
title = deleteText,
165+
message = deleteWarningText,
166+
confirmButton = Pair(deleteText) {
167+
onDelete(peerId)
168+
},
169+
dismissButton = Pair(cancelText) {}
170+
)
171+
},
110172
)
111173
}
112174
}
113-
VerticalSpace(dp = 8.dp)
114-
Text(
115-
text = latestChat?.let { getMessagePreview(it) } ?: desc,
116-
style = MaterialTheme.typography.listItemSubtitle(),
117-
maxLines = 1,
118-
overflow = TextOverflow.Ellipsis
119-
)
120175
}
121176
}
122177
}

0 commit comments

Comments
 (0)