編集履歴カラムの追加。投稿のヘッダに編集済表記を追加。

This commit is contained in:
tateisu 2022-03-16 13:04:04 +09:00
parent 8bba82b930
commit 0bbb3728bb
18 changed files with 233 additions and 97 deletions

View File

@ -3,14 +3,12 @@ package jp.juggler.subwaytooter.action
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.actmain.addColumn
import jp.juggler.subwaytooter.actmain.reloadAccountSetting import jp.juggler.subwaytooter.actmain.reloadAccountSetting
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount import jp.juggler.subwaytooter.actmain.showColumnMatchAccount
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.column.Column import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.column.findStatus
import jp.juggler.subwaytooter.column.onScheduleDeleted
import jp.juggler.subwaytooter.column.onStatusRemoved
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgConfirm import jp.juggler.subwaytooter.dialog.DlgConfirm
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
@ -642,3 +640,12 @@ fun ActMain.scheduledPostEdit(
} }
} }
} }
// アカウントを選んでタイムラインカラムを追加
fun ActMain.openStatusHistory(
pos: Int,
accessInfo: SavedAccount,
status: TootStatus,
) {
addColumn(pos, accessInfo, ColumnType.STATUS_HISTORY, status.id, status.json)
}

View File

@ -2,6 +2,7 @@ package jp.juggler.subwaytooter.api
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.util.LogCategory import jp.juggler.util.LogCategory
import jp.juggler.util.cast
import java.util.* import java.util.*
class DuplicateMap { class DuplicateMap {
@ -67,4 +68,30 @@ class DuplicateMap {
} }
return listNew return listNew
} }
private fun isDuplicateByCreatedAt(o: TimelineItem): Boolean {
if (o is TootStatus) {
val createdAt = EntityId(o.time_created_at.toString())
if (createdAt.notDefaultOrConfirming) {
if (idSet.contains(createdAt)) return true
idSet.add(createdAt)
}
}
//編集履歴以外のカラムではここを通らない
return false
}
fun filterDuplicateByCreatedAt(src: Collection<TimelineItem>?): ArrayList<TimelineItem> {
val listNew = ArrayList<TimelineItem>()
if (src != null) {
for (o in src) {
if (isDuplicateByCreatedAt(o)) {
log.d("filterDuplicateByCreatedAt: filtered. ${o.cast<TootStatus>()?.time_created_at}")
continue
}
listNew.add(o)
}
}
return listNew
}
} }

View File

@ -182,6 +182,9 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
var isPromoted = false var isPromoted = false
var isFeatured = false var isFeatured = false
// Mastodon 3.5.0
var time_edited_at = 0L
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
// 以下はentityから取得したデータではなく、アプリ内部で使う // 以下はentityから取得したデータではなく、アプリ内部で使う
@ -602,6 +605,8 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
this.favourites_count = src.long("favourites_count") this.favourites_count = src.long("favourites_count")
this.replies_count = src.long("replies_count") this.replies_count = src.long("replies_count")
this.time_edited_at = parseTime(src.string("edited_at"))
this.reactionSet = TootReactionSet.parseFedibird( this.reactionSet = TootReactionSet.parseFedibird(
src.jsonArray("emoji_reactions") src.jsonArray("emoji_reactions")
?: src.jsonObject("pleroma")?.jsonArray("emoji_reactions") ?: src.jsonObject("pleroma")?.jsonArray("emoji_reactions")
@ -1267,7 +1272,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
// 時刻を解釈してエポック秒(ミリ単位)を返す // 時刻を解釈してエポック秒(ミリ単位)を返す
// 解釈に失敗すると0Lを返す // 解釈に失敗すると0Lを返す
fun parseTime(strTime: String?): Long { fun parseTime(strTime: String?): Long {
if (strTime?.isNotBlank() != true) return 0L if (strTime.isNullOrBlank()) return 0L
// last_status_at などでは YYYY-MM-DD になることがある // last_status_at などでは YYYY-MM-DD になることがある
reDate.find(strTime)?.groupValues?.let { gv -> reDate.find(strTime)?.groupValues?.let { gv ->
@ -1552,5 +1557,23 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
return null return null
} }
private val supplyEditHistoryKeys = arrayOf(
"id",
"uri",
"url",
"visibility",
)
// 編集履歴のデータはTootStatusとしては不足があるので、srcを元に補う
fun supplyEditHistory(array: JsonArray?, src: JsonObject?) {
src ?: return
array?.objectList()?.forEach {
for (key in supplyEditHistoryKeys) {
if (it.containsKey(key)) continue
it[key] = src[key]
}
}
}
} }
} }

