Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Memos 0.23.1 support #236

Merged
merged 2 commits into from
Jan 30, 2025
Merged
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
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -15,8 +15,8 @@ android {
applicationId "me.mudkip.moememos"
minSdk 26
targetSdk 34
versionCode 27
versionName "0.8.3"
versionCode 28
versionName "0.8.4"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
36 changes: 19 additions & 17 deletions app/src/main/java/me/mudkip/moememos/data/api/MemosV1Api.kt
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ package me.mudkip.moememos.data.api
import android.net.Uri
import androidx.annotation.Keep
import com.skydoves.sandwich.ApiResponse
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
@@ -71,14 +73,13 @@ interface MemosV1Api {
@Keep
data class MemosV1User(
val name: String,
val id: Int,
val role: MemosRole,
val username: String,
val email: String,
val nickname: String,
val avatarUrl: String,
val description: String,
val rowStatus: MemosRowStatus,
val state: MemosV1State? = null,
val createTime: Date,
val updateTime: Date
)
@@ -110,13 +111,9 @@ data class MemosV1SetMemoResourcesRequestItem(
data class UpdateMemoRequest(
val content: String? = null,
val visibility: MemosVisibility? = null,
val rowStatus: MemosRowStatus? = null,
val pinned: Boolean? = null
)

@Keep
data class ListMemoTagsResponse(
val tagAmounts: Map<String, Int>
val state: MemosV1State? = null,
val pinned: Boolean? = null,
val updateTime: Date? = null,
)

@Keep
@@ -132,16 +129,11 @@ data class CreateResourceRequest(
val memo: String?
)

@Keep
data class MemosMemoProperty(
val tags: List<String>? = null,
)

@Keep
data class MemosV1Memo(
val name: String,
val uid: String,
val rowStatus: MemosRowStatus,
val state: MemosV1State? = null,
val creator: String,
val createTime: Date,
val updateTime: Date,
@@ -150,7 +142,7 @@ data class MemosV1Memo(
val visibility: MemosVisibility,
val pinned: Boolean,
val resources: List<MemosV1Resource>,
val property: MemosMemoProperty?
val tags: List<String>? = null
)

@Keep
@@ -179,4 +171,14 @@ data class MemosV1UserSetting(
val locale: String,
val appearance: String,
val memoVisibility: MemosVisibility
)
)

@JsonClass(generateAdapter = false)
enum class MemosV1State {
@field:Json(name = "STATE_UNSPECIFIED")
STATE_UNSPECIFIED,
@field:Json(name = "NORMAL")
NORMAL,
@field:Json(name = "ARCHIVED")
ARCHIVED,
}
Original file line number Diff line number Diff line change
@@ -9,13 +9,13 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import me.mudkip.moememos.data.api.CreateResourceRequest
import me.mudkip.moememos.data.api.MemosRowStatus
import me.mudkip.moememos.data.api.MemosV1Api
import me.mudkip.moememos.data.api.MemosV1CreateMemoRequest
import me.mudkip.moememos.data.api.MemosV1Memo
import me.mudkip.moememos.data.api.MemosV1Resource
import me.mudkip.moememos.data.api.MemosV1SetMemoResourcesRequest
import me.mudkip.moememos.data.api.MemosV1SetMemoResourcesRequestItem
import me.mudkip.moememos.data.api.MemosV1State
import me.mudkip.moememos.data.api.MemosView
import me.mudkip.moememos.data.api.MemosVisibility
import me.mudkip.moememos.data.api.UpdateMemoRequest
@@ -27,6 +27,7 @@ import me.mudkip.moememos.data.model.User
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okio.ByteString.Companion.toByteString
import java.util.Date

private const val PAGE_SIZE = 100
private const val ALL_PAGE_SIZE = 1000000
@@ -84,34 +85,34 @@ class MemosV1Repository(
}

override suspend fun listMemos(): ApiResponse<List<Memo>> {
return listMemosByFilter("creator == \"users/${account.info.id}\" && row_status == \"NORMAL\" && order_by_pinned == true")
return listMemosByFilter("creator == \"users/${account.info.id}\" && state == \"NORMAL\" && order_by_pinned == true")
}

override suspend fun listArchivedMemos(): ApiResponse<List<Memo>> {
return listMemosByFilter("creator == \"users/${account.info.id}\" && row_status == \"ARCHIVED\"")
return listMemosByFilter("creator == \"users/${account.info.id}\" && state == \"ARCHIVED\"")
}

override suspend fun listWorkspaceMemos(
pageSize: Int,
pageToken: String?
): ApiResponse<Pair<List<Memo>, String?>> {
val resp = memosApi.listMemos(pageSize, pageToken, "row_status == \"NORMAL\" && visibilities == ['PUBLIC', 'PROTECTED']")
val resp = memosApi.listMemos(pageSize, pageToken, "state == \"NORMAL\" && visibilities == ['PUBLIC', 'PROTECTED']")
if (resp !is ApiResponse.Success) {
return resp.mapSuccess { emptyList<Memo>() to null }
}
val respData = resp.getOrThrow()
val users = respData.memos.map { it.creator.substringAfter("/") }.toSet()
val users = respData.memos.map { it.creator }.toSet()
val userResp = coroutineScope {
users.map { userId ->
async { memosApi.getUser(userId).getOrNull() }
}.awaitAll()
}.filterNotNull()
val userMap = mapOf(*userResp.map { it.id.toString() to it }.toTypedArray())
val userMap = mapOf(*userResp.map { it.name to it }.toTypedArray())

return resp
.mapSuccess { this.memos.map {
convertMemo(it)
.copy(creator = userMap[it.creator.substringAfter("/")]?.let { user -> User(user.name, user.nickname, user.createTime.toInstant()) } )
.copy(creator = userMap[it.creator]?.let { user -> User(user.name, user.nickname, user.createTime.toInstant()) } )
} to this.nextPageToken.ifEmpty { null } }
}

@@ -146,6 +147,7 @@ class MemosV1Repository(
content = content,
visibility = visibility?.let { MemosVisibility.fromMemoVisibility(it) },
pinned = pinned,
updateTime = Date(),
)).mapSuccess { convertMemo(this) }
if (resp !is ApiResponse.Success || resources == null || resources.map { it.identifier }.toSet() == resp.data.resources.map { it.identifier }.toSet()) {
return resp
@@ -163,18 +165,18 @@ class MemosV1Repository(
}

override suspend fun archiveMemo(identifier: String): ApiResponse<Unit> {
return memosApi.updateMemo(getId(identifier), UpdateMemoRequest(rowStatus = MemosRowStatus.ARCHIVED)).mapSuccess { }
return memosApi.updateMemo(getId(identifier), UpdateMemoRequest(state = MemosV1State.ARCHIVED)).mapSuccess { }
}

override suspend fun restoreMemo(identifier: String): ApiResponse<Unit> {
return memosApi.updateMemo(getId(identifier), UpdateMemoRequest(rowStatus = MemosRowStatus.NORMAL)).mapSuccess { }
return memosApi.updateMemo(getId(identifier), UpdateMemoRequest(state = MemosV1State.NORMAL)).mapSuccess { }
}

override suspend fun listTags(): ApiResponse<List<String>> {
val resp = memosApi.listMemos(ALL_PAGE_SIZE, filter = "creator == \"users/${account.info.id}\" && row_status == \"NORMAL\"", view = MemosView.MEMO_VIEW_METADATA_ONLY).getOrNull()
val resp = memosApi.listMemos(ALL_PAGE_SIZE, filter = "creator == \"users/${account.info.id}\" && state == \"NORMAL\"", view = MemosView.MEMO_VIEW_METADATA_ONLY).getOrNull()
val tags = HashSet<String>()
resp?.memos?.forEach { memo ->
tags.addAll(memo.property?.tags ?: emptyList())
tags.addAll(memo.tags ?: emptyList())
}
return ApiResponse.Success(tags.toList())
}
7 changes: 6 additions & 1 deletion app/src/main/java/me/mudkip/moememos/ext/MemoExt.kt
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Public
import androidx.compose.ui.graphics.vector.ImageVector
import me.mudkip.moememos.R
import me.mudkip.moememos.data.model.Memo
import me.mudkip.moememos.data.model.MemoVisibility

val MemoVisibility.icon: ImageVector get() = when (this) {
@@ -18,4 +19,8 @@ val MemoVisibility.titleResource: Int get() = when (this) {
MemoVisibility.PRIVATE -> R.string.memo_visibility_private
MemoVisibility.PROTECTED -> R.string.memo_visibility_protected
MemoVisibility.PUBLIC -> R.string.memo_visibility_public
}
}

fun Memo.getFullLink(host: String): String {
return "${host}/m/${identifier}"
}
27 changes: 25 additions & 2 deletions app/src/main/java/me/mudkip/moememos/ui/component/MemosCard.kt
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.Archive
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Link
import androidx.compose.material.icons.outlined.PinDrop
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material.icons.outlined.Share
@@ -48,6 +49,7 @@ import com.skydoves.sandwich.suspendOnSuccess
import kotlinx.coroutines.launch
import me.mudkip.moememos.R
import me.mudkip.moememos.data.model.Memo
import me.mudkip.moememos.ext.getFullLink
import me.mudkip.moememos.ext.icon
import me.mudkip.moememos.ext.string
import me.mudkip.moememos.ext.titleResource
@@ -60,6 +62,7 @@ import me.mudkip.moememos.viewmodel.LocalUserState
fun MemosCard(
memo: Memo,
onEdit: (Memo) -> Unit,
host: String? = null,
previewMode: Boolean = false
) {
val memosViewModel = LocalMemos.current
@@ -112,7 +115,7 @@ fun MemosCard(
)
}
Spacer(modifier = Modifier.weight(1f))
MemosCardActionButton(memo)
MemosCardActionButton(memo, host)
}

MemoContent(
@@ -141,7 +144,8 @@ fun MemosCard(

@Composable
fun MemosCardActionButton(
memo: Memo
memo: Memo,
host: String? = null,
) {
var menuExpanded by remember { mutableStateOf(false) }
val context = LocalContext.current
@@ -218,6 +222,25 @@ fun MemosCardActionButton(
contentDescription = null
)
})
host?.let {
DropdownMenuItem(
text = { Text(R.string.copy_link.string) },
onClick = {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, memo.getFullLink(host))
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, "Copy Link")
context.startActivity(shareIntent)
},
leadingIcon = {
Icon(
Icons.Outlined.Link,
contentDescription = null
)
})
}
DropdownMenuItem(
text = { Text(R.string.archive.string) },
onClick = {
Original file line number Diff line number Diff line change
@@ -112,6 +112,10 @@ fun LoginPage(
return@launch
}

if (!host.text.contains("//")) {
host = TextFieldValue("https://${host.text}")
}

val resp = when(loginMethod) {
LoginMethod.USERNAME_AND_PASSWORD -> userStateViewModel.loginMemos(host.text.trim(), username.text.trim(), password.text)
LoginMethod.ACCESS_TOKEN -> userStateViewModel.loginMemosWithAccessToken(host.text.trim(), accessToken.text.trim())
Original file line number Diff line number Diff line change
@@ -80,6 +80,7 @@ fun MemosList(
items(filteredMemos, key = { it.identifier }) { memo ->
MemosCard(
memo = memo,
host = viewModel.host,
onEdit = { selectedMemo ->
navController.navigate("${RouteName.EDIT}?memoId=${selectedMemo.identifier}")
},
16 changes: 15 additions & 1 deletion app/src/main/java/me/mudkip/moememos/viewmodel/MemosViewModel.kt
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import me.mudkip.moememos.data.model.DailyUsageStat
import me.mudkip.moememos.data.model.Memo
import me.mudkip.moememos.data.model.MemoVisibility
import me.mudkip.moememos.data.model.Resource
import me.mudkip.moememos.data.service.AccountService
import me.mudkip.moememos.data.service.MemoService
import me.mudkip.moememos.ext.string
import me.mudkip.moememos.ext.suspendOnErrorMessage
@@ -28,7 +29,8 @@ import javax.inject.Inject

@HiltViewModel
class MemosViewModel @Inject constructor(
private val memoService: MemoService
private val memoService: MemoService,
private val accountService: AccountService
) : ViewModel() {

var memos = mutableStateListOf<Memo>()
@@ -39,6 +41,8 @@ class MemosViewModel @Inject constructor(
private set
var matrix by mutableStateOf(DailyUsageStat.initialMatrix)
private set
var host: String? by mutableStateOf(null)
private set

init {
snapshotFlow { memos.toList() }
@@ -51,6 +55,7 @@ class MemosViewModel @Inject constructor(
memos.clear()
memos.addAll(data)
errorMessage = null
loadHost()
}.suspendOnErrorMessage {
errorMessage = it
}
@@ -63,6 +68,15 @@ class MemosViewModel @Inject constructor(
}
}

suspend fun loadHost() = withContext(viewModelScope.coroutineContext) {
accountService.currentAccount.collect { currentAccount ->
val currentHost = currentAccount?.toUserData()?.memosV1?.host ?: let {
currentAccount?.toUserData()?.memosV0?.host
}
host = currentHost
}
}

suspend fun deleteTag(name: String) = withContext(viewModelScope.coroutineContext) {
memoService.repository.deleteTag(name).suspendOnSuccess {
tags.remove(name)
Original file line number Diff line number Diff line change
@@ -194,7 +194,7 @@ class UserStateViewModel @Inject constructor(
private fun getAccount(host: String, accessToken: String, user: MemosV1User): Account = Account.MemosV1(
info = MemosAccount.newBuilder()
.setHost(host)
.setId(user.id.toLong())
.setId(user.name.substringAfterLast('/').toLong())
.setName(user.username)
.setAvatarUrl(user.avatarUrl)
.setAccessToken(accessToken)
1 change: 1 addition & 0 deletions app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@
<string name="invalid_access_token">Недопустимый Access Token</string>
<string name="pin">Закрепить</string>
<string name="share">Поделиться</string>
<string name="copy_link">Скопировать ссылку</string>
<string name="archive">Архивировать</string>
<string name="memo_visibility_public">Видимый для гостей</string>
<string name="memo_visibility_protected">Видимый для локальных пользователей</string>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -58,6 +58,7 @@
<string name="invalid_access_token">Invalid Access Token</string>
<string name="pin">Pin</string>
<string name="share">Share</string>
<string name="copy_link">Copy Link</string>
<string name="archive">Archive</string>
<string name="memo_visibility_public">Visible to guests</string>
<string name="memo_visibility_protected">Visible to local members</string>
13 changes: 13 additions & 0 deletions fastlane/metadata/android/en-US/changelogs/28.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## What's Changed
- Added support for Memos 0.23.1. Due to breaking API changes, Memos 0.22.0–0.23.0 is no longer supported. You can still use Memos 0.21.0 or upgrade to 0.23.1.
- Added the Copy Note Link feature by @Bitnik212.
- Added German translation by @Motschen.
- Added app shortcuts for composing and searching memos by @seewhy163.
- Fixed a crash that occurred when loading memos failed.

## New Contributors
* @Bitnik212 made their first contribution in https://github.com/mudkipme/MoeMemosAndroid/pull/216
* @Motschen made their first contribution in https://github.com/mudkipme/MoeMemosAndroid/pull/232
* @seewhy163 made their first contribution in https://github.com/mudkipme/MoeMemosAndroid/pull/231

**Full Changelog**: https://github.com/mudkipme/MoeMemosAndroid/compare/0.8.3...0.8.4