Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ private val UiStrings.androidStringRes: Int
UiStrings.Reposts -> R.string.tab_settings_filter_repost

UiStrings.Articles -> R.string.profile_tab_articles
UiStrings.WeiboFriendsCircle -> R.string.weibo_friends_circle_title
UiStrings.WeiboPrivateFollowing -> R.string.weibo_private_following_title

UiStrings.Media -> R.string.profile_tab_media

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -927,4 +927,6 @@
<string name="agent_error_details">错误详情</string>
<string name="agent_error_retry">重试</string>
<string name="agent_error_interrupted">执行已中断,请确认已完成的操作后再发送新消息。</string>
<string name="weibo_friends_circle_title">好友圈</string>
<string name="weibo_private_following_title">悄悄关注</string>
</resources>
2 changes: 2 additions & 0 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -798,4 +798,6 @@
<string name="agent_error_retry">重試</string>
<string name="agent_error_interrupted">執行已中斷,請確認已完成的操作後再傳送新訊息。</string>
<string name="plugin_uninstall_confirm_message">從本機刪除 %1$s 及其帳號、憑證和帳號資料?刪除後立即生效。</string>
<string name="weibo_friends_circle_title">好友圈</string>
<string name="weibo_private_following_title">悄悄關注</string>
</resources>
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -992,4 +992,6 @@
<string name="agent_error_details">Error details</string>
<string name="agent_error_retry">Retry</string>
<string name="agent_error_interrupted">This run was interrupted. Check what was completed before sending another message.</string>
<string name="weibo_friends_circle_title">Friends Circle</string>
<string name="weibo_private_following_title">Private Following</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ public extension UiStrings {
localizedPresentationString("vvo_status_reposts", fallback: "Reposts")
case .articles:
localizedPresentationString("profile_tab_articles", fallback: "Articles")
case .weiboFriendsCircle:
localizedPresentationString("weibo_friends_circle_title", fallback: "Friends Circle")
case .weiboPrivateFollowing:
localizedPresentationString("weibo_private_following_title", fallback: "Private Following")
case .media:
localizedPresentationString("appearance_media_group_title", fallback: "Media")
}
Expand Down
44 changes: 44 additions & 0 deletions appleApp/Shared/FlareAppleResource/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -1,6 +1,50 @@
{
"sourceLanguage" : "en",
"strings" : {
"weibo_friends_circle_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Friends Circle"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "好友圈"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "好友圈"
}
}
}
},
"weibo_private_following_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Private Following"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "悄悄关注"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "悄悄關注"
}
}
}
},
"agent_error_interrupted": {
"localizations": {
"en": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,4 +446,6 @@
<string name="share_replies_retry">重试</string>
<string name="share_replies_children">加载这条评论的回复</string>

<string name="weibo_friends_circle_title">好友圈</string>
<string name="weibo_private_following_title">悄悄关注</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -436,4 +436,6 @@
<string name="fanbox_recommended_creators_title">推薦創作者</string>
<string name="ugoira_play">播放動圖</string>
<string name="ugoira_loading">正在載入動圖</string>
<string name="weibo_friends_circle_title">好友圈</string>
<string name="weibo_private_following_title">悄悄關注</string>
</resources>
2 changes: 2 additions & 0 deletions compose-ui/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -587,4 +587,6 @@
<string name="share_replies_retry">Retry</string>
<string name="share_replies_children">Load replies to this comment</string>

<string name="weibo_friends_circle_title">Friends Circle</string>
<string name="weibo_private_following_title">Private Following</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ import dev.dimension.flare.compose.ui.settings_title
import dev.dimension.flare.compose.ui.social_title
import dev.dimension.flare.compose.ui.tab_settings_default
import dev.dimension.flare.compose.ui.tab_settings_filter_repost
import dev.dimension.flare.compose.ui.weibo_friends_circle_title
import dev.dimension.flare.compose.ui.weibo_private_following_title
import dev.dimension.flare.data.model.IconType
import dev.dimension.flare.data.model.ProxyRef
import dev.dimension.flare.data.model.tab.UiTimelineTabItem
Expand Down Expand Up @@ -357,6 +359,8 @@ internal val UiStrings.res: StringResource
UiStrings.Reposts -> Res.string.tab_settings_filter_repost

UiStrings.Articles -> Res.string.profile_tab_articles
UiStrings.WeiboFriendsCircle -> Res.string.weibo_friends_circle_title
UiStrings.WeiboPrivateFollowing -> Res.string.weibo_private_following_title

UiStrings.Media -> Res.string.media_title

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -854,4 +854,6 @@
<string name="agent_error_details">错误详情</string>
<string name="agent_error_retry">重试</string>
<string name="agent_error_interrupted">执行已中断,请确认已完成的操作后再发送新消息。</string>
<string name="weibo_friends_circle_title">好友圈</string>
<string name="weibo_private_following_title">悄悄关注</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -714,4 +714,6 @@
<string name="agent_error_retry">重試</string>
<string name="agent_error_interrupted">執行已中斷,請確認已完成的操作後再傳送新訊息。</string>
<string name="plugin_uninstall_confirm_message">從本機刪除 %1$s 及其帳號、憑證和帳號資料?刪除後立即生效。</string>
<string name="weibo_friends_circle_title">好友圈</string>
<string name="weibo_private_following_title">悄悄關注</string>
</resources>
2 changes: 2 additions & 0 deletions desktopApp/src/main/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -903,4 +903,6 @@
<string name="agent_error_details">Error details</string>
<string name="agent_error_retry">Retry</string>
<string name="agent_error_interrupted">This run was interrupted. Check what was completed before sending another message.</string>
<string name="weibo_friends_circle_title">Friends Circle</string>
<string name="weibo_private_following_title">Private Following</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ import dev.dimension.flare.ui.model.UiStrings
import dev.dimension.flare.ui.model.onSuccess
import dev.dimension.flare.ui.presenter.invoke
import dev.dimension.flare.ui.screen.settings.EditTabPresenter
import dev.dimension.flare.weibo_friends_circle_title
import dev.dimension.flare.weibo_private_following_title
import io.github.composefluent.component.ContentDialog
import io.github.composefluent.component.ContentDialogButton
import io.github.composefluent.component.Text
Expand Down Expand Up @@ -276,6 +278,8 @@ private val UiStrings.desktopStringResource: StringResource
UiStrings.Reposts -> Res.string.tab_settings_filter_repost

UiStrings.Articles -> Res.string.profile_tab_articles
UiStrings.WeiboFriendsCircle -> Res.string.weibo_friends_circle_title
UiStrings.WeiboPrivateFollowing -> Res.string.weibo_private_following_title

UiStrings.Media -> Res.string.profile_tab_media

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public enum class UiStrings {
Popular,
Reposts,
Articles,
WeiboFriendsCircle,
WeiboPrivateFollowing,
}

public fun UiStrings.asText(): UiText = UiText.Localized(this)
Expand Down
1 change: 1 addition & 0 deletions social/vvo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ kotlin {
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.serialization.protobuf)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.paging.testing)
implementation(libs.ktor.client.mock)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.dimension.flare.data.datasource.vvo

import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import dev.dimension.flare.common.CacheData
import dev.dimension.flare.common.FileItem
import dev.dimension.flare.common.MemCacheable
Expand All @@ -16,6 +17,8 @@ import dev.dimension.flare.data.datasource.microblog.NotificationTimelineDataSou
import dev.dimension.flare.data.datasource.microblog.PostEvent
import dev.dimension.flare.data.datasource.microblog.ProfileTab
import dev.dimension.flare.data.datasource.microblog.datasource.NotificationDataSource
import dev.dimension.flare.data.datasource.microblog.datasource.PinnableTimelineTabDataSource
import dev.dimension.flare.data.datasource.microblog.datasource.PinnableTimelineTabSection
import dev.dimension.flare.data.datasource.microblog.datasource.PostDataSource
import dev.dimension.flare.data.datasource.microblog.datasource.RelationDataSource
import dev.dimension.flare.data.datasource.microblog.datasource.TimelineTabConfigurationDataSource
Expand All @@ -30,6 +33,7 @@ import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest
import dev.dimension.flare.data.datasource.microblog.paging.PagingResult
import dev.dimension.flare.data.datasource.microblog.paging.RemoteLoader
import dev.dimension.flare.data.datasource.microblog.paging.ShareRepliesLoader
import dev.dimension.flare.data.datasource.microblog.pagingConfig
import dev.dimension.flare.data.model.IconType
import dev.dimension.flare.data.model.ProxyRef
import dev.dimension.flare.data.model.tab.ShortcutSpec
Expand Down Expand Up @@ -80,6 +84,7 @@ internal class VVODataSource(
UserDataSource,
RelationDataSource,
TimelineTabConfigurationDataSource,
PinnableTimelineTabDataSource,
PostDataSource,
PostEventHandler.Handler {
private val imageCompressor: ImageCompressor by koinInject()
Expand Down Expand Up @@ -107,6 +112,22 @@ internal class VVODataSource(
}
}

override val pinnableTimelineTabs: List<PinnableTimelineTabSection> by lazy {
listOf(
PinnableTimelineTabSection(
title = UiStrings.List,
data = Pager(config = pagingConfig) { VVOGroupPagingSource(service, accountKey) }.flow,
),
)
}

fun groupTimeline(groupId: String): RemoteLoader<UiTimelineV2> =
VVOFeedTimelineRemoteMediator(service, accountKey, VVOFeed.Group(groupId))

fun friendsCircleTimeline(): RemoteLoader<UiTimelineV2> = VVOFeedTimelineRemoteMediator(service, accountKey, VVOFeed.Circle)

fun privateFollowingTimeline(): RemoteLoader<UiTimelineV2> = VVOFeedTimelineRemoteMediator(service, accountKey, VVOFeed.Private)

override val defaultTabs by lazy {
persistentListOf(
CommonTimelineSpecs.home
Expand All @@ -130,6 +151,8 @@ internal class VVODataSource(
),
VvoPlatformSpec.favoriteTimelineSpec.candidate(TimelineSpec.AccountBasedData(accountKey)),
VvoPlatformSpec.likedTimelineSpec.candidate(TimelineSpec.AccountBasedData(accountKey)),
VvoPlatformSpec.friendsCircleTimelineSpec.candidate(TimelineSpec.AccountBasedData(accountKey)),
VvoPlatformSpec.privateFollowingTimelineSpec.candidate(TimelineSpec.AccountBasedData(accountKey)),
)
}

Expand Down Expand Up @@ -159,6 +182,22 @@ internal class VVODataSource(
VvoPlatformSpec.likedTimelineSpec.candidate(TimelineSpec.AccountBasedData(accountKey)),
),
),
ShortcutSpec(
title = UiStrings.WeiboFriendsCircle,
icon = UiIcon.Follow,
target =
ShortcutSpec.Target.Timeline(
VvoPlatformSpec.friendsCircleTimelineSpec.candidate(TimelineSpec.AccountBasedData(accountKey)),
),
),
ShortcutSpec(
title = UiStrings.WeiboPrivateFollowing,
icon = UiIcon.Eye,
target =
ShortcutSpec.Target.Timeline(
VvoPlatformSpec.privateFollowingTimelineSpec.candidate(TimelineSpec.AccountBasedData(accountKey)),
),
),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package dev.dimension.flare.data.datasource.vvo

import dev.dimension.flare.data.datasource.microblog.paging.CacheableRemoteLoader
import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest
import dev.dimension.flare.data.datasource.microblog.paging.PagingResult
import dev.dimension.flare.data.network.vvo.VVOService
import dev.dimension.flare.model.MicroBlogKey
import dev.dimension.flare.ui.model.UiTimelineV2
import dev.dimension.flare.ui.model.mapper.render

internal sealed interface VVOFeed {
data class Group(
val id: String,
) : VVOFeed {
init {
require(id.isNotBlank()) { "Weibo group ID is required" }
}
}

data object Circle : VVOFeed

data object Private : VVOFeed
}

internal class VVOFeedTimelineRemoteMediator(
private val service: VVOService,
private val accountKey: MicroBlogKey,
private val feed: VVOFeed,
) : CacheableRemoteLoader<UiTimelineV2> {
// shortcut: Keep conservative cache boundaries until feed ordering is verified with a logged-in account.
override val pagingKey: String =
when (feed) {
is VVOFeed.Group -> "vvo_group_${accountKey}_${feed.id}"
VVOFeed.Circle -> "vvo_circle_$accountKey"
VVOFeed.Private -> "vvo_private_$accountKey"
}

override suspend fun load(
pageSize: Int,
request: PagingRequest,
): PagingResult<UiTimelineV2> {
if (request is PagingRequest.Prepend) {
return PagingResult(endOfPaginationReached = true)
}
service.ensureGroupLogin(accountKey)
val cursor = (request as? PagingRequest.Append)?.nextKey
val response =
when (feed) {
is VVOFeed.Group -> service.getGroupTimeline(groupId = feed.id, maxId = cursor)
VVOFeed.Circle -> service.getCircleTimeline(maxId = cursor)
VVOFeed.Private -> service.getPrivateTimeline(maxId = cursor)
}.requireGroupData(accountKey)
val statuses =
checkNotNull(response.statuses ?: response.cards?.mapNotNull { it.mblog }) {
"Invalid Weibo timeline"
}
val nextKey =
response.maxId?.takeIf {
statuses.isNotEmpty() && it.isNotBlank() && it != "0" && it != cursor
}
return PagingResult(
data = statuses.map { it.render(accountKey) },
nextKey = nextKey,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package dev.dimension.flare.data.datasource.vvo

import androidx.paging.PagingSource
import androidx.paging.PagingState
import dev.dimension.flare.data.model.tab.TimelineCandidate
import dev.dimension.flare.data.model.tab.TimelineSpec
import dev.dimension.flare.data.network.vvo.VVOService
import dev.dimension.flare.data.network.vvo.model.VVOResponse
import dev.dimension.flare.data.platform.VVO_PLATFORM_ID
import dev.dimension.flare.data.platform.VvoPlatformSpec
import dev.dimension.flare.data.repository.LoginExpiredException
import dev.dimension.flare.model.MicroBlogKey
import dev.dimension.flare.ui.model.UiText
import kotlinx.coroutines.CancellationException

internal class VVOGroupPagingSource(
private val service: VVOService,
private val accountKey: MicroBlogKey,
) : PagingSource<Int, TimelineCandidate<*>>() {
override fun getRefreshKey(state: PagingState<Int, TimelineCandidate<*>>): Int? = null

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TimelineCandidate<*>> =
try {
service.ensureGroupLogin(accountKey)
val groups = service.getGroupList().requireGroupData(accountKey).groups
LoadResult.Page(
data =
groups.distinctBy { it.gid }.map { group ->
check(group.gid.isNotBlank() && group.name.isNotBlank()) { "Invalid Weibo group" }
VvoPlatformSpec.groupTimelineSpec.candidate(
data = TimelineSpec.AccountResourceData(accountKey, group.gid),
title = UiText.Raw(group.name),
)
},
prevKey = null,
nextKey = null,
)
} catch (error: CancellationException) {
throw error
} catch (error: Exception) {
LoadResult.Error(error)
}
}

internal suspend fun VVOService.ensureGroupLogin(accountKey: MicroBlogKey) {
if (config().data?.login != true) {
throw LoginExpiredException(accountKey = accountKey, platformId = VVO_PLATFORM_ID)
}
}

internal fun <T : Any> VVOResponse<T>.requireGroupData(accountKey: MicroBlogKey): T {
if (ok == -100L) {
throw LoginExpiredException(accountKey = accountKey, platformId = VVO_PLATFORM_ID)
}
check(ok == 1L) { "Weibo group request failed" }
return checkNotNull(data) { "Invalid Weibo group response" }
}
Loading
Loading