View File

@ -160,6 +160,7 @@ class Column(
internal var profileTab = ProfileTab.Status internal var profileTab = ProfileTab.Status
internal var statusId: EntityId? = null internal var statusId: EntityId? = null
internal var originalStatus: JsonObject? = null
// プロフカラムではアカウントのID。リストカラムではリストのID // プロフカラムではアカウントのID。リストカラムではリストのID
internal var profileId: EntityId? = null internal var profileId: EntityId? = null

View File

@ -242,6 +242,7 @@ fun Column.onMuteUpdated() {
} }
fun Column.replaceStatus(statusId: EntityId, statusJson: JsonObject) { fun Column.replaceStatus(statusId: EntityId, statusJson: JsonObject) {
if (type == ColumnType.STATUS_HISTORY) return
fun createStatus() = fun createStatus() =
TootParser(context, accessInfo).status(statusJson) TootParser(context, accessInfo).status(statusJson)

View File

@ -56,6 +56,7 @@ object ColumnEncoder {
private const val KEY_PROFILE_ID = "profile_id" private const val KEY_PROFILE_ID = "profile_id"
private const val KEY_PROFILE_TAB = "tab" private const val KEY_PROFILE_TAB = "tab"
private const val KEY_STATUS_ID = "status_id" private const val KEY_STATUS_ID = "status_id"
private const val KEY_ORIGINAL_STATUS = "original_status"
private const val KEY_HASHTAG = "hashtag" private const val KEY_HASHTAG = "hashtag"
private const val KEY_HASHTAG_ANY = "hashtag_any" private const val KEY_HASHTAG_ANY = "hashtag_any"
@ -170,6 +171,11 @@ object ColumnEncoder {
-> ->
dst[KEY_STATUS_ID] = statusId.toString() dst[KEY_STATUS_ID] = statusId.toString()
ColumnType.STATUS_HISTORY -> {
dst[KEY_STATUS_ID] = statusId.toString()
dst[KEY_ORIGINAL_STATUS] = originalStatus
}
ColumnType.FEDERATED_AROUND -> { ColumnType.FEDERATED_AROUND -> {
dst[KEY_STATUS_ID] = statusId.toString() dst[KEY_STATUS_ID] = statusId.toString()
dst[KEY_REMOTE_ONLY] = remoteOnly dst[KEY_REMOTE_ONLY] = remoteOnly
@ -299,6 +305,11 @@ object ColumnEncoder {
ColumnType.ACCOUNT_AROUND, ColumnType.ACCOUNT_AROUND,
-> statusId = EntityId.mayNull(src.string(KEY_STATUS_ID)) -> statusId = EntityId.mayNull(src.string(KEY_STATUS_ID))
ColumnType.STATUS_HISTORY -> {
statusId = EntityId.mayNull(src.string(KEY_STATUS_ID))
originalStatus = src.jsonObject(KEY_ORIGINAL_STATUS)
}
ColumnType.FEDERATED_AROUND, ColumnType.FEDERATED_AROUND,
-> { -> {
statusId = EntityId.mayNull(src.string(KEY_STATUS_ID)) statusId = EntityId.mayNull(src.string(KEY_STATUS_ID))

View File

@ -36,6 +36,7 @@ fun Column.canReloadWhenRefreshTop(): Boolean = when (type) {
ColumnType.TREND_TAG, ColumnType.TREND_TAG,
ColumnType.FOLLOW_SUGGESTION, ColumnType.FOLLOW_SUGGESTION,
ColumnType.PROFILE_DIRECTORY, ColumnType.PROFILE_DIRECTORY,
ColumnType.STATUS_HISTORY,
-> true -> true
ColumnType.LIST_MEMBER, ColumnType.LIST_MEMBER,
@ -56,7 +57,7 @@ fun Column.canRefreshTopBySwipe(): Boolean =
else -> true else -> true
} }
// カラム操作的にリフレッシュを許容するかどうか // カラム操作的に下端リフレッシュを許容するかどうか
fun Column.canRefreshBottomBySwipe(): Boolean = when (type) { fun Column.canRefreshBottomBySwipe(): Boolean = when (type) {
ColumnType.LIST_LIST, ColumnType.LIST_LIST,
ColumnType.CONVERSATION, ColumnType.CONVERSATION,
@ -65,6 +66,7 @@ fun Column.canRefreshBottomBySwipe(): Boolean = when (type) {
ColumnType.SEARCH, ColumnType.SEARCH,
ColumnType.TREND_TAG, ColumnType.TREND_TAG,
ColumnType.FOLLOW_SUGGESTION, ColumnType.FOLLOW_SUGGESTION,
ColumnType.STATUS_HISTORY,
-> false -> false
ColumnType.FOLLOW_REQUESTS -> isMisskey ColumnType.FOLLOW_REQUESTS -> isMisskey

View File

@ -44,20 +44,26 @@ fun Column.getFilterContext() = when (type) {
ColumnType.PROFILE -> TootFilter.CONTEXT_PROFILE ColumnType.PROFILE -> TootFilter.CONTEXT_PROFILE
ColumnType.STATUS_HISTORY -> TootFilter.CONTEXT_NONE
else -> TootFilter.CONTEXT_PUBLIC else -> TootFilter.CONTEXT_PUBLIC
// ColumnType.MISSKEY_HYBRID や ColumnType.MISSKEY_ANTENNA_TL はHOMEでもPUBLICでもある… // ColumnType.MISSKEY_HYBRID や ColumnType.MISSKEY_ANTENNA_TL はHOMEでもPUBLICでもある…
// Misskeyだし関係ないが、NONEにするとアプリ内で完結するフィルタも働かなくなる // Misskeyだし関係ないが、NONEにするとアプリ内で完結するフィルタも働かなくなる
} }
// カラム設定に正規表現フィルタを含めるなら真 // カラム設定に正規表現フィルタを含めるなら真
fun Column.canStatusFilter(): Boolean { fun Column.canStatusFilter() =
if (getFilterContext() != TootFilter.CONTEXT_NONE) return true when (type) {
ColumnType.SEARCH_MSP,
return when (type) { ColumnType.SEARCH_TS,
ColumnType.SEARCH_MSP, ColumnType.SEARCH_TS, ColumnType.SEARCH_NOTESTOCK -> true ColumnType.SEARCH_NOTESTOCK,
else -> false ColumnType.STATUS_HISTORY,
-> true
else -> when {
getFilterContext() == TootFilter.CONTEXT_NONE -> false
else -> true
}
} }
}
// カラム設定に「すべての画像を隠す」ボタンを含めるなら真 // カラム設定に「すべての画像を隠す」ボタンを含めるなら真
fun Column.canNSFWDefault(): Boolean = canStatusFilter() fun Column.canNSFWDefault(): Boolean = canStatusFilter()

View File

@ -4,6 +4,7 @@ import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.EntityId import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.JsonObject
import jp.juggler.util.LogCategory import jp.juggler.util.LogCategory
private val log = LogCategory("ColumnSpec") private val log = LogCategory("ColumnSpec")
@ -34,6 +35,12 @@ object ColumnSpec {
else -> error("getParamString [$idx] bad type. $o") else -> error("getParamString [$idx] bad type. $o")
} }
private fun getParamJsonObject(params: Array<out Any>, idx: Int): JsonObject =
when (val o = params[idx]) {
is JsonObject -> o
else -> error("getParamJsonObject [$idx] bad type. $o")
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private inline fun <reified T> getParamAtNullable(params: Array<out Any>, idx: Int): T? { private inline fun <reified T> getParamAtNullable(params: Array<out Any>, idx: Int): T? {
if (idx >= params.size) return null if (idx >= params.size) return null
@ -53,6 +60,11 @@ object ColumnSpec {
-> ->
statusId = getParamEntityId(params, 0) statusId = getParamEntityId(params, 0)
ColumnType.STATUS_HISTORY -> {
statusId = getParamEntityId(params, 0)
originalStatus = getParamJsonObject(params, 1)
}
ColumnType.PROFILE, ColumnType.LIST_TL, ColumnType.LIST_MEMBER, ColumnType.PROFILE, ColumnType.LIST_TL, ColumnType.LIST_MEMBER,
ColumnType.MISSKEY_ANTENNA_TL, ColumnType.MISSKEY_ANTENNA_TL,
-> ->
@ -121,19 +133,20 @@ object ColumnSpec {
ColumnType.LOCAL_AROUND, ColumnType.LOCAL_AROUND,
ColumnType.FEDERATED_AROUND, ColumnType.FEDERATED_AROUND,
ColumnType.ACCOUNT_AROUND, ColumnType.ACCOUNT_AROUND,
ColumnType.STATUS_HISTORY,
-> ->
column.statusId == getParamEntityId(params, 0) column.statusId == getParamEntityId(params, 0)
ColumnType.HASHTAG -> { ColumnType.HASHTAG -> {
(getParamString(params, 0) == column.hashtag) && (getParamString(params, 0) == column.hashtag) &&
((getParamAtNullable<String>(params, 1) ?: "") == column.hashtagAny) && ((getParamAtNullable<String>(params, 1) ?: "") == column.hashtagAny) &&
((getParamAtNullable<String>(params, 2) ?: "") == column.hashtagAll) && ((getParamAtNullable<String>(params, 2) ?: "") == column.hashtagAll) &&
((getParamAtNullable<String>(params, 3) ?: "") == column.hashtagNone) ((getParamAtNullable<String>(params, 3) ?: "") == column.hashtagNone)
} }
ColumnType.HASHTAG_FROM_ACCT -> { ColumnType.HASHTAG_FROM_ACCT -> {
(getParamString(params, 0) == column.hashtag) && (getParamString(params, 0) == column.hashtag) &&
((getParamAtNullable<String>(params, 1) ?: "") == column.hashtagAcct) ((getParamAtNullable<String>(params, 1) ?: "") == column.hashtagAcct)
} }
ColumnType.NOTIFICATION_FROM_ACCT -> { ColumnType.NOTIFICATION_FROM_ACCT -> {
@ -142,7 +155,7 @@ object ColumnSpec {
ColumnType.SEARCH -> ColumnType.SEARCH ->
getParamString(params, 0) == column.searchQuery && getParamString(params, 0) == column.searchQuery &&
getParamAtNullable<Boolean>(params, 1) == column.searchResolve getParamAtNullable<Boolean>(params, 1) == column.searchResolve
ColumnType.SEARCH_MSP, ColumnType.SEARCH_MSP,
ColumnType.SEARCH_TS, ColumnType.SEARCH_TS,
@ -155,8 +168,8 @@ object ColumnSpec {
ColumnType.PROFILE_DIRECTORY -> ColumnType.PROFILE_DIRECTORY ->
getParamString(params, 0) == column.instanceUri && getParamString(params, 0) == column.instanceUri &&
getParamAtNullable<String>(params, 1) == column.searchQuery && getParamAtNullable<String>(params, 1) == column.searchQuery &&
getParamAtNullable<Boolean>(params, 2) == column.searchResolve getParamAtNullable<Boolean>(params, 2) == column.searchResolve
ColumnType.DOMAIN_TIMELINE -> ColumnType.DOMAIN_TIMELINE ->
getParamString(params, 0) == column.instanceUri getParamString(params, 0) == column.instanceUri

View File

@ -104,6 +104,9 @@ class ColumnTask_Loading(
// 検索カラムはIDによる重複排除が不可能 // 検索カラムはIDによる重複排除が不可能
ColumnType.SEARCH -> listTmp ColumnType.SEARCH -> listTmp
// 編集履歴は投稿日時で重複排除する
ColumnType.STATUS_HISTORY -> column.duplicateMap.filterDuplicateByCreatedAt(listTmp)
// 他のカラムは重複排除してから追加 // 他のカラムは重複排除してから追加
else -> column.duplicateMap.filterDuplicate(listTmp) else -> column.duplicateMap.filterDuplicate(listTmp)
} }
@ -847,6 +850,19 @@ class ColumnTask_Loading(
return result return result
} }
suspend fun getEditHistory(client: TootApiClient): TootApiResult? {
// ページングなし
val result = client.request("/api/v1/statuses/${column.statusId}/history")
// TootStatusとしては不足している情報があるのを補う
TootStatus.supplyEditHistory(result?.jsonArray, column.originalStatus)
val src = parser.statusList(result?.jsonArray).reversed()
listTmp = addAll(listTmp, src)
column.saveRange(bBottom = true, bTop = true, result = result, list = src)
return result
}
suspend fun getListList( suspend fun getListList(
client: TootApiClient, client: TootApiClient,
pathBase: String, pathBase: String,

View File

@ -92,7 +92,12 @@ class ColumnTask_Refresh(
return return
} }
val listNew = column.duplicateMap.filterDuplicate(listTmp) val listNew = when (column.type) {
// 編集履歴は投稿日時で重複排除する
ColumnType.STATUS_HISTORY -> column.duplicateMap.filterDuplicateByCreatedAt(listTmp)
else -> column.duplicateMap.filterDuplicate(listTmp)
}
if (listNew.isEmpty()) { if (listNew.isEmpty()) {
column.fireShowContent( column.fireShowContent(
reason = "refresh list_new is empty", reason = "refresh list_new is empty",
@ -1153,4 +1158,17 @@ class ColumnTask_Refresh(
column.saveRange(bBottom, !bBottom, result, src) column.saveRange(bBottom, !bBottom, result, src)
return result return result
} }
suspend fun getEditHistory(client: TootApiClient): TootApiResult? {
// ページングなし
val result = client.request("/api/v1/statuses/${column.statusId}/history")
// TootStatusとしては不足している情報があるのを補う
TootStatus.supplyEditHistory(result?.jsonArray, column.originalStatus)
val src = parser.statusList(result?.jsonArray).reversed()
listTmp = addAll(listTmp, src)
column.saveRange(bBottom, !bBottom, result, src)
return result
}
} }

View File

@ -663,7 +663,11 @@ enum class ColumnType(
}, },
loading = { client -> getPublicTlAroundTime(client, column.makePublicFederateUrl()) }, loading = { client -> getPublicTlAroundTime(client, column.makePublicFederateUrl()) },
refresh = { client -> getStatusList(client, column.makePublicFederateUrl(), useMinId = true) }, refresh = { client ->
getStatusList(client,
column.makePublicFederateUrl(),
useMinId = true)
},
canStreamingMastodon = streamingTypeNo, canStreamingMastodon = streamingTypeNo,
canStreamingMisskey = streamingTypeNo, canStreamingMisskey = streamingTypeNo,
@ -1965,6 +1969,24 @@ enum class ColumnType(
// Misskey10 にアンテナはない // Misskey10 にアンテナはない
), ),
STATUS_HISTORY(
43,
iconId = { R.drawable.ic_history },
name1 = { it.getString(R.string.edit_history) },
bAllowPseudo = true,
bAllowMisskey = false,
loading = { client ->
getEditHistory(client)
},
refresh = { client ->
getEditHistory(client)
},
canStreamingMastodon = streamingTypeNo,
canStreamingMisskey = streamingTypeNo,
),
; ;
init { init {
@ -1985,7 +2007,7 @@ enum class ColumnType(
min = min(min, id) min = min(min, id)
max = max(max, id) max = max(max, id)
} }
log.d("dump: ColumnType range=$min..$max") log.i("dump: ColumnType range=$min..$max")
} }
fun parse(id: Int) = Column.typeMap[id] ?: HOME fun parse(id: Int) = Column.typeMap[id] ?: HOME
@ -2015,7 +2037,11 @@ fun Column.streamKeyHashtagTl() =
"hashtag" "hashtag"
.appendIf(":local", instanceLocal) .appendIf(":local", instanceLocal)
private fun unmatchMastodonStream(stream: JsonArray, name: String, expectArg: String? = null): Boolean { private fun unmatchMastodonStream(
stream: JsonArray,
name: String,
expectArg: String? = null,
): Boolean {
val key = stream.string(0) val key = stream.string(0)

View File

@ -88,71 +88,11 @@ internal class DlgContextMenu(
dialog.setCancelable(true) dialog.setCancelable(true)
dialog.setCanceledOnTouchOutside(true) dialog.setCanceledOnTouchOutside(true)
arrayOf( views.root.scan { v ->
views.btnAccountWebPage, when (v) {
views.btnAroundAccountTL, is Button -> v.setOnClickListener(this)
views.btnAroundFTL, }
views.btnAroundLTL, }
views.btnAvatarImage,
views.btnBlock,
views.btnBookmarkAnotherAccount,
views.btnBoostAnotherAccount,
views.btnBoostedBy,
views.btnBoostWithVisibility,
views.btnConversationAnotherAccount,
views.btnConversationMute,
views.btnCopyAccountId,
views.btnDelete,
views.btnDeleteSuggestion,
views.btnDomainBlock,
views.btnDomainTimeline,
views.btnEndorse,
views.btnFavouriteAnotherAccount,
views.btnFavouritedBy,
views.btnFollow,
views.btnFollowFromAnotherAccount,
views.btnFollowRequestNG,
views.btnFollowRequestOK,
views.btnHideBoost,
views.btnHideFavourite,
views.btnInstanceInformation,
views.btnListMemberAddRemove,
views.btnMute,
views.btnMuteApp,
views.btnNotificationDelete,
views.btnNotificationFrom,
views.btnOpenAccountInAdminWebUi,
views.btnOpenInstanceInAdminWebUi,
views.btnOpenProfileFromAnotherAccount,
views.btnOpenTimeline,
views.btnProfile,
views.btnProfileDirectory,
views.btnProfilePin,
views.btnProfileUnpin,
views.btnQuoteAnotherAccount,
views.btnQuoteTootBT,
views.btnReactionAnotherAccount,
views.btnRedraft,
views.btnStatusEdit,
views.btnReplyAnotherAccount,
views.btnReportStatus,
views.btnReportUser,
views.btnSendMessage,
views.btnSendMessageFromAnotherAccount,
views.btnShowBoost,
views.btnShowFavourite,
views.btnStatusNotification,
views.btnStatusWebPage,
views.btnText,
views.btnQuoteUrlStatus,
views.btnTranslate,
views.btnQuoteUrlAccount,
views.btnShareUrlStatus,
views.btnShareUrlAccount,
views.btnQuoteName
).forEach { it.setOnClickListener(this) }
arrayOf( arrayOf(
views.btnBlock, views.btnBlock,
@ -219,6 +159,11 @@ internal class DlgContextMenu(
} }
} }
} }
views.btnStatusHistory.vg(status.time_edited_at > 0L && columnType != ColumnType.STATUS_HISTORY)
?.text = activity.getString(R.string.edit_history) + "\n" +
TootStatus.formatTime(activity, status.time_edited_at, bAllowRelative = false)
views.llLinks.vg(views.llLinks.childCount > 1) views.llLinks.vg(views.llLinks.childCount > 1)
views.btnGroupStatusByMe.vg(statusByMe) views.btnGroupStatusByMe.vg(statusByMe)
@ -372,8 +317,6 @@ internal class DlgContextMenu(
} }
} }
views.btnAccountText.setOnClickListener(this)
if (accessInfo.isPseudo) { if (accessInfo.isPseudo) {
views.btnProfile.visibility = View.GONE views.btnProfile.visibility = View.GONE
views.btnSendMessage.visibility = View.GONE views.btnSendMessage.visibility = View.GONE
@ -399,10 +342,6 @@ internal class DlgContextMenu(
views.btnSendMessageFromAnotherAccount.visibility = View.GONE views.btnSendMessageFromAnotherAccount.visibility = View.GONE
} }
views.btnNickname.setOnClickListener(this)
views.btnCancel.setOnClickListener(this)
views.btnAccountQrCode.setOnClickListener(this)
if (accessInfo.isPseudo || if (accessInfo.isPseudo ||
who == null || who == null ||
!relation.getFollowing(who) || !relation.getFollowing(who) ||
@ -641,6 +580,7 @@ internal class DlgContextMenu(
R.id.btnConversationMute -> conversationMute(accessInfo, status) R.id.btnConversationMute -> conversationMute(accessInfo, status)
R.id.btnProfilePin -> statusPin(accessInfo, status, true) R.id.btnProfilePin -> statusPin(accessInfo, status, true)
R.id.btnProfileUnpin -> statusPin(accessInfo, status, false) R.id.btnProfileUnpin -> statusPin(accessInfo, status, false)
R.id.btnStatusHistory -> openStatusHistory(pos, accessInfo, status)
else -> return false else -> return false
} }
return true return true

View File

@ -2,10 +2,12 @@ package jp.juggler.subwaytooter.itemviewholder
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Typeface
import android.os.SystemClock import android.os.SystemClock
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
import android.view.View import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
@ -602,6 +604,14 @@ fun ItemViewHolder.showStatusTime(
if (sb.isNotEmpty()) sb.append(' ') if (sb.isNotEmpty()) sb.append(' ')
sb.append(activity.getString(R.string.featured)) sb.append(activity.getString(R.string.featured))
} }
if (status.time_edited_at > 0L) {
if (sb.isNotEmpty()) sb.append(' ')
val start = sb.length
sb.append(activity.getString(R.string.edited))
val end = sb.length
sb.setSpan(StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
} else { } else {
reblogVisibility?.takeIf { it != TootVisibility.Unknown }?.let { visibility -> reblogVisibility?.takeIf { it != TootVisibility.Unknown }?.let { visibility ->
val visIconId = Styler.getVisibilityIconId(accessInfo.isMisskey, visibility) val visIconId = Styler.getVisibilityIconId(accessInfo.isMisskey, visibility)
@ -626,12 +636,18 @@ fun ItemViewHolder.showStatusTime(
time != null -> TootStatus.formatTime( time != null -> TootStatus.formatTime(
activity, activity,
time, time,
column.type != ColumnType.CONVERSATION when (column.type) {
ColumnType.CONVERSATION, ColumnType.STATUS_HISTORY -> false
else -> true
}
) )
status != null -> TootStatus.formatTime( status != null -> TootStatus.formatTime(
activity, activity,
status.time_created_at, status.time_created_at,
column.type != ColumnType.CONVERSATION when (column.type) {
ColumnType.CONVERSATION, ColumnType.STATUS_HISTORY -> false
else -> true
}
) )
else -> "?" else -> "?"
} }

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>

View File

@ -50,6 +50,21 @@
android:textColor="?attr/colorTimeSmall" android:textColor="?attr/colorTimeSmall"
android:textSize="12sp" /> android:textSize="12sp" />
<Button
android:id="@+id/btnStatusHistory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/btn_bg_transparent_round6dp"
android:gravity="start|center_vertical"
android:minHeight="32dp"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp"
android:text="@string/edit_history"
android:textAllCaps="false" />
<Button <Button
android:id="@+id/btnStatusWebPage" android:id="@+id/btnStatusWebPage"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1127,4 +1127,6 @@
<string name="always">常に</string> <string name="always">常に</string>
<string name="auto">自動</string> <string name="auto">自動</string>
<string name="status_edit">編集(Mastodon 3.5.0+)</string> <string name="status_edit">編集(Mastodon 3.5.0+)</string>
<string name="edited">編集済</string>
<string name="edit_history">編集履歴</string>
</resources> </resources>

View File

@ -1138,4 +1138,6 @@
<string name="always">Always</string> <string name="always">Always</string>
<string name="auto">Auto</string> <string name="auto">Auto</string>
<string name="status_edit">Edit (Mastodon 3.5.0+)</string> <string name="status_edit">Edit (Mastodon 3.5.0+)</string>
<string name="edited">edited</string>
<string name="edit_history">Edit history</string>
</resources> </resources>