投票結果通知に対応。(サーバ側の問題によりプッシュ通知は届きません)

エクスポートを介してクライアントアプリ登録を端末間で再利用するとアクセストークンを更新しても同じ値が返ってきてプッシュ購読がうまくない問題の対応。
This commit is contained in:
tateisu 2019-03-14 02:34:56 +09:00
parent 2485e53287
commit d9487d0e78
21 changed files with 237 additions and 207 deletions

View File

@ -1224,10 +1224,9 @@ class TestTootApiClient {
callback = callback
)
client.account = accessInfo
val result = client.webSocket("/api/v1/streaming/?stream=public:local",
val(_,ws) = client.webSocket("/api/v1/streaming/?stream=public:local",
object : WebSocketListener() {
})
val ws = result?.data as? WebSocket
assertNotNull(ws)
ws?.cancel()
}

View File

@ -669,7 +669,10 @@ class ActAccountSetting
TootTaskRunner(this@ActAccountSetting).run(account, object : TootTask {
override fun background(client : TootApiClient) : TootApiResult? {
return client.authentication1(Pref.spClientName(this@ActAccountSetting))
return client.authentication1(
Pref.spClientName(this@ActAccountSetting),
forceUpdateClient = false
)
}
override fun handleResult(result : TootApiResult?) {

View File

@ -509,9 +509,9 @@ class ActMain : AppCompatActivity()
MyClickableSpan.showLinkUnderline = Pref.bpShowLinkUnderline(pref)
MyClickableSpan.defaultLinkColor = Pref.ipLinkColor(pref).notZero()
?: getAttributeColor(this, R.attr.colorLink)
// 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす
for( column in app_state.column_list){
for(column in app_state.column_list) {
column.fireColumnColor()
}
@ -1439,9 +1439,13 @@ class ActMain : AppCompatActivity()
env.tablet_pager.adapter = env.tablet_pager_adapter
env.tablet_pager.layoutManager = env.tablet_layout_manager
env.tablet_pager.addOnScrollListener(object : androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
env.tablet_pager.addOnScrollListener(object :
androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView : androidx.recyclerview.widget.RecyclerView, newState : Int) {
override fun onScrollStateChanged(
recyclerView : androidx.recyclerview.widget.RecyclerView,
newState : Int
) {
super.onScrollStateChanged(recyclerView, newState)
val vs = env.tablet_layout_manager.findFirstVisibleItemPosition()
@ -1456,7 +1460,11 @@ class ActMain : AppCompatActivity()
}
}
override fun onScrolled(recyclerView : androidx.recyclerview.widget.RecyclerView, dx : Int, dy : Int) {
override fun onScrolled(
recyclerView : androidx.recyclerview.widget.RecyclerView,
dx : Int,
dy : Int
) {
super.onScrolled(recyclerView, dx, dy)
updateColumnStripSelection(- 1, - 1f)
}
@ -1839,7 +1847,7 @@ class ActMain : AppCompatActivity()
}
// OAuth2 認証コールバック
// subwaytooter://oauth/?...
// subwaytooter://oauth(\d*)/?...
TootTaskRunner(this@ActMain).run(object : TootTask {
var ta : TootAccount? = null
@ -1892,7 +1900,7 @@ class ActMain : AppCompatActivity()
// Mastodon 認証コールバック
// エラー時
// subwaytooter://oauth
// subwaytooter://oauth(\d*)/
// ?error=access_denied
// &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82
// &state=db%3A3
@ -1901,7 +1909,7 @@ class ActMain : AppCompatActivity()
return TootApiResult(error)
}
// subwaytooter://oauth
// subwaytooter://oauth(\d*)/
// ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
// &state=host%3Amastodon.juggler.jp
@ -1915,25 +1923,33 @@ class ActMain : AppCompatActivity()
return TootApiResult("missing state in callback url.")
}
if(sv.startsWith("db:")) {
try {
val dataId = sv.substring(3).toLong(10)
val sa = SavedAccount.loadAccount(this@ActMain, dataId)
?: return TootApiResult("missing account db_id=$dataId")
this.sa = sa
client.account = sa
} catch(ex : Throwable) {
log.trace(ex)
return TootApiResult(ex.withCaption("invalid state"))
for( param in sv.split(",")){
when {
param.startsWith("db:") -> try {
val dataId = param.substring(3).toLong(10)
val sa = SavedAccount.loadAccount(this@ActMain, dataId)
?: return TootApiResult("missing account db_id=$dataId")
this.sa = sa
client.account = sa
} catch(ex : Throwable) {
log.trace(ex)
return TootApiResult(ex.withCaption("invalid state"))
}
param.startsWith("host:") -> {
val host = param.substring(5)
client.instance = host
}
else -> {
// ignore other parameter
}
}
} else if(sv.startsWith("host:")) {
val host = sv.substring(5)
client.instance = host
}
val instance = client.instance
?: return TootApiResult("missing instance in callback url.")
?: return TootApiResult("missing instance in callback url.")
this.host = instance
val client_name = Pref.spClientName(this@ActMain)

View File

@ -388,9 +388,11 @@ object AppDataExporter {
writeFromTable(writer, KEY_MUTED_APP, MutedApp.table)
writeFromTable(writer, KEY_MUTED_WORD, MutedWord.table)
writeFromTable(writer, KEY_FAV_MUTE, FavMute.table)
writeFromTable(writer, KEY_CLIENT_INFO, ClientInfo.table)
writeFromTable(writer, KEY_HIGHLIGHT_WORD, HighlightWord.table)
// 端末間でクライアントIDを再利用することはできなくなった
//writeFromTable(writer, KEY_CLIENT_INFO, ClientInfo.table)
//////////////////////////////////////
run {
writer.name(KEY_COLUMN)
@ -428,8 +430,12 @@ object AppDataExporter {
KEY_MUTED_WORD -> importTable(reader, MutedWord.table, null)
KEY_FAV_MUTE -> importTable(reader, FavMute.table, null)
KEY_HIGHLIGHT_WORD -> importTable(reader, HighlightWord.table, null)
KEY_CLIENT_INFO -> importTable(reader, ClientInfo.table, null)
KEY_COLUMN -> result = readColumn(app_state, reader, account_id_map)
// 端末間でクライアントIDを再利用することはできなくなった
// KEY_CLIENT_INFO -> importTable(reader, ClientInfo.table, null)
else-> reader.skipValue()
}
}

View File

@ -1101,14 +1101,14 @@ class Column(
if(n ++ > 0) sb.append(", ")
sb.append(context.getString(R.string.notification_type_reaction))
}
if(isMisskey && ! dont_show_vote) {
if(! dont_show_vote) {
if(n ++ > 0) sb.append(", ")
sb.append(context.getString(R.string.notification_type_vote))
}
val n_max = if(isMisskey) {
6
} else {
4
5
}
if(n == 0 || n == n_max) return "" // 全部か皆無なら部分表記は要らない
}
@ -1638,79 +1638,44 @@ class Column(
private fun isFiltered(item : TootNotification) : Boolean {
when(quick_filter) {
QUICK_FILTER_ALL -> {
when(item.type) {
TootNotification.TYPE_FAVOURITE -> if(dont_show_favourite) {
log.d("isFiltered: favourite notification filtered.")
return true
}
if(when(quick_filter) {
QUICK_FILTER_ALL -> when(item.type) {
TootNotification.TYPE_FAVOURITE -> dont_show_favourite
TootNotification.TYPE_REBLOG,
TootNotification.TYPE_RENOTE,
TootNotification.TYPE_QUOTE -> if(dont_show_boost) {
log.d("isFiltered: reblog notification filtered.")
return true
}
TootNotification.TYPE_QUOTE -> dont_show_boost
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW -> if(dont_show_follow) {
log.d("isFiltered: follow notification filtered.")
return true
}
TootNotification.TYPE_FOLLOW -> dont_show_follow
TootNotification.TYPE_MENTION,
TootNotification.TYPE_REPLY -> if(dont_show_reply) {
log.d("isFiltered: mention notification filtered.")
return true
}
TootNotification.TYPE_REACTION -> if(dont_show_reaction) {
log.d("isFiltered: reaction notification filtered.")
return true
}
TootNotification.TYPE_VOTE -> if(dont_show_vote) {
log.d("isFiltered: vote notification filtered.")
return true
}
TootNotification.TYPE_REPLY -> dont_show_reply
TootNotification.TYPE_REACTION -> dont_show_reaction
TootNotification.TYPE_VOTE,
TootNotification.TYPE_POLL -> dont_show_vote
else -> false
}
}
else -> {
when(item.type) {
TootNotification.TYPE_FAVOURITE -> if(quick_filter != QUICK_FILTER_FAVOURITE) {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
else -> when(item.type) {
TootNotification.TYPE_FAVOURITE -> quick_filter != QUICK_FILTER_FAVOURITE
TootNotification.TYPE_REBLOG,
TootNotification.TYPE_RENOTE,
TootNotification.TYPE_QUOTE -> if(quick_filter != QUICK_FILTER_BOOST) {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
TootNotification.TYPE_QUOTE -> quick_filter != QUICK_FILTER_BOOST
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW -> if(quick_filter != QUICK_FILTER_FOLLOW) {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
TootNotification.TYPE_FOLLOW -> quick_filter != QUICK_FILTER_FOLLOW
TootNotification.TYPE_MENTION,
TootNotification.TYPE_REPLY -> if(quick_filter != QUICK_FILTER_MENTION) {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
TootNotification.TYPE_REACTION -> if(quick_filter != QUICK_FILTER_REACTION) {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
TootNotification.TYPE_VOTE -> if(quick_filter != QUICK_FILTER_VOTE) {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
else -> {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
TootNotification.TYPE_REPLY -> quick_filter != QUICK_FILTER_MENTION
TootNotification.TYPE_REACTION -> quick_filter != QUICK_FILTER_REACTION
TootNotification.TYPE_VOTE,
TootNotification.TYPE_POLL -> quick_filter != QUICK_FILTER_VOTE
else -> true
}
}
}) {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
val status = item.status
@ -6985,6 +6950,7 @@ class Column(
if(dont_show_boost) sb.append("&exclude_types[]=reblog")
if(dont_show_follow) sb.append("&exclude_types[]=follow")
if(dont_show_reply) sb.append("&exclude_types[]=mention")
if(dont_show_vote) sb.append("&exclude_types[]=poll")
}
else -> {

View File

@ -658,7 +658,7 @@ class ColumnViewHolder(
vg(cbDontShowReply, column.canFilterReply())
vg(cbDontShowNormalToot, column.canFilterNormalToot())
vg(cbDontShowReaction, isNotificationColumn && column.isMisskey)
vg(cbDontShowVote, isNotificationColumn && column.isMisskey)
vg(cbDontShowVote, isNotificationColumn )
vg(cbDontShowFavourite, isNotificationColumn && ! column.isMisskey)
vg(cbDontShowFollow, isNotificationColumn)
@ -1525,8 +1525,6 @@ class ColumnViewHolder(
if(! isNotificationColumn) return
vg(btnQuickFilterReaction, column.isMisskey)
vg(btnQuickFilterVote, column.isMisskey)
vg(btnQuickFilterFavourite, ! column.isMisskey)
val insideColumnSetting = Pref.bpMoveNotificationsQuickFilter(activity.pref)

View File

@ -930,6 +930,19 @@ internal class ItemViewHolder(
}
}
TootNotification.TYPE_POLL -> {
val colorBg = 0
if(n_account != null) showBoost(
n_accountRef,
n.time_created_at,
R.drawable.ic_vote,
R.string.end_of_polling_from
)
if(n_status != null) {
showNotificationStatus(n_status, colorBg)
}
}
else -> {
val colorBg = 0
if(n_account != null) showBoost(

View File

@ -1303,6 +1303,9 @@ class PollingWorker private constructor(contextArg : Context) {
name
)
TootNotification.TYPE_POLL ->
"- " + context.getString(R.string.end_of_polling_from, name)
else -> "- " + "?"
}
}

View File

@ -2,6 +2,7 @@ package jp.juggler.subwaytooter.api
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.ClientInfo
@ -54,7 +55,9 @@ class TootApiClient(
private const val REDIRECT_URL = "subwaytooter://oauth/"
// 20181225 3=>4 client credentialの取得時にもscopeの取得が必要になった
private const val AUTH_VERSION = 4
// 20190147 4=>5 client id とユーザIDが同じだと同じアクセストークンが返ってくるので複数端末の利用で困る。
// AUTH_VERSIONが古いclient情報は使わない。また、インポートの対象にしない。
private const val AUTH_VERSION = 5
internal const val KEY_CLIENT_CREDENTIAL = "SubwayTooterClientCredential"
internal const val KEY_CLIENT_SCOPE = "SubwayTooterClientScope"
@ -212,11 +215,11 @@ class TootApiClient(
"reaction-write",
"vote-read",
"vote-write"
)
// APIのエラーを回避するため、重複を排除する
.toMutableSet()
.forEach {put(it) }
.forEach { put(it) }
}
private fun encodeScopeArray(scope_array : JSONArray?) : String? {
@ -539,8 +542,8 @@ class TootApiClient(
}
// インスタンス情報を取得する
internal fun parseInstanceInformation(result : TootApiResult?) : Pair<TootApiResult?,TootInstance?> {
var ti: TootInstance? = null
internal fun parseInstanceInformation(result : TootApiResult?) : Pair<TootApiResult?, TootInstance?> {
var ti : TootInstance? = null
val json = result?.jsonObject
if(json != null) {
val parser = TootParser(
@ -550,7 +553,7 @@ class TootApiClient(
ti = parser.instance(json)
if(ti == null) result.setError("can't parse data in instance information.")
}
return Pair(result,ti)
return Pair(result, ti)
}
private fun getAppInfoMisskey(appId : String?) : TootApiResult? {
@ -912,13 +915,19 @@ class TootApiClient(
val account = this.account
val client_id = client_info.parseString("client_id") ?: return null
val state = StringBuilder()
.append((if(account != null) "db:${account.db_id}" else "host:$instance"))
.append(',')
.append("random:${System.currentTimeMillis()}")
.toString()
return ("https://" + instance + "/oauth/authorize"
+ "?client_id=" + client_id.encodePercent()
+ "&response_type=code"
+ "&redirect_uri=" + REDIRECT_URL.encodePercent()
+ "&scope=$scope_string"
+ "&scopes=$scope_string"
+ "&state=" + (if(account != null) "db:${account.db_id}" else "host:$instance")
+ "&state=" + state.encodePercent()
+ "&grant_type=authorization_code"
+ "&approval_prompt=force"
+ "&force_login=true"
@ -926,7 +935,11 @@ class TootApiClient(
)
}
private fun prepareClientMastodon(clientNameArg : String, ti : TootInstance) : TootApiResult? {
private fun prepareClientMastodon(
clientNameArg : String,
ti : TootInstance,
forceUpdateClient : Boolean = false
) : TootApiResult? {
// 前準備
val result = TootApiResult.makeWithCaption(this.instance)
if(result.error != null) return result
@ -937,56 +950,53 @@ class TootApiClient(
var client_info = ClientInfo.load(instance, client_name)
// スコープ一覧を取得する
val scope_string = getScopeString(ti)
if(client_info != null
&& AUTH_VERSION == client_info.optInt(KEY_AUTH_VERSION)
&& ! client_info.optBoolean(KEY_IS_MISSKEY)
) {
var client_credential = client_info.parseString(KEY_CLIENT_CREDENTIAL)
val old_scope = client_info.parseString(KEY_CLIENT_SCOPE)
// client_credential をまだ取得していないなら取得する
if(client_credential?.isEmpty() != false) {
val resultSub = getClientCredential(client_info)
client_credential = resultSub?.string
if(client_credential?.isNotEmpty() == true) {
try {
client_info.put(KEY_CLIENT_CREDENTIAL, client_credential)
ClientInfo.save(instance, client_name, client_info.toString())
} catch(ignored : JSONException) {
}
}
when {
AUTH_VERSION != client_info?.optInt(KEY_AUTH_VERSION) -> {
// 古いクライアント情報は使わない。削除もしない。
}
// client_credential があるならcredentialがまだ使えるか確認する
if(client_credential?.isNotEmpty() == true) {
val resultSub = verifyClientCredential(client_credential)
val currentCC = resultSub?.jsonObject
if(currentCC != null) {
var allowReuseCC = true
if(old_scope != scope_string) {
// マストドン2.4でスコープが追加された
// 取得時のスコープ指定がマッチしない(もしくは記録されていない)ならクライアント情報を再利用してはいけない
ClientInfo.delete(instance, client_name)
// client credential をタンスから消去する
revokeClientCredential(client_info, client_credential)
// XXX クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
allowReuseCC = false
client_info.optBoolean(KEY_IS_MISSKEY) -> {
// Misskeyにはclient情報をまだ利用できるかどうか調べる手段がないので、再利用しない
}
else -> {
val old_scope = client_info.parseString(KEY_CLIENT_SCOPE)
// client_credential をまだ取得していないなら取得する
var client_credential = client_info.parseString(KEY_CLIENT_CREDENTIAL)
if(client_credential?.isEmpty() != false) {
val resultSub = getClientCredential(client_info)
client_credential = resultSub?.string
if(client_credential?.isNotEmpty() == true) {
try {
client_info.put(KEY_CLIENT_CREDENTIAL, client_credential)
ClientInfo.save(instance, client_name, client_info.toString())
} catch(ignored : JSONException) {
}
}
if(allowReuseCC) {
// クライアント情報を再利用する
result.data = client_info
return result
}
// client_credential があるならcredentialがまだ使えるか確認する
if(client_credential?.isNotEmpty() == true) {
val resultSub = verifyClientCredential(client_credential)
val currentCC = resultSub?.jsonObject
if(currentCC != null) {
if(old_scope != scope_string || forceUpdateClient) {
// マストドン2.4でスコープが追加された
// 取得時のスコープ指定がマッチしない(もしくは記録されていない)ならクライアント情報を再利用してはいけない
ClientInfo.delete(instance, client_name)
// client credential をタンスから消去する
revokeClientCredential(client_info, client_credential)
// XXX クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
} else {
// クライアント情報を再利用する
result.data = client_info
return result
}
}
}
}
@ -1021,9 +1031,10 @@ class TootApiClient(
private fun authentication1Mastodon(
clientNameArg : String,
ti : TootInstance
ti : TootInstance,
forceUpdateClient : Boolean = false
) : TootApiResult? =
prepareClientMastodon(clientNameArg, ti)?.also { result ->
prepareClientMastodon(clientNameArg, ti, forceUpdateClient)?.also { result ->
val client_info = result.jsonObject
if(client_info != null) {
result.data = prepareBrowserUrl(getScopeString(ti), client_info)
@ -1044,13 +1055,16 @@ class TootApiClient(
}
// クライアントを登録してブラウザで開くURLを生成する
fun authentication1(clientNameArg : String) : TootApiResult? {
fun authentication1(
clientNameArg : String,
forceUpdateClient : Boolean = false
) : TootApiResult? {
var lastRi : TootApiResult?
// misskeyのインスタンス情報
run{
val (ri ,ti) = parseInstanceInformation(getInstanceInformationMisskey())
run {
val (ri, ti) = parseInstanceInformation(getInstanceInformationMisskey())
lastRi = ri
if(ti != null && (ri?.response?.code() ?: 0) in 200 until 300) {
return authentication1Misskey(clientNameArg, ti)
@ -1058,11 +1072,11 @@ class TootApiClient(
}
// マストドンのインスタンス情報
run{
val (ri,ti) = parseInstanceInformation(getInstanceInformationMastodon())
run {
val (ri, ti) = parseInstanceInformation(getInstanceInformationMastodon())
lastRi = ri
if(ti != null && (ri?.response?.code() ?: 0) in 200 until 300) {
return authentication1Mastodon(clientNameArg, ti)
return authentication1Mastodon(clientNameArg, ti, forceUpdateClient)
}
}
@ -1170,9 +1184,9 @@ class TootApiClient(
}
fun createUser1(clientNameArg : String) : TootApiResult? {
var lastRi : TootApiResult?
// misskeyのインスタンス情報
run {
val (ri, ti) = parseInstanceInformation(getInstanceInformationMisskey())
@ -1195,7 +1209,7 @@ class TootApiClient(
return prepareClientMastodon(clientNameArg, ti)
}
}
return lastRi
}
@ -1360,24 +1374,27 @@ class TootApiClient(
return result
}
fun getHttpBytes(url : String) :Pair<TootApiResult?,ByteArray?> {
fun getHttpBytes(url : String) : Pair<TootApiResult?, ByteArray?> {
val result = TootApiResult.makeWithCaption(url)
if(result.error != null) return Pair(result,null)
if(result.error != null) return Pair(result, null)
if( !sendRequest(result, progressPath = url) {
if(! sendRequest(result, progressPath = url) {
Request.Builder().url(url).build()
}) {
return Pair(result, null)
}
val r2 = parseBytes(result)
return Pair(r2,r2?.data as? ByteArray)
return Pair(r2, r2?.data as? ByteArray)
}
fun webSocket(path : String, ws_listener : WebSocketListener) : Pair<TootApiResult?,WebSocket?> {
var ws:WebSocket? = null
fun webSocket(
path : String,
ws_listener : WebSocketListener
) : Pair<TootApiResult?, WebSocket?> {
var ws : WebSocket? = null
val result = TootApiResult.makeWithCaption(instance)
if(result.error != null) return Pair(result,null)
val account = this.account ?: return Pair(TootApiResult("account is null"),null)
if(result.error != null) return Pair(result, null)
val account = this.account ?: return Pair(TootApiResult("account is null"), null)
try {
var url = "wss://$instance$path"
@ -1394,13 +1411,14 @@ class TootApiClient(
ws = httpClient.getWebSocket(request, ws_listener)
if(isApiCancelled) {
ws.cancel()
return Pair(null,null)
return Pair(null, null)
}
} catch(ex : Throwable) {
log.trace(ex)
result.error = "${result.caption}: ${ex.withCaption(context.resources, R.string.network_error)}"
result.error =
"${result.caption}: ${ex.withCaption(context.resources, R.string.network_error)}"
}
return Pair(result,ws)
return Pair(result, ws)
}
@ -1504,7 +1522,10 @@ fun TootApiClient.syncAccountByAcct(
return Pair(result, ar)
}
fun TootApiClient.syncStatus(accessInfo : SavedAccount, urlArg : String) : Pair<TootApiResult?,TootStatus?> {
fun TootApiClient.syncStatus(
accessInfo : SavedAccount,
urlArg : String
) : Pair<TootApiResult?, TootStatus?> {
var url = urlArg
@ -1532,18 +1553,18 @@ fun TootApiClient.syncStatus(accessInfo : SavedAccount, urlArg : String) : Pair<
.status(result.jsonObject)
?.apply {
if(host.equals(accessInfo.host, ignoreCase = true)) {
return Pair(result,this)
return Pair(result, this)
}
uri.letNotEmpty { url = it }
}
}
?: return Pair(null,null) // cancelled.
?: return Pair(null, null) // cancelled.
}
// 使いたいタンス上の投稿IDを取得する
val parser = TootParser(context, accessInfo)
var targetStatus :TootStatus? = null
var targetStatus : TootStatus? = null
val result = if(accessInfo.isMisskey) {
request(
"/api/ap/show",
@ -1566,13 +1587,13 @@ fun TootApiClient.syncStatus(accessInfo : SavedAccount, urlArg : String) : Pair<
}
}
}
return Pair(result,targetStatus)
return Pair(result, targetStatus)
}
fun TootApiClient.syncStatus(
accessInfo : SavedAccount,
statusRemote : TootStatus
) : Pair<TootApiResult?,TootStatus?> {
) : Pair<TootApiResult?, TootStatus?> {
// URL->URIの順に試す
@ -1600,10 +1621,10 @@ fun TootApiClient.syncStatus(
for(uri in uriList) {
val pair = syncStatus(accessInfo, uri)
if( pair.second != null || pair.first == null ) {
if(pair.second != null || pair.first == null) {
return pair
}
}
return Pair(TootApiResult("can't resolve status URL/URI."),null)
return Pair(TootApiResult("can't resolve status URL/URI."), null)
}

View File

@ -128,23 +128,25 @@ class NicoEnquete(
this.votes_count = src.parseInt("votes_count")
this.myVoted = if(src.optBoolean("voted", false)) 1 else null
if(this.items == null) {
maxVotesCount = null
} else if(this.multiple){
var max :Int? = null
for( item in items){
val v = item.votes
if( v != null && (max == null || v > max) ) max =v
when {
this.items == null -> maxVotesCount = null
this.multiple -> {
var max :Int? = null
for( item in items){
val v = item.votes
if( v != null && (max == null || v > max) ) max =v
}
maxVotesCount = max
}
maxVotesCount = max
} else {
var sum :Int?= null
for( item in items){
val v = item.votes
if( v != null ) sum = (sum?:0) + v
else -> {
var sum :Int?= null
for( item in items){
val v = item.votes
if( v != null ) sum = (sum?:0) + v
}
maxVotesCount = sum
}
maxVotesCount = sum
}
} else {

View File

@ -29,7 +29,9 @@ class TootNotification(parser : TootParser, src : JSONObject) : TimelineItem() {
// 投票
const val TYPE_VOTE = "poll_vote"
const val TYPE_FOLLOW_REQUEST = "receiveFollowRequest"
// (Mastodon 2.8)投票完了
const val TYPE_POLL = "poll"
}
val json : JSONObject

View File

@ -918,7 +918,8 @@ class SavedAccount(
TootNotification.TYPE_REACTION -> notification_reaction
TootNotification.TYPE_VOTE -> notification_vote
TootNotification.TYPE_VOTE,TootNotification.TYPE_POLL -> notification_vote
else -> false
}

View File

@ -48,7 +48,7 @@ class PushSubscriptionHelper(
if(account.notification_follow) n += 4
if(account.notification_mention) n += 8
if(account.isMisskey && account.notification_reaction) n += 16
if(account.isMisskey && account.notification_vote) n += 32
if(account.notification_vote) n += 32
this.flags = n
}
@ -365,6 +365,7 @@ class PushSubscriptionHelper(
put("favourite", account.notification_favourite)
put("reblog", account.notification_boost)
put("mention", account.notification_mention)
put("poll", account.notification_vote)
})
})
}

View File

@ -610,7 +610,7 @@
<CheckBox
android:id="@+id/cbNotificationVote"
style="@style/setting_horizontal_stretch"
android:text="@string/vote_misskey"
android:text="@string/vote_polls"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">

View File

@ -415,7 +415,7 @@
<TextView
style="@style/setting_row_label_indent1"
android:text="@string/vote_misskey"
android:text="@string/vote_polls"
/>
<LinearLayout style="@style/setting_row_form">

View File

@ -565,7 +565,7 @@
android:layout_height="match_parent"
android:layout_margin="0dp"
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/vote_misskey"
android:contentDescription="@string/vote_polls"
/>
</LinearLayout>
</HorizontalScrollView>

View File

@ -748,7 +748,6 @@
<string name="follow_request_cancelled">Follow request was cancelled.</string>
<string name="confirm_cancel_follow_request_who_from">La demande de suivi de %2$s à %1$s sera rejetée. Vous êtes sûr ?</string>
<string name="reaction">Réaction (Misskey)</string>
<string name="vote_misskey">Vote (Misskey)</string>
<string name="timeout_for_embed_media_viewer">Timeout for embed media viewer (unit:seconds, app restart(delete from app history) required)</string>
<string name="media_attachment_max_byte_size_movie">Taille maximale en octets des médias vidéo (unité : méga octets. par défaut : 40)</string>
<string name="link_color">Couleur des liens (redémarrage requis)</string>

View File

@ -749,7 +749,7 @@
<string name="visibility_local_followers">フォロワー (ローカル)</string>
<string name="visibility_local_unlisted">未収載 (ローカル)</string>
<string name="vote_misskey">投票 (Misskey)</string>
<string name="vote_polls">投票やその結果</string>
<string name="vote_count_text">%1$d票</string>
<string name="wait_previous_operation">直前の操作が完了するまでお待ちください</string>
<string name="with_attachment">添付データあり</string>
@ -880,9 +880,9 @@
<string name="poll_expire_hours">時間</string>
<string name="poll_expire_minutes"></string>
<string name="vote_1">1 vote</string>
<string name="vote_2">%1$d votes</string>
<string name="vote_2">%1$d 人。</string>
<string name="vote_expire_at">投票期限 %1$s</string>
<string name="vote_count_unavailable">\?\?\?票</string>
<string name="vote_button">投票</string>
<string name="end_of_polling_from">%1$sの調査の終了</string>
</resources>

View File

@ -769,7 +769,6 @@
<string name="follow_request_cancelled">팔로우 요청 취소됨.</string>
<string name="confirm_cancel_follow_request_who_from">%2$s로부터 %1$s로의 팔로우 요청을 취소할까요\?</string>
<string name="reaction">반응 (Misskey)</string>
<string name="vote_misskey">투표 (Misskey)</string>
<string name="timeout_for_embed_media_viewer">내장 미디어 뷰어 시간제한 (단위:초, 앱 재시작(앱 사용이력에서 삭제) 필요)</string>
<string name="link_color">링크 색 (앱 재시작 필요)</string>
<string name="missing_closeable_column">닫을 칼럼이 표시 범위에 없음.</string>

View File

@ -719,7 +719,6 @@
<string name="follow_request_cancelled">Følgingsforespørsel forkastet.</string>
<string name="confirm_cancel_follow_request_who_from">Følgingsforespørsel fra %1$s til %2$s vil forkastes. Er du sikker\?</string>
<string name="reaction">Reaksjon (Misskey)</string>
<string name="vote_misskey">Stem (Misskey)</string>
<string name="timeout_for_embed_media_viewer">Tidsavbrudd for innebygd mediaviser (enhet:sekunder, programomstart (sletting fra programhistorikk) kreves)</string>
<string name="link_color">Lenkefarge (programomstart kreves)</string>
<string name="missing_closeable_column">mangler lukkbar kolonne i synlig område.</string>

View File

@ -756,7 +756,7 @@
<string name="follow_request_cancelled">Follow request cancelled.</string>
<string name="confirm_cancel_follow_request_who_from">Cancel follow request from %2$s to %1$s?</string>
<string name="reaction">Reaction (Misskey)</string>
<string name="vote_misskey">Vote (Misskey)</string>
<string name="vote_polls">voting or its result</string>
<string name="timeout_for_embed_media_viewer">Timeout for embedded media viewer (Unit:seconds, app restart(delete from app history) required)</string>
<string name="link_color">Link color (app restart required)</string>
<string name="missing_closeable_column">Missing closeable column in visible range.</string>
@ -875,8 +875,10 @@
<string name="poll_expire_hours">hours</string>
<string name="poll_expire_minutes">minutes</string>
<string name="vote_1">1 vote</string>
<string name="vote_2">%1$d votes</string>
<string name="vote_2">%1$d votes.</string>
<string name="vote_expire_at">time limit: %1$s</string>
<string name="vote_count_unavailable">\?\?\? votes</string>
<string name="vote_button">Vote</string>
<string name="end_of_polling_from">End of polling from %1$s</string>
</resources>