feat: upgrade to lemmy 0.19 (#59)

* chore: add auth headers to all network calls

* chore: add support to scaled sort

* chore: support for cursor based pagination

* chore: relax required fields

* feat: add block instance
This commit is contained in:
Diego Beraldin 2023-10-21 08:16:50 +02:00 committed by GitHub
parent 905916e56f
commit 808f521e20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 379 additions and 83 deletions

View File

@ -0,0 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class BlockSiteForm(
@SerialName("instance_id") val instanceId: InstanceId,
@SerialName("block") val block: Boolean,
)

View File

@ -15,5 +15,5 @@ data class CommunityAggregates(
@SerialName("users_active_week") val usersActiveWeek: Int,
@SerialName("users_active_month") val usersActiveMonth: Int,
@SerialName("users_active_half_year") val usersActiveHalfYear: Int,
@SerialName("hot_rank") val hotRank: Int,
@SerialName("hot_rank") val hotRank: Int? = null,
)

View File

@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable
@Serializable
data class GetPostsResponse(
@SerialName("posts") val posts: List<PostView>,
@SerialName("next_page") val nextPage: String? = null,
)

View File

@ -18,7 +18,7 @@ data class Person(
@SerialName("banner") val banner: String? = null,
@SerialName("deleted") val deleted: Boolean,
@SerialName("matrix_user_id") val matrixUserId: String? = null,
@SerialName("admin") val admin: Boolean,
@SerialName("admin") val admin: Boolean? = null,
@SerialName("bot_account") val botAccount: Boolean,
@SerialName("ban_expires") val banExpires: String? = null,
@SerialName("instance_id") val instanceId: InstanceId,

View File

@ -12,10 +12,10 @@ data class PostAggregates(
@SerialName("upvotes") val upvotes: Int,
@SerialName("downvotes") val downvotes: Int,
@SerialName("published") val published: String,
@SerialName("newest_comment_time_necro") val newestCommentTimeNecro: String,
@SerialName("newest_comment_time") val newestCommentTime: String,
@SerialName("featured_community") val featuredCommunity: Boolean,
@SerialName("featured_local") val featuredLocal: Boolean,
@SerialName("hot_rank") val hotRank: Int,
@SerialName("hot_rank_active") val hotRankActive: Int,
@SerialName("newest_comment_time_necro") val newestCommentTimeNecro: String? = null,
@SerialName("newest_comment_time") val newestCommentTime: String? = null,
@SerialName("featured_community") val featuredCommunity: Boolean? = null,
@SerialName("featured_local") val featuredLocal: Boolean? = null,
@SerialName("hot_rank") val hotRank: Int? = null,
@SerialName("hot_rank_active") val hotRankActive: Int? = null,
)

View File

@ -59,4 +59,7 @@ enum class SortType {
@SerialName("Controversial")
Controversial,
@SerialName("Scaled")
Scaled,
}

View File

@ -13,6 +13,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SaveCommentForm
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Header
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.PUT
@ -21,6 +22,7 @@ import de.jensklingenberg.ktorfit.http.Query
interface CommentService {
@GET("comment/list")
suspend fun getAll(
@Header("Authorization") authHeader: String? = null,
@Query("auth") auth: String? = null,
@Query("limit") limit: Int? = null,
@Query("sort") sort: CommentSortType? = null,
@ -36,27 +38,43 @@ interface CommentService {
@GET("comment")
suspend fun getBy(
@Header("Authorization") authHeader: String? = null,
@Query("id") id: Int,
@Query("auth") auth: String? = null,
): Response<GetCommentResponse>
@PUT("comment/save")
@Headers("Content-Type: application/json")
suspend fun save(@Body form: SaveCommentForm): Response<CommentResponse>
suspend fun save(
@Header("Authorization") authHeader: String? = null,
@Body form: SaveCommentForm,
): Response<CommentResponse>
@POST("comment/like")
@Headers("Content-Type: application/json")
suspend fun like(@Body form: CreateCommentLikeForm): Response<CommentResponse>
suspend fun like(
@Header("Authorization") authHeader: String? = null,
@Body form: CreateCommentLikeForm,
): Response<CommentResponse>
@POST("comment")
@Headers("Content-Type: application/json")
suspend fun create(@Body form: CreateCommentForm): Response<CommentResponse>
suspend fun create(
@Header("Authorization") authHeader: String? = null,
@Body form: CreateCommentForm,
): Response<CommentResponse>
@PUT("comment")
@Headers("Content-Type: application/json")
suspend fun edit(@Body form: EditCommentForm): Response<CommentResponse>
suspend fun edit(
@Header("Authorization") authHeader: String? = null,
@Body form: EditCommentForm,
): Response<CommentResponse>
@POST("comment/delete")
@Headers("Content-Type: application/json")
suspend fun delete(@Body form: DeleteCommentForm): Response<CommentResponse>
suspend fun delete(
@Header("Authorization") authHeader: String? = null,
@Body form: DeleteCommentForm,
): Response<CommentResponse>
}

View File

@ -10,6 +10,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Header
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Query
@ -18,6 +19,7 @@ interface CommunityService {
@GET("community")
suspend fun get(
@Header("Authorization") authHeader: String? = null,
@Query("auth") auth: String? = null,
@Query("id") id: Int? = null,
@Query("name") name: String? = null,
@ -25,6 +27,7 @@ interface CommunityService {
@GET("community/list")
suspend fun getAll(
@Header("Authorization") authHeader: String? = null,
@Query("auth") auth: String? = null,
@Query("page") page: Int? = null,
@Query("limit") limit: Int? = null,
@ -34,9 +37,15 @@ interface CommunityService {
@POST("community/follow")
@Headers("Content-Type: application/json")
suspend fun follow(@Body form: FollowCommunityForm): Response<CommunityResponse>
suspend fun follow(
@Header("Authorization") authHeader: String? = null,
@Body form: FollowCommunityForm,
): Response<CommunityResponse>
@POST("community/block")
@Headers("Content-Type: application/json")
suspend fun block(@Body form: BlockCommunityForm): Response<BlockCommunityResponse>
suspend fun block(
@Header("Authorization") authHeader: String? = null,
@Body form: BlockCommunityForm,
): Response<BlockCommunityResponse>
}

View File

@ -16,7 +16,6 @@ import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Header
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.Multipart
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.PUT
import de.jensklingenberg.ktorfit.http.Query
@ -27,10 +26,12 @@ interface PostService {
@GET("post/list")
suspend fun getAll(
@Header("Authorization") authHeader: String? = null,
@Query("auth") auth: String? = null,
@Query("limit") limit: Int? = null,
@Query("sort") sort: SortType? = null,
@Query("page") page: Int? = null,
@Query("page_cursor") pageCursor: String? = null,
@Query("type_") type: ListingType? = null,
@Query("community_id") communityId: Int? = null,
@Query("community_name") communityName: String? = null,
@ -39,6 +40,7 @@ interface PostService {
@GET("post")
suspend fun get(
@Header("Authorization") authHeader: String? = null,
@Query("auth") auth: String? = null,
@Query("id") id: Int? = null,
@Query("comment_id") commentId: Int? = null,
@ -46,28 +48,44 @@ interface PostService {
@PUT("post/save")
@Headers("Content-Type: application/json")
suspend fun save(@Body form: SavePostForm): Response<PostResponse>
suspend fun save(
@Header("Authorization") authHeader: String? = null,
@Body form: SavePostForm,
): Response<PostResponse>
@POST("post/like")
@Headers("Content-Type: application/json")
suspend fun like(@Body form: CreatePostLikeForm): Response<PostResponse>
suspend fun like(
@Header("Authorization") authHeader: String? = null,
@Body form: CreatePostLikeForm,
): Response<PostResponse>
@POST("post")
@Headers("Content-Type: application/json")
suspend fun create(@Body form: CreatePostForm): Response<PostResponse>
suspend fun create(
@Header("Authorization") authHeader: String? = null,
@Body form: CreatePostForm,
): Response<PostResponse>
@PUT("post")
@Headers("Content-Type: application/json")
suspend fun edit(@Body form: EditPostForm): Response<PostResponse>
suspend fun edit(
@Header("Authorization") authHeader: String? = null,
@Body form: EditPostForm,
): Response<PostResponse>
@POST("post/delete")
@Headers("Content-Type: application/json")
suspend fun delete(@Body form: DeletePostForm): Response<PostResponse>
suspend fun delete(
@Header("Authorization") authHeader: String? = null,
@Body form: DeletePostForm,
): Response<PostResponse>
@POST
suspend fun uploadImage(
@Url url: String,
@Header("Cookie") token: String,
@Header("Authorization") authHeader: String? = null,
@Body content: MultiPartFormDataContent,
): Response<PictrsImages>
}

View File

@ -7,6 +7,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PrivateMessagesResp
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Header
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Query
@ -14,6 +15,7 @@ import de.jensklingenberg.ktorfit.http.Query
interface PrivateMessageService {
@GET("private_message/list")
suspend fun getPrivateMessages(
@Header("Authorization") authHeader: String? = null,
@Query("auth") auth: String? = null,
@Query("page") page: Int? = null,
@Query("limit") limit: Int? = null,
@ -22,9 +24,15 @@ interface PrivateMessageService {
@POST("private_message")
@Headers("Content-Type: application/json")
suspend fun createPrivateMessage(@Body form: CreatePrivateMessageForm): Response<PrivateMessageResponse>
suspend fun createPrivateMessage(
@Header("Authorization") authHeader: String? = null,
@Body form: CreatePrivateMessageForm,
): Response<PrivateMessageResponse>
@POST("private_message/mark_as_read")
@Headers("Content-Type: application/json")
suspend fun markPrivateMessageAsRead(@Body form: MarkPrivateMessageAsReadForm): Response<PrivateMessageResponse>
suspend fun markPrivateMessageAsRead(
@Header("Authorization") authHeader: String? = null,
@Body form: MarkPrivateMessageAsReadForm,
): Response<PrivateMessageResponse>
}

View File

@ -6,11 +6,13 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SearchType
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Header
import de.jensklingenberg.ktorfit.http.Query
interface SearchService {
@GET("search")
suspend fun search(
@Header("Authorization") authHeader: String? = null,
@Query("q") q: String,
@Query("community_id") communityId: Int? = null,
@Query("community_name") communityName: String? = null,

View File

@ -1,19 +1,33 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.service
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.BlockSiteForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetSiteMetadataResponse
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetSiteResponse
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Header
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Query
interface SiteService {
@GET("site")
suspend fun get(
@Header("Authorization") authHeader: String? = null,
@Query("auth") auth: String? = null,
): Response<GetSiteResponse>
@POST("site/block")
@Headers("Content-Type: application/json")
suspend fun block(
@Header("Authorization") authHeader: String? = null,
@Body form: BlockSiteForm,
): Response<GetSiteResponse>
@GET("post/site_metadata")
suspend fun getSiteMetadata(
@Header("Authorization") authHeader: String? = null,
@Query("url")
url: String,
): Response<GetSiteMetadataResponse>

View File

@ -14,6 +14,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PersonMentionRespon
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Header
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Query
@ -22,6 +23,7 @@ interface UserService {
@GET("user")
suspend fun getDetails(
@Header("Authorization") authHeader: String? = null,
@Query("auth") auth: String? = null,
@Query("community_id") communityId: Int? = null,
@Query("person_id") personId: Int? = null,
@ -34,6 +36,7 @@ interface UserService {
@GET("user/mention")
suspend fun getMentions(
@Header("Authorization") authHeader: String? = null,
@Query("auth") auth: String? = null,
@Query("page") page: Int? = null,
@Query("limit") limit: Int? = null,
@ -43,6 +46,7 @@ interface UserService {
@GET("user/replies")
suspend fun getReplies(
@Header("Authorization") authHeader: String? = null,
@Query("auth") auth: String? = null,
@Query("page") page: Int? = null,
@Query("limit") limit: Int? = null,
@ -52,17 +56,29 @@ interface UserService {
@POST("user/mark_all_as_read")
@Headers("Content-Type: application/json")
suspend fun markAllAsRead(@Body form: MarkAllAsReadForm): Response<GetRepliesResponse>
suspend fun markAllAsRead(
@Header("Authorization") authHeader: String? = null,
@Body form: MarkAllAsReadForm,
): Response<GetRepliesResponse>
@POST("user/mention/mark_as_read")
@Headers("Content-Type: application/json")
suspend fun markPersonMentionAsRead(@Body form: MarkPersonMentionAsReadForm): Response<PersonMentionResponse>
suspend fun markPersonMentionAsRead(
@Header("Authorization") authHeader: String? = null,
@Body form: MarkPersonMentionAsReadForm,
): Response<PersonMentionResponse>
@POST("comment/mark_as_read")
@Headers("Content-Type: application/json")
suspend fun markCommentReplyAsRead(@Body form: MarkCommentReplyAsReadForm): Response<CommentReplyResponse>
suspend fun markCommentReplyAsRead(
@Header("Authorization") authHeader: String? = null,
@Body form: MarkCommentReplyAsReadForm,
): Response<CommentReplyResponse>
@POST("user/block")
@Headers("Content-Type: application/json")
suspend fun blockPerson(@Body form: BlockPersonForm): Response<BlockPersonResponse>
suspend fun block(
@Header("Authorization") authHeader: String? = null,
@Body form: BlockPersonForm,
): Response<BlockPersonResponse>
}

View File

@ -24,6 +24,7 @@ interface CommunityDetailMviModel :
data class DeletePost(val id: Int) : Intent
data class SharePost(val index: Int) : Intent
data object Block : Intent
data object BlockInstance : Intent
}
data class UiState(

View File

@ -285,15 +285,15 @@ class CommunityDetailScreen(
stringResource(MR.strings.community_detail_info),
stringResource(MR.strings.community_detail_instance_info),
stringResource(MR.strings.community_detail_block),
stringResource(MR.strings.community_detail_block_instance),
),
onOpenImage = { url ->
navigator?.push(ZoomableImageScreen(url))
},
onOptionSelected = { optionIdx ->
when (optionIdx) {
2 -> {
model.reduce(CommunityDetailMviModel.Intent.Block)
}
3 -> model.reduce(CommunityDetailMviModel.Intent.BlockInstance)
2 -> model.reduce(CommunityDetailMviModel.Intent.Block)
1 -> {
navigator?.push(

View File

@ -37,6 +37,7 @@ class CommunityDetailViewModel(
CommunityDetailMviModel {
private var currentPage: Int = 1
private var pageCursor: String? = null
override fun onStarted() {
mvi.onStarted()
@ -107,11 +108,13 @@ class CommunityDetailViewModel(
)
CommunityDetailMviModel.Intent.Block -> blockCommunity()
CommunityDetailMviModel.Intent.BlockInstance -> blockInstance()
}
}
private fun refresh() {
currentPage = 1
pageCursor = null
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
val auth = identityRepository.authToken.value
mvi.scope?.launch(Dispatchers.IO) {
@ -153,11 +156,12 @@ class CommunityDetailViewModel(
val refreshing = currentState.refreshing
val sort = currentState.sortType
val communityId = currentState.community.id
val itemList = if (otherInstance.isNotEmpty()) {
val (itemList, nextPage) = if (otherInstance.isNotEmpty()) {
postRepository.getAll(
instance = otherInstance,
communityId = communityId,
page = currentPage,
pageCursor = pageCursor,
sort = sort,
)
} else {
@ -165,12 +169,28 @@ class CommunityDetailViewModel(
auth = auth,
communityId = communityId,
page = currentPage,
pageCursor = pageCursor,
sort = sort,
)
}
}?.let {
if (refreshing) {
it
} else {
// prevents accidental duplication
val posts = it.first
it.copy(
first = posts.filter { p1 ->
currentState.posts.none { p2 -> p2.id == p1.id }
},
)
}
} ?: (null to null)
if (!itemList.isNullOrEmpty()) {
currentPage++
}
if (nextPage != null) {
pageCursor = nextPage
}
mvi.updateState {
val newItems = if (refreshing) {
itemList.orEmpty()
@ -383,4 +403,20 @@ class CommunityDetailViewModel(
}
}
}
private fun blockInstance() {
mvi.updateState { it.copy(asyncInProgress = true) }
mvi.scope?.launch(Dispatchers.IO) {
try {
val instanceId = community.instanceId
val auth = identityRepository.authToken.value
siteRepository.block(instanceId, true, auth).getOrThrow()
mvi.emitEffect(CommunityDetailMviModel.Effect.BlockSuccess)
} catch (e: Throwable) {
mvi.emitEffect(CommunityDetailMviModel.Effect.BlockError(e.message))
} finally {
mvi.updateState { it.copy(asyncInProgress = false) }
}
}
}
}

View File

@ -83,6 +83,7 @@ val commonUiModule = module {
userRepository = get(),
postRepository = get(),
commentRepository = get(),
siteRepository = get(),
themeRepository = get(),
settingsRepository = get(),
shareHelper = get(),

View File

@ -35,6 +35,7 @@ class InstanceInfoViewModel(
val metadata = siteRepository.getMetadata(url)
if (metadata != null) {
metadata.title
mvi.updateState {
it.copy(
title = metadata.title,

View File

@ -46,6 +46,7 @@ class SortBottomSheet(
SortType.MostComments,
SortType.Old,
SortType.Controversial,
SortType.Scaled,
SortType.Top.Generic,
),
private val expandTop: Boolean = false,
@ -68,7 +69,6 @@ class SortBottomSheet(
SortBottomSheetMain(
values = values,
expandTop = expandTop,
mainKey = key,
)
)
}
@ -78,7 +78,6 @@ class SortBottomSheet(
internal class SortBottomSheetMain(
private val values: List<SortType>,
private val expandTop: Boolean = false,
private val mainKey: String,
) : Screen {
@Composable
override fun Content() {

View File

@ -22,11 +22,11 @@ interface UserDetailMviModel :
data class SavePost(val index: Int, val feedback: Boolean = false) : Intent
data class UpVoteComment(val index: Int, val feedback: Boolean = false) : Intent
data class DownVoteComment(val index: Int, val feedback: Boolean = false) : Intent
data class SaveComment(val index: Int, val feedback: Boolean = false) : Intent
data object HapticIndication : Intent
data class SharePost(val index: Int) : Intent
data object Block : Intent
data object BlockInstance : Intent
}
data class UiState(

View File

@ -263,15 +263,17 @@ class UserDetailScreen(
UserHeader(
user = uiState.user,
autoLoadImages = uiState.autoLoadImages,
options = listOf(stringResource(MR.strings.community_detail_block)),
options = listOf(
stringResource(MR.strings.community_detail_block),
stringResource(MR.strings.community_detail_block_instance),
),
onOpenImage = { url ->
navigator?.push(ZoomableImageScreen(url))
},
onOptionSelected = { optionIdx ->
when (optionIdx) {
else -> {
model.reduce(UserDetailMviModel.Intent.Block)
}
1 -> model.reduce(UserDetailMviModel.Intent.BlockInstance)
else -> model.reduce(UserDetailMviModel.Intent.Block)
}
},
)

View File

@ -17,6 +17,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.shareUrl
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
@ -32,6 +33,7 @@ class UserDetailViewModel(
private val userRepository: UserRepository,
private val postRepository: PostRepository,
private val commentRepository: CommentRepository,
private val siteRepository: SiteRepository,
private val themeRepository: ThemeRepository,
private val shareHelper: ShareHelper,
private val hapticFeedback: HapticFeedback,
@ -129,6 +131,7 @@ class UserDetailViewModel(
)
UserDetailMviModel.Intent.Block -> blockUser()
UserDetailMviModel.Intent.BlockInstance -> blockInstance()
}
}
@ -557,4 +560,20 @@ class UserDetailViewModel(
}
}
}
private fun blockInstance() {
mvi.updateState { it.copy(asyncInProgress = true) }
mvi.scope?.launch(Dispatchers.IO) {
try {
val instanceId = user.instanceId
val auth = identityRepository.authToken.value
siteRepository.block(instanceId, true, auth).getOrThrow()
mvi.emitEffect(UserDetailMviModel.Effect.BlockSuccess)
} catch (e: Throwable) {
mvi.emitEffect(UserDetailMviModel.Effect.BlockError(e.message))
} finally {
mvi.updateState { it.copy(asyncInProgress = false) }
}
}
}
}

View File

@ -4,6 +4,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class CommunityModel(
val id: Int = 0,
val instanceId: Int = 0,
val name: String = "",
val description: String = "",
val title: String = "",

View File

@ -6,6 +6,7 @@ import androidx.compose.material.icons.filled.Forum
import androidx.compose.material.icons.filled.LocalFireDepartment
import androidx.compose.material.icons.filled.MarkUnreadChatAlt
import androidx.compose.material.icons.filled.MilitaryTech
import androidx.compose.material.icons.filled.MonitorWeight
import androidx.compose.material.icons.filled.RocketLaunch
import androidx.compose.material.icons.filled.Thunderstorm
import androidx.compose.material.icons.filled.TrendingUp
@ -33,6 +34,7 @@ sealed interface SortType {
}
data object Controversial : SortType
data object Scaled : SortType
}
fun SortType.toInt() = when (this) {
@ -50,6 +52,7 @@ fun SortType.toInt() = when (this) {
SortType.Top.Year -> 12
SortType.Old -> 13
SortType.Controversial -> 14
SortType.Scaled -> 15
else -> 0
}
@ -68,6 +71,7 @@ fun Int.toSortType() = when (this) {
12 -> SortType.Top.Year
13 -> SortType.Old
14 -> SortType.Controversial
15 -> SortType.Scaled
else -> SortType.Active
}
@ -79,6 +83,7 @@ fun SortType.toIcon(): ImageVector = when (this) {
SortType.NewComments -> Icons.Default.MarkUnreadChatAlt
SortType.Old -> Icons.Default.ElderlyWoman
SortType.Controversial -> Icons.Default.Thunderstorm
SortType.Scaled -> Icons.Default.MonitorWeight
else -> Icons.Default.MilitaryTech
}
@ -98,5 +103,6 @@ fun SortType.toReadableName(): String = when (this) {
SortType.Top.Year -> stringResource(MR.strings.home_sort_type_top_year)
SortType.Old -> stringResource(MR.strings.home_sort_type_old)
SortType.Controversial -> stringResource(MR.strings.home_sort_type_controversial)
SortType.Scaled -> stringResource(MR.strings.home_sort_type_scaled)
else -> stringResource(MR.strings.home_sort_type_top)
}

View File

@ -4,6 +4,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class UserModel(
val id: Int = 0,
val instanceId: Int = 0,
val name: String = "",
val displayName: String = "",
val avatar: String? = null,

View File

@ -10,6 +10,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toAuthHeader
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toCommentDto
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toDto
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toModel
@ -35,6 +36,7 @@ class CommentRepository(
): List<CommentModel>? = runCatching {
val response = if (instance.isNullOrEmpty()) {
services.comment.getAll(
authHeader = auth.toAuthHeader(),
auth = auth,
postId = postId,
page = page,
@ -61,10 +63,14 @@ class CommentRepository(
suspend fun getBy(id: Int, auth: String?, instance: String? = null): CommentModel? =
runCatching {
if (instance.isNullOrEmpty()) {
services.comment.getBy(id, auth).body()
services.comment.getBy(
authHeader = auth.toAuthHeader(),
id = id,
auth = auth,
).body()
} else {
customServices.changeInstance(instance)
customServices.comment.getBy(id).body()
customServices.comment.getBy(id = id).body()
}?.commentView?.toModel()
}.getOrNull()
@ -79,6 +85,7 @@ class CommentRepository(
): List<CommentModel>? = runCatching {
val response = if (instance.isNullOrEmpty()) {
services.comment.getAll(
authHeader = auth.toAuthHeader(),
auth = auth,
parentId = parentId,
limit = limit,
@ -142,7 +149,7 @@ class CommentRepository(
score = if (voted) 1 else 0,
auth = auth,
)
services.comment.like(data)
services.comment.like(authHeader = auth.toAuthHeader(), form = data)
}
fun asDownVoted(comment: CommentModel, downVoted: Boolean) = comment.copy(
@ -187,7 +194,7 @@ class CommentRepository(
score = if (downVoted) -1 else 0,
auth = auth,
)
services.comment.like(data)
services.comment.like(authHeader = auth.toAuthHeader(), form = data)
}
fun asSaved(comment: CommentModel, saved: Boolean) = comment.copy(saved = saved)
@ -198,7 +205,7 @@ class CommentRepository(
save = saved,
auth = auth,
)
services.comment.save(data)
services.comment.save(authHeader = auth.toAuthHeader(), form = data)
}
suspend fun create(
@ -213,7 +220,7 @@ class CommentRepository(
parentId = parentId,
auth = auth,
)
services.comment.create(data)
services.comment.create(authHeader = auth.toAuthHeader(), form = data)
}
suspend fun edit(
@ -226,7 +233,7 @@ class CommentRepository(
commentId = commentId,
auth = auth,
)
services.comment.edit(data)
services.comment.edit(authHeader = auth.toAuthHeader(), form = data)
}
suspend fun delete(
@ -238,6 +245,6 @@ class CommentRepository(
deleted = true,
auth = auth
)
services.comment.delete(data)
services.comment.delete(authHeader = auth.toAuthHeader(), form = data)
}
}

View File

@ -9,6 +9,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResultType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toAuthHeader
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toDto
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toModel
@ -33,6 +34,7 @@ class CommunityRepository(
): List<Any>? = runCatching {
if (instance.isNullOrEmpty()) {
val response = services.search.search(
authHeader = auth.toAuthHeader(),
q = query,
auth = auth,
page = page,
@ -61,7 +63,10 @@ class CommunityRepository(
suspend fun getSubscribed(
auth: String? = null,
): List<CommunityModel> = runCatching {
val response = services.site.get(auth).body()
val response = services.site.get(
authHeader = auth.toAuthHeader(),
auth = auth,
).body()
response?.myUser?.follows?.map { it.community.toModel() }.orEmpty()
}.getOrElse { emptyList() }
@ -73,16 +78,14 @@ class CommunityRepository(
): CommunityModel? = runCatching {
val response = if (instance.isNullOrEmpty()) {
services.community.get(
authHeader = auth.toAuthHeader(),
auth = auth,
id = id,
name = name,
).body()
} else {
customServices.changeInstance(instance)
customServices.community.get(
auth = auth,
name = name,
).body()
customServices.community.get(name = name).body()
}
response?.communityView?.toModel()
}.getOrNull()
@ -96,7 +99,10 @@ class CommunityRepository(
communityId = id,
follow = true,
)
val response = services.community.follow(data)
val response = services.community.follow(
authHeader = auth.toAuthHeader(),
form = data
)
response.body()?.communityView?.toModel()
}.getOrNull()
@ -109,7 +115,10 @@ class CommunityRepository(
communityId = id,
follow = false,
)
val response = services.community.follow(data)
val response = services.community.follow(
authHeader = auth.toAuthHeader(),
form = data
)
response.body()?.communityView?.toModel()
}.getOrNull()
@ -119,7 +128,10 @@ class CommunityRepository(
block = blocked,
auth = auth.orEmpty(),
)
services.community.block(data)
services.community.block(
authHeader = auth.toAuthHeader(),
form = data,
)
}
}

View File

@ -9,6 +9,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvide
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toAuthHeader
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toDto
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toModel
import io.ktor.client.request.forms.MultiPartFormDataContent
@ -28,17 +29,20 @@ class PostRepository(
suspend fun getAll(
auth: String? = null,
page: Int,
pageCursor: String? = null,
limit: Int = DEFAULT_PAGE_SIZE,
type: ListingType = ListingType.Local,
sort: SortType = SortType.Active,
communityId: Int? = null,
instance: String? = null,
): List<PostModel>? = runCatching {
): Pair<List<PostModel>, String?>? = runCatching {
val response = if (instance.isNullOrEmpty()) {
services.post.getAll(
authHeader = auth.toAuthHeader(),
auth = auth,
communityId = communityId,
page = page,
pageCursor = pageCursor,
limit = limit,
type = type.toDto(),
sort = sort.toDto(),
@ -48,13 +52,15 @@ class PostRepository(
customServices.post.getAll(
communityId = communityId,
page = page,
pageCursor = pageCursor,
limit = limit,
type = type.toDto(),
sort = sort.toDto(),
)
}
val dto = response.body()?.posts ?: emptyList()
dto.map { it.toModel() }
val body = response.body()
val posts = body?.posts?.map { it.toModel() } ?: emptyList()
posts to body?.nextPage
}.getOrNull()
suspend fun get(
@ -63,7 +69,11 @@ class PostRepository(
instance: String? = null,
): PostModel? = runCatching {
val response = if (instance.isNullOrEmpty()) {
services.post.get(auth, id).body()
services.post.get(
authHeader = auth.toAuthHeader(),
auth = auth,
id = id,
).body()
} else {
customServices.changeInstance(instance)
customServices.post.get(id = id).body()
@ -98,7 +108,10 @@ class PostRepository(
score = if (voted) 1 else 0,
auth = auth,
)
services.post.like(data)
services.post.like(
authHeader = auth.toAuthHeader(),
form = data,
)
}
fun asDownVoted(post: PostModel, downVoted: Boolean) = post.copy(
@ -125,7 +138,10 @@ class PostRepository(
score = if (downVoted) -1 else 0,
auth = auth,
)
services.post.like(data)
services.post.like(
authHeader = auth.toAuthHeader(),
form = data,
)
}
fun asSaved(post: PostModel, saved: Boolean): PostModel = post.copy(saved = saved)
@ -136,7 +152,10 @@ class PostRepository(
save = saved,
auth = auth,
)
services.post.save(data)
services.post.save(
authHeader = auth.toAuthHeader(),
form = data,
)
}
suspend fun create(
@ -155,7 +174,10 @@ class PostRepository(
nsfw = nsfw,
auth = auth,
)
services.post.create(data)
services.post.create(
authHeader = auth.toAuthHeader(),
form = data,
)
}
suspend fun edit(
@ -174,7 +196,10 @@ class PostRepository(
nsfw = nsfw,
auth = auth,
)
services.post.edit(data)
services.post.edit(
authHeader = auth.toAuthHeader(),
form = data,
)
}
suspend fun delete(id: Int, auth: String) {
@ -183,7 +208,10 @@ class PostRepository(
deleted = true,
auth = auth
)
services.post.delete(data)
services.post.delete(
authHeader = auth.toAuthHeader(),
form = data,
)
}
suspend fun uploadImage(auth: String, bytes: ByteArray): String? = try {
@ -194,7 +222,12 @@ class PostRepository(
append(HttpHeaders.ContentDisposition, "filename=image.jpeg")
})
})
val images = services.post.uploadImage(url, "jwt=$auth", multipart).body()
val images = services.post.uploadImage(
url = url,
token = "jwt=$auth",
authHeader = auth.toAuthHeader(),
content = multipart,
).body()
"$url/${images?.files?.firstOrNull()?.file}"
} catch (e: Exception) {
e.printStackTrace()

View File

@ -4,6 +4,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePrivateMessag
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkPrivateMessageAsReadForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PrivateMessageModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toAuthHeader
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toModel
class PrivateMessageRepository(
@ -16,6 +17,7 @@ class PrivateMessageRepository(
unreadOnly: Boolean = true,
): List<PrivateMessageModel>? = runCatching {
val response = serviceProvider.privateMessages.getPrivateMessages(
authHeader = auth.toAuthHeader(),
auth = auth,
limit = limit,
page = page,
@ -35,7 +37,10 @@ class PrivateMessageRepository(
auth = auth.orEmpty(),
recipientId = recipiendId,
)
serviceProvider.privateMessages.createPrivateMessage(data)
serviceProvider.privateMessages.createPrivateMessage(
authHeader = auth.toAuthHeader(),
form = data,
)
}
suspend fun markAsRead(
@ -48,6 +53,9 @@ class PrivateMessageRepository(
auth = auth.orEmpty(),
read = read,
)
serviceProvider.privateMessages.markPrivateMessageAsRead(data)
serviceProvider.privateMessages.markPrivateMessageAsRead(
authHeader = auth.toAuthHeader(),
form = data,
)
}
}

View File

@ -1,16 +1,21 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.BlockSiteForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SiteMetadata
import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.MetadataModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toAuthHeader
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toModel
class SiteRepository(
private val serviceProvider: ServiceProvider,
) {
suspend fun getCurrentUser(auth: String): UserModel? = runCatching {
val response = serviceProvider.site.get(auth = auth)
val response = serviceProvider.site.get(
auth = auth,
authHeader = auth.toAuthHeader(),
)
response.body()?.myUser?.let {
val user = it.localUserView.person
val counts = it.localUserView.counts
@ -18,8 +23,21 @@ class SiteRepository(
}
}.getOrNull()
suspend fun block(id: Int, blocked: Boolean, auth: String? = null): Result<Unit> = runCatching {
val data = BlockSiteForm(
instanceId = id,
block = blocked,
)
serviceProvider.site.block(
authHeader = auth.toAuthHeader(),
form = data,
)
}
suspend fun getMetadata(url: String): MetadataModel? = runCatching {
val response = serviceProvider.site.getSiteMetadata(url = url)
val response = serviceProvider.site.getSiteMetadata(
url = url,
)
response.body()?.metadata?.toModel()
}.getOrNull()
}

View File

@ -10,6 +10,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PersonMentionM
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toAuthHeader
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toCommentDto
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toHost
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toModel
@ -25,6 +26,7 @@ class UserRepository(
username: String? = null,
): UserModel? = runCatching {
val response = serviceProvider.user.getDetails(
authHeader = auth.toAuthHeader(),
auth = auth,
personId = id,
username = username,
@ -48,6 +50,7 @@ class UserRepository(
): UserModel? = runCatching {
customServiceProvider.changeInstance(instance)
val response = customServiceProvider.user.getDetails(
authHeader = auth.toAuthHeader(),
auth = auth,
username = username,
)
@ -71,6 +74,7 @@ class UserRepository(
sort: SortType = SortType.Active,
): List<PostModel>? = runCatching {
val response = serviceProvider.user.getDetails(
authHeader = auth.toAuthHeader(),
auth = auth,
personId = id,
page = page,
@ -89,6 +93,7 @@ class UserRepository(
sort: SortType = SortType.Active,
): List<PostModel>? = runCatching {
val response = serviceProvider.user.getDetails(
authHeader = auth.toAuthHeader(),
auth = auth,
personId = id,
page = page,
@ -108,6 +113,7 @@ class UserRepository(
sort: SortType = SortType.Active,
): List<CommentModel>? = runCatching {
val response = serviceProvider.user.getDetails(
authHeader = auth.toAuthHeader(),
auth = auth,
personId = id,
page = page,
@ -126,6 +132,7 @@ class UserRepository(
sort: SortType = SortType.Active,
): List<CommentModel>? = runCatching {
val response = serviceProvider.user.getDetails(
authHeader = auth.toAuthHeader(),
auth = auth,
personId = id,
page = page,
@ -145,6 +152,7 @@ class UserRepository(
unreadOnly: Boolean = true,
): List<PersonMentionModel>? = runCatching {
val response = serviceProvider.user.getMentions(
authHeader = auth.toAuthHeader(),
auth = auth,
limit = limit,
sort = sort.toCommentDto(),
@ -163,6 +171,7 @@ class UserRepository(
unreadOnly: Boolean = true,
): List<PersonMentionModel>? = runCatching {
val response = serviceProvider.user.getReplies(
authHeader = auth.toAuthHeader(),
auth = auth,
limit = limit,
sort = sort.toCommentDto(),
@ -177,7 +186,10 @@ class UserRepository(
auth: String? = null,
) {
val data = MarkAllAsReadForm(auth.orEmpty())
serviceProvider.user.markAllAsRead(data)
serviceProvider.user.markAllAsRead(
authHeader = auth.toAuthHeader(),
form = data,
)
}
suspend fun setMentionRead(read: Boolean, mentionId: Int, auth: String? = null) = runCatching {
@ -186,7 +198,10 @@ class UserRepository(
read = read,
auth = auth.orEmpty(),
)
serviceProvider.user.markPersonMentionAsRead(data)
serviceProvider.user.markPersonMentionAsRead(
authHeader = auth.toAuthHeader(),
form = data,
)
}
suspend fun setReplyRead(read: Boolean, replyId: Int, auth: String? = null) = runCatching {
@ -195,7 +210,10 @@ class UserRepository(
read = read,
auth = auth.orEmpty(),
)
serviceProvider.user.markCommentReplyAsRead(data)
serviceProvider.user.markCommentReplyAsRead(
authHeader = auth.toAuthHeader(),
form = data,
)
}
suspend fun block(id: Int, blocked: Boolean, auth: String? = null): Result<Unit> = runCatching {
@ -204,6 +222,9 @@ class UserRepository(
block = blocked,
auth = auth.orEmpty(),
)
serviceProvider.user.blockPerson(data)
serviceProvider.user.block(
authHeader = auth.toAuthHeader(),
form = data,
)
}
}

View File

@ -80,6 +80,7 @@ internal fun SearchResultType.toDto(): SearchType = when (this) {
internal fun Person.toModel() = UserModel(
id = id,
instanceId = instanceId,
name = name,
displayName = displayName.orEmpty(),
avatar = avatar,
@ -131,6 +132,7 @@ internal fun CommentView.toModel() = CommentModel(
internal fun Community.toModel() = CommunityModel(
id = id,
instanceId = instanceId,
name = name,
title = title,
description = description.orEmpty(),
@ -222,6 +224,8 @@ internal fun PrivateMessageView.toModel() = PrivateMessageModel(
read = privateMessage.read,
)
internal fun String?.toAuthHeader() = this?.let { "Bearer $it" }
internal fun String.toHost(): String = this.replace("https://", "").let {
val index = it.indexOf("/")
if (index < 0) {

View File

@ -40,6 +40,7 @@ class PostListViewModel(
MviModel<PostListMviModel.Intent, PostListMviModel.UiState, PostListMviModel.Effect> by mvi {
private var currentPage: Int = 1
private var pageCursor: String? = null
private var firstLoad = true
init {
@ -150,6 +151,7 @@ class PostListViewModel(
private fun refresh() {
currentPage = 1
pageCursor = null
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
loadNextPage()
}
@ -168,9 +170,10 @@ class PostListViewModel(
val sort = currentState.sortType ?: SortType.Active
val refreshing = currentState.refreshing
val includeNsfw = settingsRepository.currentSettings.value.includeNsfw
val itemList = postRepository.getAll(
val (itemList, nextPage) = postRepository.getAll(
auth = auth,
page = currentPage,
pageCursor = pageCursor,
type = type,
sort = sort,
)?.let {
@ -178,14 +181,20 @@ class PostListViewModel(
it
} else {
// prevents accidental duplication
it.filter { p1 ->
currentState.posts.none { p2 -> p2.id == p1.id }
}
val posts = it.first
it.copy(
first = posts.filter { p1 ->
currentState.posts.none { p2 -> p2.id == p1.id }
},
)
}
}
} ?: (null to null)
if (!itemList.isNullOrEmpty()) {
currentPage++
}
if (nextPage != null) {
pageCursor = nextPage
}
mvi.updateState {
val newPosts = if (refreshing) {
itemList.orEmpty()
@ -375,6 +384,7 @@ class PostListViewModel(
private fun handleLogout() {
currentPage = 1
pageCursor = null
mvi.updateState {
it.copy(
posts = emptyList(),

View File

@ -10,11 +10,13 @@ internal class CommunityPaginator(
private val postRepository: PostRepository,
) {
private var currentPage: Int = 1
private var pageCursor: String? = null
var canFetchMore: Boolean = true
private set
fun reset() {
currentPage = 1
pageCursor = null
canFetchMore = true
}
@ -23,20 +25,29 @@ internal class CommunityPaginator(
sort: SortType,
currentIds: List<Int>,
): List<PostModel> {
val result = postRepository.getAll(
val (result, nextPage) = postRepository.getAll(
auth = auth,
page = currentPage,
pageCursor = pageCursor,
limit = PostRepository.DEFAULT_PAGE_SIZE,
type = ListingType.All,
sort = sort,
communityId = communityId,
)?.filter {
)?.let {
// prevents accidental duplication
it.id !in currentIds
}
val posts = it.first
it.copy(
first = posts.filter { p1 ->
p1.id !in currentIds
},
)
} ?: (null to null)
if (!result.isNullOrEmpty()) {
currentPage++
}
if (nextPage != null) {
pageCursor = nextPage
}
canFetchMore = result?.isEmpty() != true
return result.orEmpty()
}

View File

@ -55,6 +55,7 @@
<string name="home_sort_type_top_year_short">year</string>
<string name="home_sort_type_old">Old</string>
<string name="home_sort_type_controversial">Controversial</string>
<string name="home_sort_type_scaled">Scaled</string>
<string name="home_instance_via">via %1$s</string>
<string name="post_hour_short">h</string>
<string name="post_minute_short">m</string>
@ -152,6 +153,7 @@
<string name="community_detail_info">Community info</string>
<string name="community_detail_instance_info">Instance details</string>
<string name="community_detail_block">Block</string>
<string name="community_detail_block_instance">Block instance</string>
<string name="instance_detail_title">Instance: %1$s</string>
<string name="instance_detail_communities">Communities</string>
<string name="action_back_to_top">Back to top</string>

View File

@ -50,6 +50,7 @@
<string name="home_sort_type_top_week_short">semana</string>
<string name="home_sort_type_top_year_short">año</string>
<string name="home_sort_type_controversial">Contravertidos</string>
<string name="home_sort_type_scaled">Proporcional</string>
<string name="home_sort_type_old">Antiguos</string>
<string name="home_instance_via">a través de %1$s</string>
<string name="post_hour_short">h</string>
@ -150,6 +151,7 @@
<string name="community_detail_info">Info comunidad</string>
<string name="community_detail_instance_info">Detalles instancia</string>
<string name="community_detail_block">Bloquear</string>
<string name="community_detail_block_instance">Bloquear instancia</string>
<string name="instance_detail_title">Instancia: %1$s</string>
<string name="instance_detail_communities">Comunidades</string>
<string name="action_back_to_top">Volver arriba</string>

View File

@ -51,6 +51,7 @@
<string name="home_sort_type_top_year_short">anno</string>
<string name="home_sort_type_old">Vecchi</string>
<string name="home_sort_type_controversial">Controversi</string>
<string name="home_sort_type_scaled">Proporzionale</string>
<string name="home_instance_via">via %1$s</string>
<string name="post_hour_short">h</string>
<string name="post_minute_short">m</string>
@ -84,6 +85,7 @@
<string name="login_field_instance_name">Nome istanza</string>
<string name="login_field_user_name">Nome utente (o email)</string>
<string name="community_detail_block">Blocca</string>
<string name="community_detail_block_instance">Blocca istanza</string>
<string name="login_field_password">Password</string>
<string name="login_field_token">TOTP 2FA token</string>
<string name="login_field_label_optional">(opzionale)</string>