Skip to content

Commit a636e20

Browse files
authored
Merge pull request #813 from DimensionDev/feature/pin
add pinned display
2 parents 9c89ee6 + 300a68c commit a636e20

File tree

14 files changed

+244
-62
lines changed

14 files changed

+244
-62
lines changed

iosApp/iosApp/UI/Page/Home/Components/Status/StatusRetweetHeaderComponent.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct StatusRetweetHeaderComponent: View {
1717
case .repost: String(localized: "bluesky_notification_item_reblogged_your_status")
1818
case .unKnown: String(localized: "bluesky_notification_item_unKnown")
1919
case .starterpackJoined: String(localized: "bluesky_notification_item_starterpack_joined")
20+
case .pinned: String(localized: "bluesky_notification_item_pin")
2021
}
2122
case let .mastodon(data):
2223
switch onEnum(of: data) {
@@ -29,6 +30,7 @@ struct StatusRetweetHeaderComponent: View {
2930
case .status: String(localized: "mastodon_notification_item_posted_status")
3031
case .update: String(localized: "mastodon_notification_item_updated_status")
3132
case .unKnown: String(localized: "mastodon_notification_item_updated_status")
33+
case .pinned: String(localized: "mastodon_item_pinned")
3234
}
3335
case let .misskey(data):
3436
switch onEnum(of: data) {
@@ -44,6 +46,7 @@ struct StatusRetweetHeaderComponent: View {
4446
case .renote: String(localized: "misskey_notification_renote")
4547
case .reply: String(localized: "misskey_notification_reply")
4648
case .unKnown: String(localized: "misskey_notification_unknown")
49+
case .pinned: String(localized: "misskey_item_pinned")
4750
}
4851
case let .vVO(data):
4952
switch onEnum(of: data) {
@@ -132,6 +135,14 @@ struct StatusRetweetHeaderComponent: View {
132135
#endif
133136
.size(14)
134137
.frame(alignment: .center)
138+
case .pin: Awesome.Classic.Solid.thumbtack.image
139+
#if os(macOS)
140+
.foregroundColor(.labelColor)
141+
#elseif os(iOS)
142+
.foregroundColor(.label)
143+
#endif
144+
.size(14)
145+
.frame(alignment: .center)
135146
}
136147
Markdown {
137148
(nameMarkdown ?? "") + (nameMarkdown == nil ? "" : " ") + text

shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Mastodon.kt

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ internal object Mastodon {
2323
pagingKey: String,
2424
database: CacheDatabase,
2525
data: List<Status>,
26-
sortIdProvider: (Status) -> Long = { it.createdAt?.toEpochMilliseconds() ?: 0 },
26+
sortIdProvider: (Status) -> Long = {
27+
if (it.pinned == true) {
28+
Long.MAX_VALUE
29+
} else {
30+
it.createdAt?.toEpochMilliseconds() ?: 0
31+
}
32+
},
2733
) {
2834
val items = data.toDbPagingTimeline(accountKey, pagingKey, sortIdProvider)
2935
saveToDatabase(database, items)
@@ -62,7 +68,8 @@ internal fun List<Notification>.toDb(
6268
}
6369

6470
private fun Notification.toDbStatusWithUser(accountKey: MicroBlogKey): DbStatusWithUser {
65-
val user = this.account?.toDbUser(accountKey.host) ?: throw IllegalStateException("account is null")
71+
val user =
72+
this.account?.toDbUser(accountKey.host) ?: throw IllegalStateException("account is null")
6673
val status = this.toDbStatus(accountKey)
6774
return DbStatusWithUser(
6875
data = status,
@@ -71,7 +78,8 @@ private fun Notification.toDbStatusWithUser(accountKey: MicroBlogKey): DbStatusW
7178
}
7279

7380
private fun Notification.toDbStatus(accountKey: MicroBlogKey): DbStatus {
74-
val user = this.account?.toDbUser(accountKey.host) ?: throw IllegalStateException("account is null")
81+
val user =
82+
this.account?.toDbUser(accountKey.host) ?: throw IllegalStateException("account is null")
7583
return DbStatus(
7684
statusKey =
7785
MicroBlogKey(
@@ -115,7 +123,8 @@ private fun Status.toDbStatusWithUser(accountKey: MicroBlogKey): DbStatusWithUse
115123
DbStatus(
116124
statusKey =
117125
MicroBlogKey(
118-
id ?: throw IllegalArgumentException("mastodon Status.idStr should not be null"),
126+
id
127+
?: throw IllegalArgumentException("mastodon Status.idStr should not be null"),
119128
host = user.userKey.host,
120129
),
121130
content =
@@ -129,7 +138,13 @@ private fun Status.toDbStatusWithUser(accountKey: MicroBlogKey): DbStatusWithUse
129138
append(spoilerText)
130139
append("\n\n")
131140
}
132-
append(parseMastodonContent(this@toDbStatusWithUser, accountKey, accountKey.host).toUi().raw)
141+
append(
142+
parseMastodonContent(
143+
this@toDbStatusWithUser,
144+
accountKey,
145+
accountKey.host,
146+
).toUi().raw,
147+
)
133148
},
134149
)
135150
return DbStatusWithUser(

shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ internal class GuestMastodonDataSource(
252252
host = host,
253253
userId = userKey.id,
254254
service = service,
255+
withPinned = true,
255256
)
256257
}.flow.cachedIn(scope),
257258
),

shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestUserTimelinePagingSource.kt

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,37 @@ internal class GuestUserTimelinePagingSource(
1212
private val userId: String,
1313
private val withReply: Boolean = false,
1414
private val onlyMedia: Boolean = false,
15+
private val withPinned: Boolean = false,
1516
) : BasePagingSource<String, UiTimeline>() {
1617
override fun getRefreshKey(state: PagingState<String, UiTimeline>): String? = null
1718

1819
override suspend fun doLoad(params: LoadParams<String>): LoadResult<String, UiTimeline> {
1920
val maxId = params.key
2021
val limit = params.loadSize
22+
val pinned =
23+
if (withPinned && maxId == null) {
24+
service.userTimeline(
25+
user_id = userId,
26+
pinned = true,
27+
)
28+
} else {
29+
emptyList()
30+
}
2131
val statuses =
22-
service.userTimeline(
23-
user_id = userId,
24-
limit = limit,
25-
max_id = maxId,
26-
only_media = onlyMedia,
27-
exclude_replies = !withReply,
28-
)
32+
service
33+
.userTimeline(
34+
user_id = userId,
35+
limit = limit,
36+
max_id = maxId,
37+
only_media = onlyMedia,
38+
exclude_replies = !withReply,
39+
).let {
40+
if (withPinned) {
41+
pinned + it
42+
} else {
43+
it
44+
}
45+
}
2946
return LoadResult.Page(
3047
data =
3148
statuses.map {

shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1348,7 +1348,24 @@ internal open class MastodonDataSource(
13481348
listOfNotNull(
13491349
ProfileTab.Timeline(
13501350
type = ProfileTab.Timeline.Type.Status,
1351-
flow = userTimeline(userKey, scope, pagingSize),
1351+
flow =
1352+
timelinePager(
1353+
pageSize = pagingSize,
1354+
pagingKey = "user_timeline_$userKey",
1355+
accountKey = accountKey,
1356+
database = database,
1357+
filterFlow = localFilterRepository.getFlow(forTimeline = true),
1358+
scope = scope,
1359+
mediator =
1360+
UserTimelineRemoteMediator(
1361+
service = service,
1362+
database = database,
1363+
accountKey = accountKey,
1364+
userKey = userKey,
1365+
pagingKey = "user_timeline_$userKey",
1366+
withPinned = true,
1367+
),
1368+
),
13521369
),
13531370
ProfileTab.Timeline(
13541371
type = ProfileTab.Timeline.Type.StatusWithReplies,

shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/UserTimelineRemoteMediator.kt

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,38 @@ internal class UserTimelineRemoteMediator(
1919
private val pagingKey: String,
2020
private val onlyMedia: Boolean = false,
2121
private val withReplies: Boolean = false,
22+
private val withPinned: Boolean = false,
2223
) : BaseRemoteMediator<Int, DbPagingTimelineWithStatus>() {
2324
override suspend fun doLoad(
2425
loadType: LoadType,
2526
state: PagingState<Int, DbPagingTimelineWithStatus>,
2627
): MediatorResult {
2728
val response =
2829
when (loadType) {
29-
LoadType.REFRESH ->
30-
service.userTimeline(
31-
user_id = userKey.id,
32-
limit = state.config.pageSize,
33-
only_media = onlyMedia,
34-
exclude_replies = !withReplies,
35-
)
30+
LoadType.REFRESH -> {
31+
val pinned =
32+
if (withPinned) {
33+
service.userTimeline(
34+
user_id = userKey.id,
35+
limit = state.config.pageSize,
36+
pinned = true,
37+
)
38+
} else {
39+
emptyList()
40+
}
41+
service
42+
.userTimeline(
43+
user_id = userKey.id,
44+
limit = state.config.pageSize,
45+
only_media = onlyMedia,
46+
exclude_replies = !withReplies,
47+
).also {
48+
database.pagingTimelineDao().delete(
49+
pagingKey = pagingKey,
50+
accountKey = accountKey,
51+
)
52+
} + pinned
53+
}
3654

3755
LoadType.PREPEND -> {
3856
val firstItem = state.firstItemOrNull()

shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,24 @@ internal class MisskeyDataSource(
10041004
listOfNotNull(
10051005
ProfileTab.Timeline(
10061006
type = ProfileTab.Timeline.Type.Status,
1007-
flow = userTimeline(userKey, scope, pagingSize),
1007+
flow =
1008+
timelinePager(
1009+
pageSize = pagingSize,
1010+
pagingKey = "user_timeline_$userKey",
1011+
accountKey = accountKey,
1012+
database = database,
1013+
filterFlow = localFilterRepository.getFlow(forTimeline = true),
1014+
scope = scope,
1015+
mediator =
1016+
UserTimelineRemoteMediator(
1017+
accountKey = accountKey,
1018+
service = service,
1019+
userKey = userKey,
1020+
database = database,
1021+
pagingKey = "user_timeline_$userKey",
1022+
withPinned = true,
1023+
),
1024+
),
10081025
),
10091026
ProfileTab.Timeline(
10101027
type = ProfileTab.Timeline.Type.StatusWithReplies,

shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/UserTimelineRemoteMediator.kt

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import dev.dimension.flare.data.database.cache.mapper.Misskey
99
import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus
1010
import dev.dimension.flare.data.network.misskey.MisskeyService
1111
import dev.dimension.flare.data.network.misskey.api.model.UsersNotesRequest
12+
import dev.dimension.flare.data.network.misskey.api.model.UsersShowRequest
1213
import dev.dimension.flare.model.MicroBlogKey
14+
import kotlinx.datetime.Instant
1315

1416
@OptIn(ExperimentalPagingApi::class)
1517
internal class UserTimelineRemoteMediator(
@@ -20,7 +22,10 @@ internal class UserTimelineRemoteMediator(
2022
private val pagingKey: String,
2123
private val onlyMedia: Boolean = false,
2224
private val withReplies: Boolean = false,
25+
private val withPinned: Boolean = false,
2326
) : BaseRemoteMediator<Int, DbPagingTimelineWithStatus>() {
27+
var pinnedIds = emptyList<String>()
28+
2429
override suspend fun doLoad(
2530
loadType: LoadType,
2631
state: PagingState<Int, DbPagingTimelineWithStatus>,
@@ -32,24 +37,43 @@ internal class UserTimelineRemoteMediator(
3237
)
3338

3439
LoadType.REFRESH -> {
35-
service.usersNotes(
36-
UsersNotesRequest(
37-
userId = userKey.id,
38-
limit = state.config.pageSize,
39-
withReplies = withReplies,
40-
).let {
41-
if (onlyMedia) {
42-
it.copy(
43-
withFiles = true,
44-
withRenotes = false,
45-
withReplies = false,
46-
withChannelNotes = true,
47-
)
48-
} else {
49-
it
40+
val pinned =
41+
if (withPinned) {
42+
service
43+
.usersShow(
44+
usersShowRequest =
45+
UsersShowRequest(
46+
userId = userKey.id,
47+
),
48+
).let {
49+
pinnedIds = it.pinnedNoteIds
50+
it.pinnedNotes
51+
}
52+
} else {
53+
emptyList()
54+
}
55+
pinned +
56+
service
57+
.usersNotes(
58+
UsersNotesRequest(
59+
userId = userKey.id,
60+
limit = state.config.pageSize,
61+
withReplies = withReplies,
62+
).let {
63+
if (onlyMedia) {
64+
it.copy(
65+
withFiles = true,
66+
withRenotes = false,
67+
withReplies = false,
68+
withChannelNotes = true,
69+
)
70+
} else {
71+
it
72+
}
73+
},
74+
).filter {
75+
it.id !in pinnedIds
5076
}
51-
},
52-
)
5377
}
5478

5579
LoadType.APPEND -> {
@@ -58,25 +82,28 @@ internal class UserTimelineRemoteMediator(
5882
?: return MediatorResult.Success(
5983
endOfPaginationReached = true,
6084
)
61-
service.usersNotes(
62-
UsersNotesRequest(
63-
userId = userKey.id,
64-
limit = state.config.pageSize,
65-
untilId = lastItem.timeline.statusKey.id,
66-
withReplies = withReplies,
67-
).let {
68-
if (onlyMedia) {
69-
it.copy(
70-
withFiles = true,
71-
withRenotes = false,
72-
withReplies = false,
73-
withChannelNotes = true,
74-
)
75-
} else {
76-
it
77-
}
78-
},
79-
)
85+
service
86+
.usersNotes(
87+
UsersNotesRequest(
88+
userId = userKey.id,
89+
limit = state.config.pageSize,
90+
untilId = lastItem.timeline.statusKey.id,
91+
withReplies = withReplies,
92+
).let {
93+
if (onlyMedia) {
94+
it.copy(
95+
withFiles = true,
96+
withRenotes = false,
97+
withReplies = false,
98+
withChannelNotes = true,
99+
)
100+
} else {
101+
it
102+
}
103+
},
104+
).filter {
105+
it.id !in pinnedIds
106+
}
80107
}
81108
} ?: return MediatorResult.Success(
82109
endOfPaginationReached = true,
@@ -90,6 +117,13 @@ internal class UserTimelineRemoteMediator(
90117
accountKey = accountKey,
91118
pagingKey = pagingKey,
92119
data = response,
120+
sortIdProvider = {
121+
if (it.id in pinnedIds) {
122+
Long.MAX_VALUE
123+
} else {
124+
Instant.parse(it.createdAt).toEpochMilliseconds()
125+
}
126+
},
93127
)
94128

95129
return MediatorResult.Success(

0 commit comments

Comments
 (0)