キャプチャイベントを同じカラムに複数回分配してしまうバグを修正

This commit is contained in:
tateisu 2018-11-04 01:14:14 +09:00
parent 8da73791ca
commit 0bb7906ff7
5 changed files with 208 additions and 196 deletions

View File

@ -1139,62 +1139,6 @@ class Column(
}
fun onNoteUpdated( ev: MisskeyNoteUpdate ) {
runOnMainLooper {
if(is_dispose.get() ) return@runOnMainLooper
val changeList = ArrayList<AdapterChange>()
// userId が自分かどうか調べる
val myId = EntityId.from(access_info.token_info,TootApiClient.KEY_USER_ID)
val byMe = myId != null && myId == ev.userId
fun scanStatus1(s:TootStatus?,idx:Int,block:(s:TootStatus,byMe:Boolean)->Boolean){
s ?: return
if(s.id == ev.noteId){
if( block(s,byMe) ){
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx, 1))
}
}
scanStatus1(s.reblog,idx,block)
scanStatus1(s.reply,idx,block)
}
fun scanStatusAll(block:(s:TootStatus,byMe:Boolean)->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)
}
}
}
when(ev.type){
MisskeyNoteUpdate.Type.REACTION ->{
scanStatusAll{ s,byMe->
s.increaseReaction(ev.reaction,byMe)
}
}
MisskeyNoteUpdate.Type.VOTED ->{
scanStatusAll{ s,byMe->
s.enquete?.increaseVote(context,ev.choice,byMe) ?: false
}
}
MisskeyNoteUpdate.Type.DELETED ->{
scanStatusAll{ s,_->
s.markDeleted(context,ev.deletedAt) ?: false
}
}
}
if( changeList.isNotEmpty()){
fireShowContent(reason="onNoteUpdated",changeList = changeList)
}
}
}
fun removeNotifications() {
cancelLastTask()
@ -1730,7 +1674,7 @@ class Column(
add(n.accountRef)
add(n.status)
}
internal fun update(client : TootApiClient, parser : TootParser) {
var n : Int
@ -1782,8 +1726,8 @@ class Column(
val result =
client.request("/api/users/relation", params.toPostRequestBuilder())
if( result == null || result.response?.code() in 400 until 500 ) break
if(result == null || result.response?.code() in 400 until 500) break
val list = parseList(::TootRelationShip, parser, result.jsonArray)
if(list.size == userIdList.size) {
@ -6224,11 +6168,12 @@ class Column(
override fun onListeningStateChanged() {
if(is_dispose.get()) return
runOnMainLooper {
when{
is_dispose.get() ->{
when {
is_dispose.get() -> {
}
else-> {
else -> {
fireShowColumnStatus()
updateMisskeyCapture()
}
@ -6267,6 +6212,70 @@ class Column(
handler.post(mergeStreamingMessage)
}
override fun onNoteUpdated(ev : MisskeyNoteUpdate) {
// userId が自分かどうか調べる
// アクセストークンの更新をして自分のuserIdが分かる状態でないとキャプチャ結果を反映させない
// でないとリアクションの2重カウントなどが発生してしまう)
val myId = EntityId.from(access_info.token_info, TootApiClient.KEY_USER_ID)
if(myId == null) {
log.w("onNoteUpdated: missing my userId. updating access token is recommenced!!")
return
}
val byMe = myId == ev.userId
runOnMainLooper {
if(is_dispose.get()) return@runOnMainLooper
val changeList = ArrayList<AdapterChange>()
fun scanStatus1(s : TootStatus?, idx : Int, block : (s : TootStatus) -> Boolean) {
s ?: return
if(s.id == ev.noteId) {
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)
}
}
}
when(ev.type) {
MisskeyNoteUpdate.Type.REACTION -> {
scanStatusAll { s ->
s.increaseReaction(ev.reaction, byMe, "onNoteUpdated ${ev.userId}")
}
}
MisskeyNoteUpdate.Type.VOTED -> {
scanStatusAll { s ->
s.enquete?.increaseVote(context, ev.choice, byMe) ?: false
}
}
MisskeyNoteUpdate.Type.DELETED -> {
scanStatusAll { s ->
s.markDeleted(context, ev.deletedAt) ?: false
}
}
}
if(changeList.isNotEmpty()) {
fireShowContent(reason = "onNoteUpdated", changeList = changeList)
}
}
}
}
private fun resumeStreaming(bPutGap : Boolean) {
@ -6317,7 +6326,12 @@ class Column(
stream_data_queue.clear()
streamReader =app_state.stream_reader.register(access_info, stream_path, highlight_trie, streamCallback)
streamReader = app_state.stream_reader.register(
access_info,
stream_path,
highlight_trie,
streamCallback
)
fireShowColumnStatus()
}
@ -6515,32 +6529,32 @@ class Column(
}
}
private fun min(a:Int,b:Int):Int = if( a<b) a else b
private fun min(a : Int, b : Int) : Int = if(a < b) a else b
private fun updateMisskeyCapture(){
if(!isMisskey) return
streamReader?: return
private fun updateMisskeyCapture() {
if(! isMisskey) return
streamReader ?: return
val max = 40
val list = ArrayList<EntityId>(max*2) // リブログなどで膨れる場合がある
fun add(s:TootStatus?){
s?:return
list.add( s.id )
add( s.reblog)
add( s.reply)
val list = ArrayList<EntityId>(max * 2) // リブログなどで膨れる場合がある
fun add(s : TootStatus?) {
s ?: return
list.add(s.id)
add(s.reblog)
add(s.reply)
}
for(i in 0 until min( max, list_data.size)){
for(i in 0 until min(max, list_data.size)) {
val o = list_data[i]
if( o is TootStatus ){
if(o is TootStatus) {
add(o)
}else if( o is TootNotification){
} else if(o is TootNotification) {
add(o.status)
}
}
if( list.isNotEmpty() ) streamReader?.capture(list)
if(list.isNotEmpty()) streamReader?.capture(list)
}
private fun replaceConversationSummary(
@ -6778,8 +6792,6 @@ class Column(
}
}
// fun findListIndexByTimelineId(orderId : EntityId) : Int? {
// list_data.forEachIndexed { i, v ->
// if(v.getOrderId() == orderId) return i

View File

@ -1907,14 +1907,10 @@ internal class ItemViewHolder(
}
if((result.response?.code() ?: - 1) in 200 until 300) {
if(status.reactionCounts == null) {
status.reactionCounts = HashMap()
if(status.increaseReaction(code,true,"addReaction")){
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
list_adapter.notifyChange(reason = "addReaction complete", reset = true)
}
val count = status.reactionCounts?.get(code) ?: 0
status.reactionCounts?.put(code, count + 1)
status.myReaction = code
// 1個だけ描画更新するのではなく、TLにある複数の要素をまとめて更新する
list_adapter.notifyChange(reason = "addReaction complete", reset = true)
}
}
@ -2033,15 +2029,14 @@ internal class ItemViewHolder(
val data = result.jsonObject
if(data != null) {
if(accessInfo.isMisskey) {
enquete.myVoted = idx
val choice = enquete.items?.get(idx)
if(choice != null) choice.votes ++
if( enquete.increaseVote(activity,idx,true) ){
showToast(context, false, R.string.enquete_voted)
// 1個だけ開閉するのではなく、例えば通知TLにある複数の要素をまとめて開閉するなどある
list_adapter.notifyChange(reason = "onClickEnqueteChoice", reset = true)
}
// 204 no content
showToast(context, false, R.string.enquete_voted)
// 1個だけ開閉するのではなく、例えば通知TLにある複数の要素をまとめて開閉するなどある
list_adapter.notifyChange(reason = "onClickEnqueteChoice", reset = true)
} else {
val message = data.parseString("message") ?: "?"
val valid = data.optBoolean("valid")

View File

@ -26,6 +26,7 @@ internal class StreamReader(
internal interface StreamCallback {
fun onTimelineItem(item : TimelineItem)
fun onListeningStateChanged()
fun onNoteUpdated(ev:MisskeyNoteUpdate)
}
companion object {
@ -162,10 +163,9 @@ internal class StreamReader(
runOnMainLooper {
synchronized(this) {
if(bDisposed.get()) return@runOnMainLooper
val acct = access_info.acct
for(column in App1.getAppState(context).column_list) {
for(callback in callback_list) {
try {
if( column.access_info.acct == acct) column.onNoteUpdated(ev)
callback.onNoteUpdated(ev)
} catch(ex : Throwable) {
log.trace(ex)
}

View File

@ -254,44 +254,47 @@ class NicoEnquete(
fun increaseVote(context : Context, argChoice : Int?, isMyVoted : Boolean) : Boolean {
argChoice ?: return false
try {
// 既に投票済み状態なら何もしない
if(myVoted != null) return false
val item = this.items?.get(argChoice) ?: return false
item.votes += 1
if(isMyVoted) item.isVoted = true
// update ratios
val votesList = ArrayList<Int>()
var votesMax = 1
items.forEachIndexed { index, choice ->
if(choice.isVoted) this.myVoted = index
val votes = choice.votes
votesList.add(votes)
if(votes > votesMax) votesMax = votes
synchronized(this){
try {
// 既に投票済み状態なら何もしない
if(myVoted != null) return false
val item = this.items?.get(argChoice) ?: return false
item.votes += 1
if(isMyVoted) item.isVoted = true
// update ratios
val votesList = ArrayList<Int>()
var votesMax = 1
items.forEachIndexed { index, choice ->
if(choice.isVoted) this.myVoted = index
val votes = choice.votes
votesList.add(votes)
if(votes > votesMax) votesMax = votes
}
if(votesList.isNotEmpty()) {
this.ratios = votesList.asSequence()
.map { (it.toFloat() / votesMax.toFloat()) }
.toMutableList()
this.ratios_text = votesList.asSequence()
.map { context.getString(R.string.vote_count_text, it) }
.toMutableList()
} else {
this.ratios = null
this.ratios_text = null
}
return true
} catch(ex : Throwable) {
log.e(ex, "increaseVote failed")
return false
}
if(votesList.isNotEmpty()) {
this.ratios = votesList.asSequence()
.map { (it.toFloat() / votesMax.toFloat()) }
.toMutableList()
this.ratios_text = votesList.asSequence()
.map { context.getString(R.string.vote_count_text, it) }
.toMutableList()
} else {
this.ratios = null
this.ratios_text = null
}
return true
} catch(ex : Throwable) {
log.e(ex, "increaseVote failed")
return false
}
}

View File

@ -138,11 +138,11 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
var viaMobile : Boolean = false
var reactionCounts : HashMap<String, Int>? = null
var myReaction :String? =null
var myReaction : String? = null
var reply : TootStatus?
val serviceType :ServiceType
val serviceType : ServiceType
val deletedAt : String?
val time_deleted_at : Long
@ -171,7 +171,6 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
var conversationSummary : TootConversationSummary? = null
////////////////////////////////////////////////////////
init {
@ -184,10 +183,10 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
this.host_access = parser.linkHelper.host
val uri = src.parseString("uri")
if( uri != null ){
if(uri != null) {
this.uri = uri
this.url = uri
}else {
} else {
this.uri = "https://$instance/notes/$misskeyId"
this.url = "https://$instance/notes/$misskeyId"
}
@ -249,7 +248,6 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
this.viaMobile = src.optBoolean("viaMobile")
// this.decoded_tags = HTMLDecoder.decodeTags( account,status.tags );
// content
@ -272,7 +270,8 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
this.highlight_sound = options.highlight_sound
}
// Markdownのデコード結果からmentionsを読むのだった
this.mentions = (decoded_content as? MisskeyMarkdownDecoder.SpannableStringBuilderEx)?.mentions
this.mentions =
(decoded_content as? MisskeyMarkdownDecoder.SpannableStringBuilderEx)?.mentions
this.decoded_mentions = HTMLDecoder.decodeMentions(
parser.linkHelper,
this.mentions,
@ -317,8 +316,8 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
misskeyVisibleIds = null
reply = null
deletedAt = null
time_deleted_at =0L
time_deleted_at = 0L
this.uri = src.parseString("uri") // MSPだとuriは提供されない
this.url = src.parseString("url") // 頻繁にnullになる
this.created_at = src.parseString("created_at")
@ -463,7 +462,7 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
this.reblog = parser.status(src.optJSONObject("reblog"))
// 2.6.0からステータスにもカード情報が含まれる
this.card = parseItem(::TootCard,src.optJSONObject("card"))
this.card = parseItem(::TootCard, src.optJSONObject("card"))
}
}
@ -512,38 +511,35 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
val filtered : Boolean
get() = _filtered || reblog?._filtered == true
private fun hasReceipt(access_info:SavedAccount):TootVisibility{
private fun hasReceipt(access_info : SavedAccount) : TootVisibility {
val fullAcctMe = access_info.getFullAcct(account)
val reply_account = reply?.account
if( reply_account != null && fullAcctMe != access_info.getFullAcct(reply_account) ) {
if(reply_account != null && fullAcctMe != access_info.getFullAcct(reply_account)) {
return TootVisibility.DirectSpecified
}
val in_reply_to_account_id = this.in_reply_to_account_id
if( in_reply_to_account_id != null && in_reply_to_account_id != account.id) {
if(in_reply_to_account_id != null && in_reply_to_account_id != account.id) {
return TootVisibility.DirectSpecified
}
mentions?.forEach{
mentions?.forEach {
if(fullAcctMe != access_info.getFullAcct(it.acct))
return@hasReceipt TootVisibility.DirectSpecified
}
return TootVisibility.DirectPrivate
}
fun getBackgroundColorType(access_info:SavedAccount) =
when(visibility){
fun getBackgroundColorType(access_info : SavedAccount) =
when(visibility) {
TootVisibility.DirectPrivate,
TootVisibility.DirectSpecified -> hasReceipt(access_info)
else-> visibility
else -> visibility
}
fun updateFiltered(muted_words : WordTrieTree?) {
_filtered = checkFiltered(muted_words)
reblog?.updateFiltered(muted_words)
@ -561,53 +557,55 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
return false
}
fun hasAnyContent() =when{
fun hasAnyContent() = when {
reblog == null -> true // reblog以外はオリジナルコンテンツがあると見なす
serviceType != ServiceType.MISSKEY -> false // misskey以外のreblogはコンテンツがないと見なす
content?.isNotEmpty()== true
|| spoiler_text?.isNotEmpty()== true
|| media_attachments?.isNotEmpty()== true
content?.isNotEmpty() == true
|| spoiler_text?.isNotEmpty() == true
|| media_attachments?.isNotEmpty() == true
|| enquete != null -> true
else-> false
else -> false
}
// return true if updated
fun increaseReaction(reaction : String?,byMe:Boolean):Boolean {
reaction?: return false
fun increaseReaction(reaction : String?, byMe : Boolean,caller:String) : Boolean {
reaction ?: return false
MisskeyReaction.shortcodeMap[reaction] ?: return false
if(byMe) {
if(myReaction != null){
// 自分でリアクションしたらUIで更新した後にストリーミングイベントが届くことがある
return false
}else{
// 別クライアントから更新したのだろう
synchronized(this) {
if(byMe) {
if(myReaction != null) {
// 自分でリアクションしたらUIで更新した後にストリーミングイベントが届くことがある
return false
}
myReaction = reaction
}
log.d("increaseReaction noteId=$id byMe=$byMe caller=$caller")
// カウントを増やす
var map = this.reactionCounts
if(map == null) {
map = HashMap()
this.reactionCounts = map
}
map[reaction] = (map[reaction] ?: 0) + 1
return true
}
// カウントを増やす
var map = this.reactionCounts
if(map==null) {
map = HashMap()
this.reactionCounts = map
}
val v = map[ reaction ]
map[ reaction ] = (v?:0) +1
return true
}
fun markDeleted(context : Context, deletedAt: Long? ) : Boolean? {
fun markDeleted(context : Context, deletedAt : Long?) : Boolean? {
var sv = if(deletedAt != null) {
context.getString(R.string.status_deleted_at, formatTime(context,deletedAt,false))
}else{
context.getString(R.string.status_deleted_at, formatTime(context, deletedAt, false))
} else {
context.getString(R.string.status_deleted)
}
this.content = sv
this.decoded_content = SpannableString(sv)
sv = ""
this.spoiler_text = sv
this.decoded_spoiler_text = SpannableString(sv)
@ -835,26 +833,30 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
return if(host != null && host.isNotEmpty() && host != "?") host else null
}
private val reMisskeyNoteUrl = Pattern.compile("""https://([^/]+)/notes/([0-9A-F]+)""",Pattern.CASE_INSENSITIVE)
private val reMisskeyNoteUrl =
Pattern.compile("""https://([^/]+)/notes/([0-9A-F]+)""", Pattern.CASE_INSENSITIVE)
fun readMisskeyNoteId(url:String):EntityId?{
fun readMisskeyNoteId(url : String) : EntityId? {
// https://misskey.xyz/notes/5b802367744b650030a13640
val m = reMisskeyNoteUrl.matcher(url)
if(m.find() ) return EntityIdString(m.group(2))
if(m.find()) return EntityIdString(m.group(2))
return null
}
fun validStatusId(src:EntityId?):EntityId?{
return when{
fun validStatusId(src : EntityId?) : EntityId? {
return when {
src == null -> null
src is EntityIdLong && src.toLong() == TootStatus.INVALID_ID ->null
else ->src
src is EntityIdLong && src.toLong() == TootStatus.INVALID_ID -> null
else -> src
}
}
// 投稿元タンスでのステータスIDを調べる
fun findStatusIdFromUri(uri : String?, url : String?, bAllowStringId:Boolean =false) : EntityId? {
fun findStatusIdFromUri(
uri : String?,
url : String?,
bAllowStringId : Boolean = false
) : EntityId? {
// pleromaのuriやURL からはステータスIDは取れません
// uri https://pleroma.miniwa.moe/objects/d6e83d3c-cf9e-46ac-8245-f91716088e17
@ -874,9 +876,9 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
m = reTootUriAP2.matcher(uri)
if(m.find()) return EntityIdLong(m.group(2).toLong(10))
if(bAllowStringId){
if(bAllowStringId) {
val id = readMisskeyNoteId(uri)
if(id!=null) return id
if(id != null) return id
}
log.w("can't parse status uri: $uri")
@ -892,9 +894,9 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
m = reTootUriAP2.matcher(url)
if(m.find()) return EntityIdLong(m.group(2).toLong(10))
if(bAllowStringId){
if(bAllowStringId) {
val id = readMisskeyNoteId(url)
if(id!=null) return id
if(id != null) return id
}
log.w("can't parse status URL: $url")