(Nightly fedibird)絵文字リアクション機能
This commit is contained in:
parent
f93d2c4a32
commit
97276495b8
|
@ -606,7 +606,7 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
cbConfirmUnfavourite.isEnabled = enabled
|
||||
cbConfirmToot.isEnabled = enabled
|
||||
|
||||
val ti = TootInstance.getCached(a.apiHost.ascii)
|
||||
val ti = TootInstance.getCached(a.apiHost)
|
||||
if (ti == null) {
|
||||
etMediaSizeMax.setText(a.image_max_megabytes ?: "")
|
||||
etMovieSizeMax.setText(a.movie_max_megabytes ?: "")
|
||||
|
|
|
@ -1323,7 +1323,7 @@ class ActPost : AsyncActivity(),
|
|||
|
||||
else -> {
|
||||
// インスタンス情報を確認する
|
||||
val info = TootInstance.getCached(account.apiHost.ascii)
|
||||
val info = TootInstance.getCached(account.apiHost)
|
||||
if (info == null || info.isExpire) {
|
||||
// 情報がないか古いなら再取得
|
||||
|
||||
|
@ -2222,7 +2222,7 @@ class ActPost : AsyncActivity(),
|
|||
return
|
||||
}
|
||||
|
||||
val instance = TootInstance.getCached(account.apiHost.ascii)
|
||||
val instance = TootInstance.getCached(account.apiHost)
|
||||
if (instance?.instanceType == InstanceType.Pixelfed) {
|
||||
if (in_reply_to_id != null) {
|
||||
showToast(true, R.string.pixelfed_does_not_allow_reply_with_media)
|
||||
|
@ -2660,7 +2660,7 @@ class ActPost : AsyncActivity(),
|
|||
}
|
||||
|
||||
private fun performVisibility() {
|
||||
val ti = account?.let { TootInstance.getCached(it.apiHost.ascii) }
|
||||
val ti = account?.let { TootInstance.getCached(it.apiHost) }
|
||||
|
||||
val list = when {
|
||||
account?.isMisskey == true ->
|
||||
|
|
|
@ -578,20 +578,26 @@ class Column(
|
|||
val streamCallback = object : StreamCallback {
|
||||
|
||||
override fun onStreamStatusChanged(status: StreamStatus) {
|
||||
log.d("onStreamStatusChanged status=${status}, bFirstInitialized=$bFirstInitialized, bInitialLoading=$bInitialLoading, column=${access_info.acct}/${getColumnName(true)}")
|
||||
log.d(
|
||||
"onStreamStatusChanged status=${status}, bFirstInitialized=$bFirstInitialized, bInitialLoading=$bInitialLoading, column=${access_info.acct}/${
|
||||
getColumnName(
|
||||
true
|
||||
)
|
||||
}"
|
||||
)
|
||||
|
||||
if (status == StreamStatus.Subscribed) {
|
||||
updateMisskeyCapture()
|
||||
}
|
||||
|
||||
runOnMainLooperForStreamingEvent {
|
||||
if(is_dispose.get()) return@runOnMainLooperForStreamingEvent
|
||||
if (is_dispose.get()) return@runOnMainLooperForStreamingEvent
|
||||
fireShowColumnStatus()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTimelineItem(item: TimelineItem, channelId: String?, stream: JsonArray?) {
|
||||
if(StreamManager.traceDelivery) log.v("${access_info.acct} onTimelineItem")
|
||||
if (StreamManager.traceDelivery) log.v("${access_info.acct} onTimelineItem")
|
||||
if (!canHandleStreamingMessage()) return
|
||||
|
||||
when (item) {
|
||||
|
@ -628,6 +634,14 @@ class Column(
|
|||
app_state.handler.post(mergeStreamingMessage)
|
||||
}
|
||||
|
||||
override fun onEmojiReaction(item: TootNotification) {
|
||||
// 自分によるリアクションは通知されない
|
||||
// リアクション削除は通知されない
|
||||
runOnMainLooperForStreamingEvent {
|
||||
updateEmojiReaction(item.status)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNoteUpdated(ev: MisskeyNoteUpdate, channelId: String?) {
|
||||
runOnMainLooperForStreamingEvent {
|
||||
|
||||
|
@ -668,13 +682,24 @@ class Column(
|
|||
when (ev.type) {
|
||||
MisskeyNoteUpdate.Type.REACTION -> {
|
||||
scanStatusAll { s ->
|
||||
s.increaseReaction(ev.reaction, byMe, ev.emoji, "onNoteUpdated ${ev.userId}")
|
||||
s.increaseReaction(
|
||||
true,
|
||||
ev.reaction,
|
||||
byMe,
|
||||
ev.emoji,
|
||||
"onNoteUpdated ${ev.userId}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MisskeyNoteUpdate.Type.UNREACTION -> {
|
||||
scanStatusAll { s ->
|
||||
s.decreaseReaction(ev.reaction, byMe, "onNoteUpdated ${ev.userId}")
|
||||
s.decreaseReaction(
|
||||
true,
|
||||
ev.reaction,
|
||||
byMe,
|
||||
"onNoteUpdated ${ev.userId}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -736,7 +761,7 @@ class Column(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onAnnouncementReaction(reaction: TootAnnouncement.Reaction) {
|
||||
override fun onAnnouncementReaction(reaction: TootReaction) {
|
||||
runOnMainLooperForStreamingEvent {
|
||||
// find announcement
|
||||
val announcement_id = reaction.announcement_id
|
||||
|
@ -752,7 +777,7 @@ class Column(
|
|||
}
|
||||
|
||||
index == null -> {
|
||||
announcement.reactions = ArrayList<TootAnnouncement.Reaction>().apply {
|
||||
announcement.reactions = ArrayList<TootReaction>().apply {
|
||||
add(reaction)
|
||||
}
|
||||
}
|
||||
|
@ -775,7 +800,7 @@ class Column(
|
|||
val handler = app_state.handler
|
||||
|
||||
// 未初期化や初期ロード中ならキューをクリアして何もしない
|
||||
if (!canHandleStreamingMessage() ) {
|
||||
if (!canHandleStreamingMessage()) {
|
||||
stream_data_queue.clear()
|
||||
handler.removeCallbacks(this)
|
||||
return
|
||||
|
@ -1923,6 +1948,7 @@ class Column(
|
|||
TootNotification.TYPE_MENTION,
|
||||
TootNotification.TYPE_REPLY -> dont_show_reply
|
||||
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION -> dont_show_reaction
|
||||
|
||||
TootNotification.TYPE_VOTE,
|
||||
|
@ -1947,6 +1973,8 @@ class Column(
|
|||
|
||||
TootNotification.TYPE_MENTION,
|
||||
TootNotification.TYPE_REPLY -> quick_filter != QUICK_FILTER_MENTION
|
||||
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION -> quick_filter != QUICK_FILTER_REACTION
|
||||
|
||||
TootNotification.TYPE_VOTE,
|
||||
|
@ -1956,7 +1984,8 @@ class Column(
|
|||
TootNotification.TYPE_STATUS -> quick_filter != QUICK_FILTER_POST
|
||||
else -> true
|
||||
}
|
||||
}) {
|
||||
}
|
||||
) {
|
||||
log.d("isFiltered: ${item.type} notification filtered.")
|
||||
return true
|
||||
}
|
||||
|
@ -1985,6 +2014,7 @@ class Column(
|
|||
TootNotification.TYPE_RENOTE,
|
||||
TootNotification.TYPE_QUOTE,
|
||||
TootNotification.TYPE_FAVOURITE,
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION,
|
||||
TootNotification.TYPE_FOLLOW,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||
|
@ -2042,10 +2072,12 @@ class Column(
|
|||
this.who_account = a
|
||||
|
||||
this.who_featured_tags = null
|
||||
client.request("/api/v1/accounts/${profile_id}/featured_tags")?.also { result2 ->
|
||||
client.request("/api/v1/accounts/${profile_id}/featured_tags")
|
||||
?.also { result2 ->
|
||||
|
||||
this.who_featured_tags = TootTag.parseListOrNull(parser, result2.jsonArray)
|
||||
}
|
||||
this.who_featured_tags =
|
||||
TootTag.parseListOrNull(parser, result2.jsonArray)
|
||||
}
|
||||
|
||||
client.publishApiProgress("") // カラムヘッダの再表示
|
||||
}
|
||||
|
@ -2782,13 +2814,13 @@ class Column(
|
|||
fun canStartStreaming() = when {
|
||||
// 未初期化なら何もしない
|
||||
!bFirstInitialized -> {
|
||||
if(StreamManager.traceDelivery) log.v("canStartStreaming: column is not initialized.")
|
||||
if (StreamManager.traceDelivery) log.v("canStartStreaming: column is not initialized.")
|
||||
false
|
||||
}
|
||||
|
||||
// 初期ロード中なら何もしない
|
||||
bInitialLoading -> {
|
||||
if(StreamManager.traceDelivery) log.v("canStartStreaming: is in initial loading.")
|
||||
if (StreamManager.traceDelivery) log.v("canStartStreaming: is in initial loading.")
|
||||
false
|
||||
}
|
||||
|
||||
|
@ -3030,6 +3062,46 @@ class Column(
|
|||
)
|
||||
}
|
||||
|
||||
// Fedibird 絵文字リアクション機能
|
||||
// APIの戻り値や通知データに新しいステータス情報が含まれるので、カラム中の該当する投稿のリアクション情報を更新する
|
||||
fun updateEmojiReaction(newStatus: TootStatus?) {
|
||||
newStatus ?: return
|
||||
val statusId = newStatus.id
|
||||
val newReactionSet = newStatus.reactionSet ?: TootReactionSet(isMisskey = false)
|
||||
|
||||
val changeList = ArrayList<AdapterChange>()
|
||||
|
||||
fun scanStatus1(s: TootStatus?, idx: Int, block: (s: TootStatus) -> Boolean) {
|
||||
s ?: return
|
||||
if (s.id == statusId) {
|
||||
if (block(s)) {
|
||||
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx, 1))
|
||||
}
|
||||
}
|
||||
scanStatus1(s.reblog, idx, block)
|
||||
scanStatus1(s.reply, idx, block)
|
||||
}
|
||||
|
||||
fun scanStatusAll(block: (s: TootStatus) -> Boolean) {
|
||||
for (i in 0 until list_data.size) {
|
||||
val o = list_data[i]
|
||||
if (o is TootStatus) {
|
||||
scanStatus1(o, i, block)
|
||||
} else if (o is TootNotification) {
|
||||
scanStatus1(o.status, i, block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanStatusAll { s ->
|
||||
s.updateReactionMastodon(newReactionSet)
|
||||
true
|
||||
}
|
||||
|
||||
if (changeList.isNotEmpty()) {
|
||||
fireShowContent(reason = "onEmojiReaction", changeList = changeList)
|
||||
}
|
||||
}
|
||||
|
||||
// fun findListIndexByTimelineId(orderId : EntityId) : Int? {
|
||||
// list_data.forEachIndexed { i, v ->
|
||||
|
@ -3041,4 +3113,6 @@ class Column(
|
|||
init {
|
||||
registerColumnId(column_id, this)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import jp.juggler.subwaytooter.api.TootTask
|
|||
import jp.juggler.subwaytooter.api.TootTaskRunner
|
||||
import jp.juggler.subwaytooter.api.entity.CustomEmoji
|
||||
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
|
||||
import jp.juggler.subwaytooter.api.entity.TootReaction
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.dialog.EmojiPicker
|
||||
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
|
||||
|
@ -284,10 +285,10 @@ class ColumnViewHolder(
|
|||
log.d("fling? not vertical view. $vx $vy")
|
||||
} else {
|
||||
|
||||
val vydp = vy / density
|
||||
val vyDp = vy / density
|
||||
val limit = 1024f
|
||||
log.d("fling? $vydp/$limit")
|
||||
if (vydp >= limit) {
|
||||
log.d("fling? $vyDp/$limit")
|
||||
if (vyDp >= limit) {
|
||||
|
||||
val column = column
|
||||
if (column != null && column.lastTask == null) {
|
||||
|
@ -2714,7 +2715,7 @@ class ColumnViewHolder(
|
|||
btn.allCaps = false
|
||||
btn.tag = reaction
|
||||
|
||||
btn.background = if (reaction.me == true) {
|
||||
btn.background = if (reaction.me) {
|
||||
getAdaptiveRippleDrawableRound(
|
||||
actMain,
|
||||
actMain.attrColor(R.attr.colorButtonBgCw),
|
||||
|
@ -2747,10 +2748,10 @@ class ColumnViewHolder(
|
|||
}
|
||||
|
||||
btn.setOnClickListener {
|
||||
if (reaction.me == true) {
|
||||
if (reaction.me) {
|
||||
removeReaction(item, reaction.name)
|
||||
} else {
|
||||
addReaction(item, TootAnnouncement.Reaction(jsonObject {
|
||||
addReaction(item, TootReaction.parseFedibird(jsonObject {
|
||||
put("name", reaction.name)
|
||||
put("count", 1)
|
||||
put("me", true)
|
||||
|
@ -2774,7 +2775,7 @@ class ColumnViewHolder(
|
|||
llAnnouncementExtra.addView(box)
|
||||
}
|
||||
|
||||
private fun addReaction(item: TootAnnouncement, sample: TootAnnouncement.Reaction?) {
|
||||
private fun addReaction(item: TootAnnouncement, sample: TootReaction?) {
|
||||
val column = column ?: return
|
||||
if (sample == null) {
|
||||
EmojiPicker(activity, column.access_info, closeOnSelected = true) { result ->
|
||||
|
@ -2785,7 +2786,7 @@ class ColumnViewHolder(
|
|||
else -> error("unknown emoji type")
|
||||
}
|
||||
log.d("addReaction: $code ${result.emoji.javaClass.simpleName}")
|
||||
addReaction(item, TootAnnouncement.Reaction(jsonObject {
|
||||
addReaction(item, TootReaction.parseFedibird(jsonObject {
|
||||
put("name", code)
|
||||
put("count", 1)
|
||||
put("me", true)
|
||||
|
|
|
@ -908,6 +908,7 @@ internal class ItemViewHolder(
|
|||
}
|
||||
}
|
||||
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION -> {
|
||||
val colorBg = Pref.ipEventBgColorReaction(activity.pref)
|
||||
if (n_account != null) showBoost(
|
||||
|
@ -915,7 +916,7 @@ internal class ItemViewHolder(
|
|||
n.time_created_at,
|
||||
R.drawable.ic_face,
|
||||
R.string.display_name_reaction_by,
|
||||
misskeyReaction = n.reaction ?: "?",
|
||||
reaction = n.reaction ?: TootReaction.UNKNOWN,
|
||||
boost_status = n_status
|
||||
)
|
||||
if (n_status != null) {
|
||||
|
@ -1168,7 +1169,7 @@ internal class ItemViewHolder(
|
|||
time: Long,
|
||||
iconId: Int,
|
||||
string_id: Int,
|
||||
misskeyReaction: String? = null,
|
||||
reaction: TootReaction? = null,
|
||||
boost_status: TootStatus? = null
|
||||
) {
|
||||
boost_account = whoRef
|
||||
|
@ -1184,7 +1185,7 @@ internal class ItemViewHolder(
|
|||
val who = whoRef.get()
|
||||
|
||||
// フォローの場合 decoded_display_name が2箇所で表示に使われるのを避ける必要がある
|
||||
val text: Spannable = if (misskeyReaction != null) {
|
||||
val text: Spannable = if (reaction != null) {
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
access_info,
|
||||
|
@ -1192,7 +1193,7 @@ internal class ItemViewHolder(
|
|||
enlargeEmoji = 1.5f,
|
||||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
val ssb = MisskeyReaction.toSpannableStringBuilder(misskeyReaction, options, boost_status)
|
||||
val ssb = reaction.toSpannableStringBuilder( options, boost_status)
|
||||
ssb.append(" ")
|
||||
ssb.append(who.decodeDisplayName(activity)
|
||||
.intoStringResource(activity, string_id))
|
||||
|
@ -2462,9 +2463,15 @@ internal class ItemViewHolder(
|
|||
|
||||
}
|
||||
|
||||
private fun makeReactionsView(status: TootStatus) {
|
||||
if (!access_info.isMisskey) return
|
||||
private fun canReaction() = when{
|
||||
access_info.isPseudo -> false
|
||||
access_info.isMisskey -> true
|
||||
TootInstance.getCached(access_info.apiHost)?.fedibird_capabilities?.contains("emoji_reaction") == true -> true
|
||||
else->false
|
||||
}
|
||||
|
||||
private fun makeReactionsView(status: TootStatus) {
|
||||
if( !canReaction() && status.reactionSet == null ) return
|
||||
|
||||
val density = activity.density
|
||||
|
||||
|
@ -2501,13 +2508,19 @@ internal class ItemViewHolder(
|
|||
R.drawable.btn_bg_transparent_round6dp
|
||||
)
|
||||
|
||||
val hasMyReaction = status.myReaction?.isNotEmpty() == true
|
||||
val hasMyReaction = status.reactionSet?.myReaction?.isNotEmpty() ?: false
|
||||
b.contentDescription =
|
||||
activity.getString(if (hasMyReaction) R.string.reaction_remove else R.string.reaction_add)
|
||||
b.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
b.padding = paddingV
|
||||
b.setOnClickListener {
|
||||
if (hasMyReaction) {
|
||||
if(!canReaction()){
|
||||
Action_Toot.reactionFromAnotherAccount(
|
||||
activity,
|
||||
access_info,
|
||||
status_showing
|
||||
)
|
||||
}else if (hasMyReaction) {
|
||||
removeReaction(status, false)
|
||||
} else {
|
||||
addReaction(status, null)
|
||||
|
@ -2532,8 +2545,8 @@ internal class ItemViewHolder(
|
|||
)
|
||||
})
|
||||
|
||||
val reactionCounts = status.reactionCounts
|
||||
if (reactionCounts != null) {
|
||||
val reactionSet = status.reactionSet
|
||||
if (reactionSet != null) {
|
||||
|
||||
var lastButton: View? = null
|
||||
|
||||
|
@ -2545,12 +2558,12 @@ internal class ItemViewHolder(
|
|||
enlargeCustomEmoji = 1.5f
|
||||
)
|
||||
|
||||
for (entry in reactionCounts.entries) {
|
||||
val key = entry.key
|
||||
val count = entry.value
|
||||
if (count <= 0) continue
|
||||
val ssb = MisskeyReaction.toSpannableStringBuilder(key, options, status)
|
||||
.also { it.append(" $count") }
|
||||
for (entry in reactionSet.entries) {
|
||||
val code = entry.key
|
||||
val reaction = entry.value
|
||||
if (reaction.count <= 0L) continue
|
||||
val ssb = reaction.toSpannableStringBuilder( options, status)
|
||||
.also { it.append(" ${reaction.count}") }
|
||||
|
||||
val b = Button(act).apply {
|
||||
layoutParams = FlexboxLayout.LayoutParams(
|
||||
|
@ -2561,7 +2574,7 @@ internal class ItemViewHolder(
|
|||
}
|
||||
minWidthCompat = buttonHeight
|
||||
|
||||
background = if (MisskeyReaction.equals(status.myReaction, key)) {
|
||||
background = if (TootReaction.equals(reactionSet.myReaction, code)) {
|
||||
// 自分がリアクションしたやつは背景を変える
|
||||
getAdaptiveRippleDrawableRound(
|
||||
act,
|
||||
|
@ -2582,13 +2595,13 @@ internal class ItemViewHolder(
|
|||
text = ssb
|
||||
|
||||
allCaps = false
|
||||
tag = key
|
||||
tag = code
|
||||
setOnClickListener {
|
||||
val code = it.tag as? String
|
||||
if( MisskeyReaction.equals(status.myReaction, code)){
|
||||
val tagStr = it.tag as? String
|
||||
if( TootReaction.equals(status.reactionSet?.myReaction, tagStr)){
|
||||
removeReaction(status, false)
|
||||
}else{
|
||||
addReaction(status,code)
|
||||
addReaction(status,tagStr)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2619,20 +2632,24 @@ internal class ItemViewHolder(
|
|||
llExtra.addView(box)
|
||||
}
|
||||
|
||||
private fun addReaction(status: TootStatus, code: String?) {
|
||||
|
||||
if (status.myReaction?.isNotEmpty() == true) {
|
||||
// code は code@dmain のような形式かもしれない
|
||||
private fun addReaction(status: TootStatus, code: String? ) {
|
||||
if (status.reactionSet?.myReaction?.notEmpty() != null ) {
|
||||
activity.showToast(false, R.string.already_reactioned)
|
||||
return
|
||||
}
|
||||
|
||||
if (access_info.isPseudo || !access_info.isMisskey) return
|
||||
if(!canReaction()) return
|
||||
|
||||
if (code == null) {
|
||||
EmojiPicker(activity, access_info, closeOnSelected = true) { result ->
|
||||
addReaction(status, when(val emoji = result.emoji){
|
||||
is UnicodeEmoji -> emoji.unifiedCode
|
||||
is CustomEmoji -> ":${emoji.shortcode}:"
|
||||
is CustomEmoji -> if(access_info.isMisskey) {
|
||||
":${emoji.shortcode}:"
|
||||
}else{
|
||||
emoji.shortcode
|
||||
}
|
||||
else->error("unknown emoji type")
|
||||
})
|
||||
}.show()
|
||||
|
@ -2642,15 +2659,23 @@ internal class ItemViewHolder(
|
|||
TootTaskRunner(activity, progress_style = TootTaskRunner.PROGRESS_NONE).run(access_info,
|
||||
object : TootTask {
|
||||
|
||||
var newStatus : TootStatus? = null
|
||||
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? {
|
||||
|
||||
val params = access_info.putMisskeyApiToken().apply {
|
||||
put("noteId", status.id.toString())
|
||||
put("reaction", code)
|
||||
}
|
||||
|
||||
// 成功すると204 no content
|
||||
return client.request("/api/notes/reactions/create", params.toPostRequestBuilder())
|
||||
return if(access_info.isMisskey){
|
||||
client.request("/api/notes/reactions/create", access_info.putMisskeyApiToken().apply {
|
||||
put("noteId", status.id.toString())
|
||||
put("reaction", code)
|
||||
}.toPostRequestBuilder())
|
||||
// 成功すると204 no content
|
||||
}else{
|
||||
client.request("/api/v1/statuses/${status.id}/emoji_reactions/${code.encodePercent("@")}",
|
||||
"".toFormRequestBody().toPut())
|
||||
// 成功すると新しいステータス
|
||||
?.also{ result->
|
||||
newStatus = TootParser(activity,access_info).status(result.jsonObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
|
@ -2663,32 +2688,38 @@ internal class ItemViewHolder(
|
|||
}
|
||||
when (val resCode = result.response?.code) {
|
||||
in 200 until 300 -> {
|
||||
if (status.increaseReaction(code, true, caller="addReaction")) {
|
||||
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
|
||||
list_adapter.notifyChange(reason = "addReaction complete", reset = true)
|
||||
}
|
||||
if(newStatus != null){
|
||||
activity.app_state.columnList.forEach { column->
|
||||
if( column.access_info.acct == access_info.acct)
|
||||
column.updateEmojiReaction( newStatus)
|
||||
}
|
||||
}else{
|
||||
if (status.increaseReaction(access_info.isMisskey,code, true, caller="addReaction")) {
|
||||
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
|
||||
list_adapter.notifyChange(reason = "addReaction complete", reset = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> activity.showToast(false, "HTTP error $resCode")
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
private fun removeReaction(status: TootStatus, confirmed: Boolean = false) {
|
||||
|
||||
val reaction = status.myReaction
|
||||
val code = status.reactionSet?.myReaction?.notEmpty()
|
||||
|
||||
if (reaction?.isNotEmpty() != true) {
|
||||
if ( code ==null ) {
|
||||
activity.showToast(false, R.string.not_reactioned)
|
||||
return
|
||||
}
|
||||
|
||||
if (access_info.isPseudo || !access_info.isMisskey) return
|
||||
if(!canReaction()) return
|
||||
|
||||
if (!confirmed) {
|
||||
AlertDialog.Builder(activity)
|
||||
.setMessage(activity.getString(R.string.reaction_remove_confirm, reaction))
|
||||
.setMessage(activity.getString(R.string.reaction_remove_confirm, code))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
removeReaction(status, confirmed = true)
|
||||
|
@ -2699,15 +2730,27 @@ internal class ItemViewHolder(
|
|||
|
||||
TootTaskRunner(activity, progress_style = TootTaskRunner.PROGRESS_NONE).run(access_info,
|
||||
object : TootTask {
|
||||
|
||||
var newStatus: TootStatus? = null
|
||||
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? =
|
||||
// 成功すると204 no content
|
||||
client.request(
|
||||
"/api/notes/reactions/delete",
|
||||
access_info.putMisskeyApiToken().apply {
|
||||
put("noteId", status.id.toString())
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
)
|
||||
if(access_info.isMisskey) {
|
||||
client.request(
|
||||
"/api/notes/reactions/delete",
|
||||
access_info.putMisskeyApiToken().apply {
|
||||
put("noteId", status.id.toString())
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
)
|
||||
// 成功すると204 no content
|
||||
}else{
|
||||
client.request("/api/v1/statuses/${status.id}/emoji_unreaction",
|
||||
"".toFormRequestBody().toPost())
|
||||
// 成功すると新しいステータス
|
||||
?.also{ result->
|
||||
newStatus = TootParser(activity,access_info).status(result.jsonObject)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
result ?: return
|
||||
|
@ -2719,13 +2762,20 @@ internal class ItemViewHolder(
|
|||
}
|
||||
|
||||
if ((result.response?.code ?: -1) in 200 until 300) {
|
||||
if (status.decreaseReaction(reaction, true, "removeReaction")) {
|
||||
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
|
||||
list_adapter.notifyChange(
|
||||
reason = "removeReaction complete",
|
||||
reset = true
|
||||
)
|
||||
}
|
||||
if(newStatus != null){
|
||||
activity.app_state.columnList.forEach { column->
|
||||
if( column.access_info.acct == access_info.acct)
|
||||
column.updateEmojiReaction( newStatus)
|
||||
}
|
||||
}else{
|
||||
if (status.decreaseReaction(access_info.isMisskey,code, true,"removeReaction")) {
|
||||
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
|
||||
list_adapter.notifyChange(
|
||||
reason = "removeReaction complete",
|
||||
reset = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -192,7 +192,7 @@ internal class StatusButtons(
|
|||
)
|
||||
}
|
||||
|
||||
val ti = TootInstance.getCached(access_info.apiHost.ascii)
|
||||
val ti = TootInstance.getCached(access_info.apiHost)
|
||||
btnQuote.vg(ti?.feature_quote == true)?.let{
|
||||
setButton(
|
||||
btnQuote,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,111 +0,0 @@
|
|||
package jp.juggler.subwaytooter.api.entity
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.Pref
|
||||
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.DecodeOptions
|
||||
import jp.juggler.subwaytooter.util.EmojiDecoder
|
||||
|
||||
//private fun findSvgFile(utf16: String) =
|
||||
// EmojiMap.sUTF16ToEmojiResource[utf16]
|
||||
//
|
||||
//fun EmojiMap.EmojiResource.loadToImageView(activity: ActMain, view: ImageView) {
|
||||
// if (isSvg) {
|
||||
// Glide.with(activity)
|
||||
// .`as`(PictureDrawable::class.java)
|
||||
// .load("file:///android_asset/${assetsName}")
|
||||
// .into(view)
|
||||
// } else {
|
||||
// Glide.with(activity)
|
||||
// .load(drawableId)
|
||||
// .into(view)
|
||||
// }
|
||||
//}
|
||||
|
||||
object MisskeyReaction {
|
||||
private val oldReactions = mapOf(
|
||||
"like" to "\ud83d\udc4d",
|
||||
"love" to "\u2665",
|
||||
"laugh" to "\ud83d\ude06",
|
||||
"hmm" to "\ud83e\udd14",
|
||||
"surprise" to "\ud83d\ude2e",
|
||||
"congrats" to "\ud83c\udf89",
|
||||
"angry" to "\ud83d\udca2",
|
||||
"confused" to "\ud83d\ude25",
|
||||
"rip" to "\ud83d\ude07",
|
||||
"pudding" to "\ud83c\udf6e",
|
||||
"star" to "\u2B50", // リモートからのFavを示す代替リアクション。ピッカーには表示しない
|
||||
)
|
||||
|
||||
private val reCustomEmoji = """\A:([^:]+):\z""".toRegex()
|
||||
|
||||
fun getAnotherExpression(reaction: String): String? {
|
||||
val customCode = reCustomEmoji.find(reaction)?.groupValues?.elementAtOrNull(1) ?: return null
|
||||
val cols = customCode.split("@")
|
||||
val name = cols.elementAtOrNull(0)
|
||||
val domain = cols.elementAtOrNull(1)
|
||||
return if (domain == null) ":$name@.:" else if (domain == ".") ":$name:" else null
|
||||
}
|
||||
|
||||
fun equals(a:String?,b:String?) = when {
|
||||
a==null -> b==null
|
||||
b==null -> false
|
||||
else -> a ==b || getAnotherExpression(a) == b || a == getAnotherExpression(b)
|
||||
}
|
||||
|
||||
fun toSpannableStringBuilder(
|
||||
code: String,
|
||||
options: DecodeOptions,
|
||||
status:TootStatus?
|
||||
): SpannableStringBuilder {
|
||||
|
||||
// 古い形式の絵文字はUnicode絵文字にする
|
||||
oldReactions[code]?.let {
|
||||
return EmojiDecoder.decodeEmoji(options, it)
|
||||
}
|
||||
|
||||
fun CustomEmoji.toSpannableStringBuilder():SpannableStringBuilder?{
|
||||
return if (Pref.bpDisableEmojiAnimation(App1.pref)) {
|
||||
static_url
|
||||
} else {
|
||||
url
|
||||
}?.let{
|
||||
SpannableStringBuilder(code).apply {
|
||||
setSpan(
|
||||
NetworkEmojiSpan(it, scale = options.enlargeCustomEmoji),
|
||||
0,
|
||||
length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// カスタム絵文字
|
||||
val customCode = reCustomEmoji.find(code)?.groupValues?.elementAtOrNull(1)
|
||||
if(customCode != null){
|
||||
var ce = status?.custom_emojis?.get( customCode)
|
||||
if(ce != null) return ce.toSpannableStringBuilder()?: EmojiDecoder.decodeEmoji(options, code)
|
||||
|
||||
val accessInfo = options.linkHelper as? SavedAccount
|
||||
|
||||
val cols = customCode.split("@",limit = 2)
|
||||
val key = cols.elementAtOrNull(0)
|
||||
val domain = cols.elementAtOrNull(1)
|
||||
if( domain == null || domain=="" || domain=="." || domain == accessInfo?.apiHost?.ascii ){
|
||||
if( accessInfo != null){
|
||||
ce = App1.custom_emoji_lister
|
||||
.getMap(accessInfo)
|
||||
?.get(key)
|
||||
if(ce != null) return ce.toSpannableStringBuilder()?: EmojiDecoder.decodeEmoji(options, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unicode絵文字、もしくは :xxx: などのshortcode表現
|
||||
return EmojiDecoder.decodeEmoji(options, code)
|
||||
}
|
||||
}
|
|
@ -10,18 +10,7 @@ import java.util.*
|
|||
|
||||
class TootAnnouncement(parser : TootParser, src : JsonObject) {
|
||||
|
||||
class Reaction(val src : JsonObject) {
|
||||
val name = src.string("name") ?: "?"
|
||||
var count = src.long("count") ?: 0
|
||||
var me = src.boolean("me") // ストリーミングイベントではmeは定義されない
|
||||
// 以下はカスタム絵文字のみ
|
||||
val url = src.string("url")
|
||||
val static_url = src.string("static_url")
|
||||
|
||||
// ストリーミングイベントでは告知IDが含まれる
|
||||
val announcement_id = EntityId.mayNull(src.string("announcement_id"))
|
||||
}
|
||||
|
||||
|
||||
// {"id":"1",
|
||||
// "content":"\u003cp\u003e日本語\u003cbr /\u003eURL \u003ca href=\"https://www.youtube.com/watch?v=2n1fM2ItdL8\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"ellipsis\"\u003eyoutube.com/watch?v=2n1fM2ItdL\u003c/span\u003e\u003cspan class=\"invisible\"\u003e8\u003c/span\u003e\u003c/a\u003e\u003cbr /\u003eカスタム絵文字 :ct013: \u003cbr /\u003e普通の絵文字 🤹 \u003c/p\u003e\u003cp\u003e改行2つ\u003c/p\u003e",
|
||||
// "starts_at":"2020-01-23T00:00:00.000Z",
|
||||
|
@ -51,7 +40,7 @@ class TootAnnouncement(parser : TootParser, src : JsonObject) {
|
|||
// An array of Mentions
|
||||
val mentions : ArrayList<TootMention>?
|
||||
|
||||
var reactions : MutableList<Reaction>? = null
|
||||
var reactions : MutableList<TootReaction>? = null
|
||||
|
||||
init {
|
||||
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
|
||||
|
@ -79,7 +68,7 @@ class TootAnnouncement(parser : TootParser, src : JsonObject) {
|
|||
this.content = src.string("content") ?: ""
|
||||
this.decoded_content = options.decodeHTML(content)
|
||||
|
||||
this.reactions = parseListOrNull(::Reaction, src.jsonArray("reactions"))
|
||||
this.reactions = parseListOrNull(TootReaction::parseFedibird, src.jsonArray("reactions"))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -132,7 +121,7 @@ class TootAnnouncement(parser : TootParser, src : JsonObject) {
|
|||
if(dstReactions == null) {
|
||||
dst.reactions = oldReactions
|
||||
} else if(oldReactions != null) {
|
||||
val reactions = mutableListOf<Reaction>()
|
||||
val reactions = mutableListOf<TootReaction>()
|
||||
reactions.addAll(oldReactions)
|
||||
for(newItem in dstReactions) {
|
||||
val oldItem = reactions.find { it.name == newItem.name }
|
||||
|
|
|
@ -398,7 +398,8 @@ class TootInstance(parser: TootParser, src: JsonObject) {
|
|||
|
||||
// get from cache
|
||||
// no request, no expiration check
|
||||
fun getCached(host: String) = Host.parse(host).getCacheEntry().cacheData
|
||||
fun getCached(apiHost: String) = Host.parse(apiHost).getCacheEntry().cacheData
|
||||
fun getCached(apiHost: Host) = apiHost.getCacheEntry().cacheData
|
||||
|
||||
suspend fun get(client: TootApiClient): Pair<TootInstance?, TootApiResult?> = getEx(client)
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.api.entity
|
|||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.util.JsonObject
|
||||
import jp.juggler.util.LogCategory
|
||||
import jp.juggler.util.notEmpty
|
||||
|
||||
class TootNotification(parser : TootParser, src : JsonObject) : TimelineItem() {
|
||||
|
||||
|
@ -24,8 +25,9 @@ class TootNotification(parser : TootParser, src : JsonObject) : TimelineItem() {
|
|||
const val TYPE_UNFOLLOW = "unfollow" // Mastodon,Misskey
|
||||
|
||||
const val TYPE_FAVOURITE = "favourite"
|
||||
const val TYPE_REACTION = "reaction"
|
||||
|
||||
const val TYPE_REACTION = "reaction" // misskey
|
||||
const val TYPE_EMOJI_REACTION = "emoji_reaction" // fedibird
|
||||
|
||||
const val TYPE_FOLLOW_REQUEST = "follow_request"
|
||||
const val TYPE_FOLLOW_REQUEST_MISSKEY = "receiveFollowRequest"
|
||||
|
||||
|
@ -46,7 +48,7 @@ class TootNotification(parser : TootParser, src : JsonObject) : TimelineItem() {
|
|||
val type : String // One of: "mention", "reblog", "favourite", "follow"
|
||||
val accountRef : TootAccountRef? // The Account sending the notification to the user
|
||||
val status : TootStatus? // The Status associated with the notification, if applicable
|
||||
var reaction : String? = null
|
||||
var reaction : TootReaction? = null
|
||||
|
||||
private val created_at : String? // The time the notification was created
|
||||
val time_created_at : Long
|
||||
|
@ -78,6 +80,8 @@ class TootNotification(parser : TootParser, src : JsonObject) : TimelineItem() {
|
|||
)
|
||||
|
||||
reaction = src.string("reaction")
|
||||
?.notEmpty()
|
||||
?.let{ TootReaction.parseMisskey(it)}
|
||||
|
||||
// Misskeyの通知APIはページネーションをIDでしか行えない
|
||||
// これは改善される予定 https://github.com/syuilo/misskey/issues/2275
|
||||
|
@ -92,8 +96,10 @@ class TootNotification(parser : TootParser, src : JsonObject) : TimelineItem() {
|
|||
accountRef =
|
||||
TootAccountRef.mayNull(parser, parser.account(src.jsonObject("account")))
|
||||
status = parser.status(src.jsonObject("status"))
|
||||
|
||||
|
||||
reaction = src.jsonObject("emoji_reaction")
|
||||
?.notEmpty()
|
||||
?.let{ TootReaction.parseFedibird(it)}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package jp.juggler.subwaytooter.api.entity
|
||||
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.TootAnnouncement.Reaction
|
||||
import jp.juggler.subwaytooter.api.entity.TootReaction
|
||||
import jp.juggler.util.*
|
||||
|
||||
object TootPayload {
|
||||
|
@ -62,7 +62,7 @@ object TootPayload {
|
|||
|
||||
"announcement" -> parseItem(::TootAnnouncement, parser, src)
|
||||
|
||||
"announcement.reaction" -> parseItem(::Reaction, src)
|
||||
"announcement.reaction" -> parseItem(TootReaction::parseFedibird, src)
|
||||
|
||||
else -> {
|
||||
log.e("unknown payload(2). message=%s", parent_text)
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
package jp.juggler.subwaytooter.api.entity
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.Pref
|
||||
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.DecodeOptions
|
||||
import jp.juggler.subwaytooter.util.EmojiDecoder
|
||||
import jp.juggler.util.JsonArray
|
||||
import jp.juggler.util.JsonObject
|
||||
import jp.juggler.util.notEmpty
|
||||
import jp.juggler.util.notZero
|
||||
|
||||
class TootReaction(
|
||||
// (fedibird絵文字リアクション) unicode絵文字はunicodeそのまま、 カスタム絵文字はコロンなしのshortcode
|
||||
// (Misskey)カスタム絵文字は前後にコロンがつく
|
||||
val name: String,
|
||||
|
||||
// カスタム絵文字の場合は定義される
|
||||
val url: String? = null,
|
||||
val static_url: String? = null,
|
||||
|
||||
// (fedibird絵文字リアクション) 通知オブジェクト直下ではcountは常に0)
|
||||
var count: Long = 0,
|
||||
|
||||
// (fedibird絵文字リアクション) 通知オブジェクト直下ではmeは常にfalse
|
||||
// 告知のストリーミングイベントではmeは定義されない
|
||||
var me: Boolean = false,
|
||||
|
||||
// 告知のストリーミングイベントでは告知IDが定義される
|
||||
val announcement_id: EntityId? = null,
|
||||
|
||||
) {
|
||||
companion object {
|
||||
|
||||
fun appendDomain(name: String, domain: String?) =
|
||||
if (domain?.isNotEmpty() == true) {
|
||||
"$name@$domain"
|
||||
} else {
|
||||
name
|
||||
}
|
||||
|
||||
// Fedibirdの投稿や通知に含まれる
|
||||
fun parseFedibird(src: JsonObject) = TootReaction(
|
||||
// (fedibird絵文字リアクション) リモートのカスタム絵文字の場合はdomainが指定される
|
||||
name = appendDomain(src.string("name") ?: "?", src.string("domain")),
|
||||
url = src.string("url"),
|
||||
static_url = src.string("static_url"),
|
||||
count = src.long("count") ?: 0,
|
||||
me = src.boolean("me") ?: false,
|
||||
announcement_id = EntityId.mayNull(src.string("announcement_id")),
|
||||
)
|
||||
|
||||
// Misskeyの通知にあるreaction文字列
|
||||
fun parseMisskey(name: String?, count: Long = 0L) =
|
||||
name?.let {
|
||||
TootReaction(name = it, count = count)
|
||||
}
|
||||
|
||||
private val misskeyOldReactions = mapOf(
|
||||
"like" to "\ud83d\udc4d",
|
||||
"love" to "\u2665",
|
||||
"laugh" to "\ud83d\ude06",
|
||||
"hmm" to "\ud83e\udd14",
|
||||
"surprise" to "\ud83d\ude2e",
|
||||
"congrats" to "\ud83c\udf89",
|
||||
"angry" to "\ud83d\udca2",
|
||||
"confused" to "\ud83d\ude25",
|
||||
"rip" to "\ud83d\ude07",
|
||||
"pudding" to "\ud83c\udf6e",
|
||||
"star" to "\u2B50", // リモートからのFavを示す代替リアクション。ピッカーには表示しない
|
||||
)
|
||||
|
||||
private val reCustomEmoji = """\A:([^:]+):\z""".toRegex()
|
||||
|
||||
fun getAnotherExpression(reaction: String): String? {
|
||||
val customCode =
|
||||
reCustomEmoji.find(reaction)?.groupValues?.elementAtOrNull(1) ?: return null
|
||||
val cols = customCode.split("@")
|
||||
val name = cols.elementAtOrNull(0)
|
||||
val domain = cols.elementAtOrNull(1)
|
||||
return if (domain == null) ":$name@.:" else if (domain == ".") ":$name:" else null
|
||||
}
|
||||
|
||||
fun equals(a: String?, b: String?) = when {
|
||||
a == null -> b == null
|
||||
b == null -> false
|
||||
else -> a == b || getAnotherExpression(a) == b || a == getAnotherExpression(b)
|
||||
}
|
||||
|
||||
val UNKNOWN = TootReaction(name = "?")
|
||||
}
|
||||
|
||||
fun toSpannableStringBuilder(
|
||||
options: DecodeOptions,
|
||||
status: TootStatus?
|
||||
): SpannableStringBuilder {
|
||||
|
||||
val code = this.name
|
||||
|
||||
fun CustomEmoji.chooseUrl() =
|
||||
if (Pref.bpDisableEmojiAnimation(App1.pref)) {
|
||||
static_url
|
||||
} else {
|
||||
url
|
||||
}
|
||||
|
||||
fun urlToSpan(options: DecodeOptions, code: String, url: String) =
|
||||
SpannableStringBuilder(code).apply {
|
||||
setSpan(
|
||||
NetworkEmojiSpan(url, scale = options.enlargeCustomEmoji),
|
||||
0,
|
||||
length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
if (options.linkHelper?.isMisskey == true) {
|
||||
|
||||
// 古い形式の絵文字はUnicode絵文字にする
|
||||
misskeyOldReactions[code]?.let {
|
||||
return EmojiDecoder.decodeEmoji(options, it)
|
||||
}
|
||||
|
||||
// カスタム絵文字
|
||||
val customCode = reCustomEmoji.find(code)?.groupValues?.elementAtOrNull(1)
|
||||
if (customCode != null) {
|
||||
status?.custom_emojis?.get(customCode)
|
||||
?.chooseUrl()
|
||||
?.notEmpty()
|
||||
?.let { return urlToSpan(options, code, it) }
|
||||
|
||||
val accessInfo = options.linkHelper as? SavedAccount
|
||||
val cols = customCode.split("@", limit = 2)
|
||||
val key = cols.elementAtOrNull(0)
|
||||
val domain = cols.elementAtOrNull(1)
|
||||
if (domain == null || domain == "" || domain == "." || domain == accessInfo?.apiHost?.ascii) {
|
||||
if (accessInfo != null) {
|
||||
App1.custom_emoji_lister
|
||||
.getMap(accessInfo)
|
||||
?.get(key)
|
||||
?.chooseUrl()
|
||||
?.notEmpty()
|
||||
?.let { return urlToSpan(options, code, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val url = if (Pref.bpDisableEmojiAnimation(App1.pref)) {
|
||||
static_url
|
||||
} else {
|
||||
url
|
||||
}
|
||||
url?.notEmpty()?.let { return urlToSpan(options, code, url) }
|
||||
}
|
||||
// unicode絵文字、もしくは :xxx: などのshortcode表現
|
||||
return EmojiDecoder.decodeEmoji(options, code)
|
||||
}
|
||||
}
|
||||
|
||||
class TootReactionSet(val isMisskey: Boolean) : LinkedHashMap<String, TootReaction>() {
|
||||
|
||||
var myReaction: String? = null
|
||||
|
||||
companion object {
|
||||
fun parseMisskey(
|
||||
src: JsonObject?,
|
||||
myReaction: String? = null
|
||||
): TootReactionSet? {
|
||||
src ?: return null
|
||||
val dst = TootReactionSet(isMisskey = true)
|
||||
for (entry in src.entries) {
|
||||
val key = entry.key.notEmpty() ?: continue
|
||||
val v = src.long(key)?.notZero() ?: continue
|
||||
dst[key] = TootReaction(name = key, count = v)
|
||||
}
|
||||
if (myReaction != null) {
|
||||
dst[myReaction]?.let {
|
||||
it.me = true
|
||||
dst.myReaction = myReaction
|
||||
}
|
||||
}
|
||||
return dst.notEmpty()
|
||||
}
|
||||
|
||||
fun parseFedibird(
|
||||
src: JsonArray? = null,
|
||||
emoji_reactioned: Boolean? = null
|
||||
): TootReactionSet? {
|
||||
src ?: return null
|
||||
val dst = TootReactionSet(isMisskey = false)
|
||||
src.objectList().forEach {
|
||||
val tr = TootReaction.parseFedibird(it)
|
||||
if (tr.count > 0) dst[tr.name] = tr
|
||||
}
|
||||
dst.values.find { it.me }?.let {
|
||||
dst.myReaction = it.name
|
||||
}
|
||||
return dst.notEmpty()
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -22,7 +22,7 @@ import jp.juggler.util.vg
|
|||
|
||||
class DlgCreateAccount(
|
||||
val activity : AppCompatActivity,
|
||||
val instance : Host,
|
||||
val apiHost : Host,
|
||||
val onClickOk : (
|
||||
dialog : Dialog,
|
||||
username : String,
|
||||
|
@ -52,7 +52,7 @@ class DlgCreateAccount(
|
|||
private val dialog = Dialog(activity)
|
||||
|
||||
init {
|
||||
viewRoot.findViewById<TextView>(R.id.tvInstance).text = instance.pretty
|
||||
viewRoot.findViewById<TextView>(R.id.tvInstance).text = apiHost.pretty
|
||||
|
||||
arrayOf(
|
||||
R.id.btnRules,
|
||||
|
@ -63,13 +63,13 @@ class DlgCreateAccount(
|
|||
viewRoot.findViewById<Button>(it)?.setOnClickListener(this)
|
||||
}
|
||||
|
||||
val instanceInfo = TootInstance.getCached(instance.ascii)
|
||||
val instanceInfo = TootInstance.getCached(apiHost)
|
||||
|
||||
tvDescription.text =
|
||||
DecodeOptions(
|
||||
activity,
|
||||
linkHelper = LinkHelper.create(
|
||||
instance,
|
||||
apiHost,
|
||||
misskeyVersion = instanceInfo?.misskeyVersion ?: 0
|
||||
)
|
||||
).decodeHTML(
|
||||
|
@ -97,10 +97,10 @@ class DlgCreateAccount(
|
|||
override fun onClick(v : View?) {
|
||||
when(v?.id) {
|
||||
R.id.btnRules ->
|
||||
activity.openCustomTab("https://$instance/about/more")
|
||||
activity.openCustomTab("https://$apiHost/about/more")
|
||||
|
||||
R.id.btnTerms ->
|
||||
activity.openCustomTab("https://$instance/terms")
|
||||
activity.openCustomTab("https://$apiHost/terms")
|
||||
|
||||
R.id.btnCancel ->
|
||||
dialog.cancel()
|
||||
|
|
|
@ -881,6 +881,7 @@ class TaskRunner(
|
|||
TootNotification.TYPE_FAVOURITE ->
|
||||
context.getString(R.string.display_name_favourited_by, name)
|
||||
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION ->
|
||||
context.getString(R.string.display_name_reaction_by, name)
|
||||
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
package jp.juggler.subwaytooter.streaming
|
||||
|
||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||
import jp.juggler.subwaytooter.api.entity.MisskeyNoteUpdate
|
||||
import jp.juggler.subwaytooter.api.entity.TimelineItem
|
||||
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.util.JsonArray
|
||||
|
||||
interface StreamCallback {
|
||||
fun onStreamStatusChanged(status: StreamStatus)
|
||||
|
||||
fun onTimelineItem(item: TimelineItem, channelId: String?,stream: JsonArray?)
|
||||
fun onEmojiReaction(item:TootNotification)
|
||||
fun onNoteUpdated(ev: MisskeyNoteUpdate, channelId: String?)
|
||||
fun onAnnouncementUpdate(item: TootAnnouncement)
|
||||
fun onAnnouncementDelete(id: EntityId)
|
||||
fun onAnnouncementReaction(reaction: TootAnnouncement.Reaction)
|
||||
fun onAnnouncementReaction(reaction: TootReaction)
|
||||
}
|
||||
|
|
|
@ -24,10 +24,10 @@ class StreamConnection(
|
|||
private val manager: StreamManager,
|
||||
private val acctGroup: StreamGroupAcct,
|
||||
val spec: StreamSpec? = null, // null if merged connection
|
||||
val name :String
|
||||
) : WebSocketListener() ,TootApiCallback {
|
||||
val name: String
|
||||
) : WebSocketListener(), TootApiCallback {
|
||||
|
||||
companion object{
|
||||
companion object {
|
||||
private val log = LogCategory("StreamConnection")
|
||||
|
||||
private const val misskeyAliveInterval = 60000L
|
||||
|
@ -65,26 +65,36 @@ class StreamConnection(
|
|||
// methods
|
||||
|
||||
private fun eachCallbackForSpec(
|
||||
spec:StreamSpec,
|
||||
spec: StreamSpec,
|
||||
channelId: String? = null,
|
||||
stream:JsonArray? = null,
|
||||
item:TimelineItem?=null,
|
||||
stream: JsonArray? = null,
|
||||
item: TimelineItem? = null,
|
||||
block: (callback: StreamCallback) -> Unit
|
||||
) {
|
||||
if (isDisposed.get()) return
|
||||
acctGroup.keyGroups[spec]?.eachCallback(channelId,stream,item,block)
|
||||
acctGroup.keyGroups[spec]?.eachCallback(channelId, stream, item, block)
|
||||
}
|
||||
|
||||
private fun eachCallbackForAcct(
|
||||
item: TimelineItem? = null,
|
||||
block: (callback: StreamCallback) -> Unit
|
||||
) {
|
||||
if (isDisposed.get()) return
|
||||
acctGroup.keyGroups.values.forEach {
|
||||
it.eachCallback(null, null, item, block)
|
||||
}
|
||||
}
|
||||
|
||||
private fun eachCallback(
|
||||
channelId: String? = null,
|
||||
stream:JsonArray? = null,
|
||||
item:TimelineItem?=null,
|
||||
stream: JsonArray? = null,
|
||||
item: TimelineItem? = null,
|
||||
block: (callback: StreamCallback) -> Unit
|
||||
) {
|
||||
if(StreamManager.traceDelivery) log.v("$name eachCallback spec=${spec?.name}")
|
||||
if (StreamManager.traceDelivery) log.v("$name eachCallback spec=${spec?.name}")
|
||||
if (spec != null) {
|
||||
eachCallbackForSpec(spec,channelId,stream,item,block)
|
||||
}else {
|
||||
eachCallbackForSpec(spec, channelId, stream, item, block)
|
||||
} else {
|
||||
if (isDisposed.get()) return
|
||||
acctGroup.keyGroups.values.forEach { it.eachCallback(channelId, stream, item, block) }
|
||||
}
|
||||
|
@ -97,15 +107,26 @@ class StreamConnection(
|
|||
socket.set(null)
|
||||
}
|
||||
|
||||
fun getStreamStatus(streamSpec: StreamSpec) :StreamStatus = when {
|
||||
fun getStreamStatus(streamSpec: StreamSpec): StreamStatus = when {
|
||||
subscriptions[streamSpec] != null -> StreamStatus.Subscribed
|
||||
else -> status
|
||||
}
|
||||
|
||||
private fun fireTimelineItem(item: TimelineItem?, channelId: String? = null,stream:JsonArray?=null) {
|
||||
item?:return
|
||||
if(StreamManager.traceDelivery) log.v("$name fireTimelineItem")
|
||||
eachCallback(channelId,stream,item=item) { it.onTimelineItem(item, channelId,stream) }
|
||||
private fun fireTimelineItem(
|
||||
item: TimelineItem?,
|
||||
channelId: String? = null,
|
||||
stream: JsonArray? = null
|
||||
) {
|
||||
item ?: return
|
||||
if (StreamManager.traceDelivery) log.v("$name fireTimelineItem")
|
||||
eachCallback(channelId, stream, item = item) { it.onTimelineItem(item, channelId, stream) }
|
||||
}
|
||||
|
||||
// fedibird emoji reaction noti
|
||||
private fun fireEmojiReaction(item: TootNotification) {
|
||||
item ?: return
|
||||
if (StreamManager.traceDelivery) log.v("$name fireTimelineItem")
|
||||
eachCallbackForAcct(){ it.onEmojiReaction(item)}
|
||||
}
|
||||
|
||||
private fun fireNoteUpdated(ev: MisskeyNoteUpdate, channelId: String? = null) {
|
||||
|
@ -118,8 +139,8 @@ class StreamConnection(
|
|||
manager.appState.columnList.forEach {
|
||||
runOnMainLooper {
|
||||
try {
|
||||
if(!it.is_dispose.get()) it.onStatusRemoved(tl_host, id)
|
||||
}catch(ex:Throwable) {
|
||||
if (!it.is_dispose.get()) it.onStatusRemoved(tl_host, id)
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
}
|
||||
|
@ -173,7 +194,7 @@ class StreamConnection(
|
|||
log.e("$name handleMisskeyMessage: noteUpdated body is null")
|
||||
return
|
||||
}
|
||||
fireNoteUpdated(MisskeyNoteUpdate(acctGroup.account.apDomain,body), channelId)
|
||||
fireNoteUpdated(MisskeyNoteUpdate(acctGroup.account.apDomain, body), channelId)
|
||||
}
|
||||
|
||||
"notification" -> {
|
||||
|
@ -229,13 +250,20 @@ class StreamConnection(
|
|||
|
||||
// {"event":"announcement.reaction","payload":"{\"name\":\"hourglass_gif\",\"count\":1,\"url\":\"https://m2j.zzz.ac/...\",\"static_url\":\"https://m2j.zzz.ac/...\",\"announcement_id\":\"9\"}"}
|
||||
"announcement.reaction" -> {
|
||||
if (payload is TootAnnouncement.Reaction) {
|
||||
if (payload is TootReaction) {
|
||||
eachCallback { it.onAnnouncementReaction(payload) }
|
||||
}
|
||||
}
|
||||
|
||||
else -> when (payload) {
|
||||
is TimelineItem -> fireTimelineItem(payload,stream=stream)
|
||||
is TimelineItem -> {
|
||||
|
||||
if (payload is TootNotification && payload.type == TootNotification.TYPE_EMOJI_REACTION) {
|
||||
fireEmojiReaction(payload)
|
||||
}
|
||||
|
||||
fireTimelineItem(payload, stream = stream)
|
||||
}
|
||||
else -> log.d("$name unsupported payload type. $payload")
|
||||
}
|
||||
}
|
||||
|
@ -256,7 +284,7 @@ class StreamConnection(
|
|||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
manager.enqueue {
|
||||
if(StreamManager.traceDelivery) log.v("$name WebSocket onMessage.")
|
||||
if (StreamManager.traceDelivery) log.v("$name WebSocket onMessage.")
|
||||
try {
|
||||
val obj = text.decodeJsonObject()
|
||||
when {
|
||||
|
@ -287,7 +315,7 @@ class StreamConnection(
|
|||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
manager.enqueue {
|
||||
if (t is SocketException && t.message?.contains("closed") ==true) {
|
||||
if (t is SocketException && t.message?.contains("closed") == true) {
|
||||
log.w("$name socket closed.")
|
||||
} else {
|
||||
log.e(t, "$name WebSocket onFailure.")
|
||||
|
@ -319,10 +347,10 @@ class StreamConnection(
|
|||
}
|
||||
}
|
||||
|
||||
private fun unsubscribe(spec:StreamSpec) {
|
||||
private fun unsubscribe(spec: StreamSpec) {
|
||||
try {
|
||||
subscriptions.remove(spec)
|
||||
eachCallbackForSpec(spec) { it.onStreamStatusChanged(getStreamStatus(spec) ) }
|
||||
eachCallbackForSpec(spec) { it.onStreamStatusChanged(getStreamStatus(spec)) }
|
||||
|
||||
val jsonObject = if (acctGroup.account.isMastodon) {
|
||||
/*
|
||||
|
@ -330,7 +358,7 @@ class StreamConnection(
|
|||
{ "stream": "hashtag:local", "tag": "foo" }
|
||||
等に後から "type": "unsubscribe" を足す
|
||||
*/
|
||||
spec.paramsClone().apply { put("type","unsubscribe") }
|
||||
spec.paramsClone().apply { put("type", "unsubscribe") }
|
||||
} else {
|
||||
/*
|
||||
Misskeyの場合
|
||||
|
@ -357,7 +385,7 @@ class StreamConnection(
|
|||
{ "stream": "hashtag:local", "tag": "foo" }
|
||||
等に後から "type": "subscribe" を足す
|
||||
*/
|
||||
spec.paramsClone().apply { put("type","subscribe") }
|
||||
spec.paramsClone().apply { put("type", "subscribe") }
|
||||
} else {
|
||||
/*
|
||||
Misskeyの場合
|
||||
|
@ -367,7 +395,7 @@ class StreamConnection(
|
|||
*/
|
||||
jsonObjectOf(
|
||||
"type" to "connect",
|
||||
"body" to spec.paramsClone().apply{ put("id",spec.channelId) }
|
||||
"body" to spec.paramsClone().apply { put("id", spec.channelId) }
|
||||
)
|
||||
}
|
||||
socket.get()?.send(jsonObject.toString())
|
||||
|
@ -411,7 +439,7 @@ class StreamConnection(
|
|||
return
|
||||
}
|
||||
|
||||
val group = spec?.let{ acctGroup.keyGroups[it] }
|
||||
val group = spec?.let { acctGroup.keyGroups[it] }
|
||||
if (group != null) {
|
||||
// 準備できたカラムがまったくないなら接続開始しない
|
||||
if (!group.destinations.values.any { it.canStartStreaming() }) return
|
||||
|
@ -475,7 +503,7 @@ class StreamConnection(
|
|||
val socket = socket.get()
|
||||
if (isDisposed.get() || socket == null) return
|
||||
|
||||
val type = when{
|
||||
val type = when {
|
||||
acctGroup.ti.versionGE(TootInstance.MISSKEY_VERSION_12_75_0) -> "sr"
|
||||
else -> "subNote"
|
||||
}
|
||||
|
|
|
@ -1105,7 +1105,8 @@ class SavedAccount(
|
|||
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> notification_follow_request
|
||||
|
||||
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION -> notification_reaction
|
||||
|
||||
TootNotification.TYPE_VOTE,
|
||||
|
|
Loading…
Reference in New Issue