From 454e93b842f2e39d3dc1c1b9cb0de6d1280ba2bc Mon Sep 17 00:00:00 2001 From: tateisu Date: Thu, 27 Apr 2017 01:36:28 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=AB=E3=83=A9=E3=83=A0=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=81=A7=E6=B7=BB=E4=BB=98=E3=83=87=E3=83=BC=E3=82=BF=E3=81=82?= =?UTF-8?q?=E3=82=8A=E3=81=AE=E7=99=BA=E8=A8=80=E3=81=A0=E3=81=91=E9=96=B2?= =?UTF-8?q?=E8=A6=A7=E3=80=82=E5=8F=96=E5=BE=97=E6=BC=8F=E3=82=8C=E3=81=AE?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E6=80=A7=E3=81=8C=E3=81=82=E3=82=8B=E5=A0=B4?= =?UTF-8?q?=E6=89=80=E3=81=AB=E9=9A=99=E9=96=93=E3=82=92=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=80=82=E3=83=96=E3=83=AD=E3=83=83=E3=82=AF=E3=81=97=E3=81=9F?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=80=81=E3=83=9F=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=97=E3=81=9F=E3=83=A6=E3=83=BC=E3=82=B6=E3=81=AE?= =?UTF-8?q?=E3=83=AA=E3=82=B9=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/dictionaries/tateisu.xml | 1 + app/build.gradle | 4 +- .../java/jp/juggler/subwaytooter/ActMain.java | 23 +- .../java/jp/juggler/subwaytooter/Column.java | 886 +++++++++++++++--- .../subwaytooter/ColumnViewHolder.java | 143 ++- .../subwaytooter/api/entity/TootAccount.java | 10 +- .../api/entity/TootAttachment.java | 18 +- .../subwaytooter/api/entity/TootGap.java | 14 + .../subwaytooter/api/entity/TootId.java | 4 - .../subwaytooter/api/entity/TootMention.java | 11 +- .../api/entity/TootNotification.java | 17 +- .../api/entity/TootRelationShip.java | 12 +- .../subwaytooter/api/entity/TootReport.java | 11 +- .../subwaytooter/api/entity/TootResults.java | 6 +- .../subwaytooter/api/entity/TootStatus.java | 26 +- .../subwaytooter/api/entity/TootTag.java | 23 +- .../juggler/subwaytooter/util/Emojione.java | 6 +- .../subwaytooter/util/HTMLDecoder.java | 42 +- app/src/main/res/drawable-hdpi/ic_block.png | Bin 0 -> 971 bytes app/src/main/res/drawable-hdpi/ic_mute.png | Bin 0 -> 762 bytes app/src/main/res/drawable-hdpi/ic_tune.png | Bin 0 -> 326 bytes app/src/main/res/drawable-mdpi/ic_block.png | Bin 0 -> 604 bytes app/src/main/res/drawable-mdpi/ic_mute.png | Bin 0 -> 470 bytes app/src/main/res/drawable-mdpi/ic_tune.png | Bin 0 -> 224 bytes app/src/main/res/drawable-xhdpi/ic_block.png | Bin 0 -> 1244 bytes app/src/main/res/drawable-xhdpi/ic_mute.png | Bin 0 -> 870 bytes app/src/main/res/drawable-xhdpi/ic_tune.png | Bin 0 -> 287 bytes app/src/main/res/drawable-xxhdpi/ic_block.png | Bin 0 -> 2042 bytes app/src/main/res/drawable-xxhdpi/ic_mute.png | Bin 0 -> 1483 bytes app/src/main/res/drawable-xxhdpi/ic_tune.png | Bin 0 -> 470 bytes app/src/main/res/layout/lv_status.xml | 89 +- app/src/main/res/layout/page_column.xml | 33 +- app/src/main/res/menu/men_navi_drawer.xml | 9 + app/src/main/res/values-ja/strings.xml | 7 + app/src/main/res/values/strings.xml | 6 + 35 files changed, 1142 insertions(+), 259 deletions(-) create mode 100644 app/src/main/java/jp/juggler/subwaytooter/api/entity/TootGap.java create mode 100644 app/src/main/res/drawable-hdpi/ic_block.png create mode 100644 app/src/main/res/drawable-hdpi/ic_mute.png create mode 100644 app/src/main/res/drawable-hdpi/ic_tune.png create mode 100644 app/src/main/res/drawable-mdpi/ic_block.png create mode 100644 app/src/main/res/drawable-mdpi/ic_mute.png create mode 100644 app/src/main/res/drawable-mdpi/ic_tune.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_block.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_mute.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_tune.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_block.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_mute.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_tune.png diff --git a/.idea/dictionaries/tateisu.xml b/.idea/dictionaries/tateisu.xml index e6745af9..333c753c 100644 --- a/.idea/dictionaries/tateisu.xml +++ b/.idea/dictionaries/tateisu.xml @@ -7,6 +7,7 @@ enty favourited hashtag + hashtags noto nsfw reblog diff --git a/app/build.gradle b/app/build.gradle index 13616fb7..fa373f72 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "jp.juggler.subwaytooter" minSdkVersion 21 targetSdkVersion 25 - versionCode 14 - versionName "0.1.4" + versionCode 15 + versionName "0.1.5" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java index 8cd87cd4..5f3925a4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java @@ -69,12 +69,17 @@ public class ActMain extends AppCompatActivity // super.attachBaseContext( CalligraphyContextWrapper.wrap(newBase)); // } + float density; + + SharedPreferences pref; @Override protected void onCreate( Bundle savedInstanceState ){ super.onCreate( savedInstanceState ); + this.density = getResources().getDisplayMetrics().density; + requestWindowFeature( Window.FEATURE_NO_TITLE ); pref = Pref.pref( this ); @@ -297,6 +302,13 @@ public class ActMain extends AppCompatActivity }else if( id == R.id.nav_app_exit ){ finish(); + }else if( id == R.id.nav_add_mutes ){ + performAddTimeline( Column.TYPE_MUTES ); + + }else if( id == R.id.nav_add_blocks ){ + performAddTimeline( Column.TYPE_BLOCKS ); + + // Handle the camera action // }else if( id == R.id.nav_gallery ){ // @@ -1206,7 +1218,7 @@ public class ActMain extends AppCompatActivity static final String FILE_COLUMN_LIST = "column_list"; - private void saveColumnList(){ + void saveColumnList(){ JSONArray array = encodeColumnList(); try{ OutputStream os = openFileOutput( FILE_COLUMN_LIST, MODE_PRIVATE ); @@ -1495,6 +1507,11 @@ public class ActMain extends AppCompatActivity for( Column column : pager_adapter.column_list ){ column.removeStatusByAccount( access_info, who.id ); } + }else{ + for( Column column : pager_adapter.column_list ){ + column.removeFromMuteList( access_info, who.id ); + } + } showColumnMatchAccount( access_info ); } @@ -1569,6 +1586,10 @@ public class ActMain extends AppCompatActivity for( Column column : pager_adapter.column_list ){ column.removeStatusByAccount( access_info, who.id ); } + }else{ + for( Column column : pager_adapter.column_list ){ + column.removeFromBlockList( access_info, who.id ); + } } showColumnMatchAccount( access_info ); } diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.java b/app/src/main/java/jp/juggler/subwaytooter/Column.java index 004f8231..5a246026 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Column.java +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.java @@ -1,8 +1,8 @@ package jp.juggler.subwaytooter; -import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; +import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.v4.os.AsyncTaskCompat; import android.text.TextUtils; @@ -22,7 +22,9 @@ import java.util.regex.Pattern; import jp.juggler.subwaytooter.api.TootApiClient; import jp.juggler.subwaytooter.api.TootApiResult; import jp.juggler.subwaytooter.api.entity.TootAccount; +import jp.juggler.subwaytooter.api.entity.TootAttachment; import jp.juggler.subwaytooter.api.entity.TootContext; +import jp.juggler.subwaytooter.api.entity.TootGap; import jp.juggler.subwaytooter.api.entity.TootNotification; import jp.juggler.subwaytooter.api.entity.TootReport; import jp.juggler.subwaytooter.api.entity.TootResults; @@ -41,27 +43,36 @@ class Column { return params[ idx ]; } - private static final String PATH_HOME = "/api/v1/timelines/home?limit=80"; - private static final String PATH_LOCAL = "/api/v1/timelines/public?limit=80&local=1"; - private static final String PATH_FEDERATE = "/api/v1/timelines/public?limit=80"; - private static final String PATH_FAVOURITES = "/api/v1/favourites?limit=80"; - private static final String PATH_REPORTS = "/api/v1/reports?limit=80"; - private static final String PATH_NOTIFICATIONS = "/api/v1/notifications?limit=80"; + private static final int READ_LIMIT = 80; // API側の上限が80です + private static final long LOOP_TIMEOUT = 10000L; - private static final String PATH_ACCOUNT = "/api/v1/accounts/%d?limit=80"; // 1:account_id - private static final String PATH_ACCOUNT_STATUSES = "/api/v1/accounts/%d/statuses?limit=80"; // 1:account_id - private static final String PATH_ACCOUNT_FOLLOWING = "/api/v1/accounts/%d/following?limit=80"; // 1:account_id - private static final String PATH_ACCOUNT_FOLLOWERS = "/api/v1/accounts/%d/followers?limit=80"; // 1:account_id + // ステータスのリストを返すAPI + private static final String PATH_HOME = "/api/v1/timelines/home?limit=" + READ_LIMIT; + private static final String PATH_LOCAL = "/api/v1/timelines/public?limit=" + READ_LIMIT + "&local=1"; + private static final String PATH_FEDERATE = "/api/v1/timelines/public?limit=" + READ_LIMIT; + private static final String PATH_FAVOURITES = "/api/v1/favourites?limit=" + READ_LIMIT; + private static final String PATH_ACCOUNT_STATUSES = "/api/v1/accounts/%d/statuses?limit=" + READ_LIMIT; // 1:account_id + private static final String PATH_HASHTAG = "/api/v1/timelines/tag/%s?limit=" + READ_LIMIT; // 1: hashtag(url encoded) - private static final String PATH_HASHTAG = "/api/v1/timelines/tag/%s?limit=80"; // 1: hashtag(url encoded) + // アカウントのリストを返すAPI + private static final String PATH_ACCOUNT_FOLLOWING = "/api/v1/accounts/%d/following?limit=" + READ_LIMIT; // 1:account_id + private static final String PATH_ACCOUNT_FOLLOWERS = "/api/v1/accounts/%d/followers?limit=" + READ_LIMIT; // 1:account_id + private static final String PATH_MUTES = "/api/v1/mutes?limit=" + READ_LIMIT; // 1:account_id + private static final String PATH_BLOCKS = "/api/v1/blocks?limit=" + READ_LIMIT; // 1:account_id + // 他のリストを返すAPI + private static final String PATH_REPORTS = "/api/v1/reports?limit=" + READ_LIMIT; + private static final String PATH_NOTIFICATIONS = "/api/v1/notifications?limit=" + READ_LIMIT; + + // リストではなくオブジェクトを返すAPI + private static final String PATH_ACCOUNT = "/api/v1/accounts/%d"; // 1:account_id private static final String PATH_STATUSES = "/api/v1/statuses/%d"; // 1:status_id private static final String PATH_STATUSES_CONTEXT = "/api/v1/statuses/%d/context"; // 1:status_id - - private static final String PATH_SEARCH = "/api/v1/search?limit=80&q=%s"; // 1: query(urlencoded) , also, append "&resolve=1" if resolve non-local accounts + private static final String PATH_SEARCH = "/api/v1/search?q=%s"; // 1: query(urlencoded) , also, append "&resolve=1" if resolve non-local accounts private static final String KEY_ACCOUNT_ROW_ID = "account_id"; private static final String KEY_TYPE = "type"; + private static final String KEY_WITH_ATTACHMENT = "with_attachment"; private static final String KEY_PROFILE_ID = "profile_id"; private static final String KEY_PROFILE_TAB = "tab"; private static final String KEY_STATUS_ID = "status_id"; @@ -73,23 +84,25 @@ class Column { static final String KEY_COLUMN_NAME = "column_name"; static final String KEY_OLD_INDEX = "old_index"; - private final - @NonNull ActMain activity; - - final - @NonNull SavedAccount access_info; - - final int type; static final int TYPE_HOME = 1; static final int TYPE_LOCAL = 2; static final int TYPE_FEDERATE = 3; static final int TYPE_PROFILE = 4; static final int TYPE_FAVOURITES = 5; - private static final int TYPE_REPORTS = 6; + static final int TYPE_REPORTS = 6; static final int TYPE_NOTIFICATIONS = 7; static final int TYPE_CONVERSATION = 8; static final int TYPE_HASHTAG = 9; static final int TYPE_SEARCH = 10; + static final int TYPE_MUTES = 11; + static final int TYPE_BLOCKS = 12; + + @NonNull private final ActMain activity; + @NonNull final SavedAccount access_info; + + final int type; + + boolean with_attachment; private long profile_id; volatile TootAccount who_account; @@ -139,6 +152,7 @@ class Column { void encodeJSON( JSONObject item, int old_index ) throws JSONException{ item.put( KEY_ACCOUNT_ROW_ID, access_info.db_id ); item.put( KEY_TYPE, type ); + item.put( KEY_WITH_ATTACHMENT, with_attachment ); switch( type ){ case TYPE_CONVERSATION: @@ -169,8 +183,9 @@ class Column { SavedAccount ac = SavedAccount.loadAccount( log, src.optLong( KEY_ACCOUNT_ROW_ID ) ); if( ac == null ) throw new RuntimeException( "missing account" ); this.access_info = ac; - this.type = src.optInt( KEY_TYPE ); + this.with_attachment = src.optBoolean( KEY_WITH_ATTACHMENT ); + switch( type ){ case TYPE_CONVERSATION: @@ -279,6 +294,12 @@ class Column { case TYPE_HASHTAG: return activity.getString( R.string.hashtag_of, hashtag ); + case TYPE_MUTES: + return activity.getString( R.string.muted_users ); + + case TYPE_BLOCKS: + return activity.getString( R.string.blocked_users ); + case TYPE_SEARCH: if( bLong ){ return activity.getString( R.string.search_of, search_query ); @@ -350,6 +371,42 @@ class Column { list_data.addAll( tmp_list ); } + // ミュート解除が成功した時に呼ばれる + void removeFromMuteList( SavedAccount target_account, long who_id ){ + if( ! target_account.acct.equals( access_info.acct ) ) return; + if( type != TYPE_MUTES ) return; + + ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() ); + for( Object o : list_data ){ + if( o instanceof TootAccount ){ + TootAccount item = (TootAccount) o; + if( item.id == who_id ) continue; + } + + tmp_list.add( o ); + } + list_data.clear(); + list_data.addAll( tmp_list ); + } + + // ミュート解除が成功した時に呼ばれる + void removeFromBlockList( SavedAccount target_account, long who_id ){ + if( ! target_account.acct.equals( access_info.acct ) ) return; + if( type != TYPE_BLOCKS ) return; + + ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() ); + for( Object o : list_data ){ + if( o instanceof TootAccount ){ + TootAccount item = (TootAccount) o; + if( item.id == who_id ) continue; + } + + tmp_list.add( o ); + } + list_data.clear(); + list_data.addAll( tmp_list ); + } + // 自分のステータスを削除した時に呼ばれる void removeStatus( SavedAccount target_account, long status_id ){ @@ -444,6 +501,12 @@ class Column { startLoading(); } + private static boolean hasMedia( TootStatus status ){ + if( status == null ) return false; + TootAttachment.List list = status.media_attachments; + return ! ( list == null || list.isEmpty() ); + } + private void startLoading(){ cancelLastTask(); @@ -458,25 +521,65 @@ class Column { AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() { - TootApiResult parseAccount1( TootApiResult result ){ - if( result != null ){ - who_account = TootAccount.parse( log, access_info, result.object ); - } + TootApiResult parseAccount1( TootApiClient client, String path_base ){ + TootApiResult result = client.request( path_base ); + Column.this.who_account = TootAccount.parse( log, access_info, result.object ); return result; } ArrayList< Object > list_tmp; - TootApiResult parseStatuses( TootApiResult result ){ - if( result != null ){ + TootApiResult getStatuses( TootApiClient client, String path_base ){ + long time_start = SystemClock.elapsedRealtime(); + TootApiResult result = client.request( path_base ); + if( result != null && result.array != null ){ saveRange( result, true, true ); - list_tmp = new ArrayList<>(); - list_tmp.addAll( TootStatus.parseList( log, access_info, result.array ) ); + // + TootStatus.List src = TootStatus.parseList( log, access_info, result.array ); + list_tmp = new ArrayList<>( src.size() ); + if( ! with_attachment ){ + list_tmp.addAll( src ); + }else{ + for( TootStatus status : src ){ + if( hasMedia( status ) || hasMedia( status.reblog ) ) + list_tmp.add( status ); + } + } + // + if( max_id != null && with_attachment ){ + char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); + for( ; ; ){ + if( src.isEmpty() ){ + // 直前のリクエストが空のリストを返したら諦める + break; + } + if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ + // タイムアウト + break; + } + String path = path_base + delimiter + "max_id=" + max_id; + TootApiResult result2 = client.request( path ); + if( result2 == null || result2.array == null ) break; + if( ! saveRangeEnd( result2 ) ) break; + + src = TootStatus.parseList( log, access_info, result2.array ); + + if( ! with_attachment ){ + list_tmp.addAll( src ); + }else{ + for( TootStatus status : src ){ + if( hasMedia( status ) || hasMedia( status.reblog ) ) + list_tmp.add( status ); + } + } + } + } } return result; } - TootApiResult parseAccountList( TootApiResult result ){ + TootApiResult parseAccountList( TootApiClient client, String path_base ){ + TootApiResult result = client.request( path_base ); if( result != null ){ saveRange( result, true, true ); list_tmp = new ArrayList<>(); @@ -485,7 +588,8 @@ class Column { return result; } - TootApiResult parseReports( TootApiResult result ){ + TootApiResult parseReports( TootApiClient client, String path_base ){ + TootApiResult result = client.request( path_base ); if( result != null ){ saveRange( result, true, true ); list_tmp = new ArrayList<>(); @@ -494,14 +598,16 @@ class Column { return result; } - TootApiResult parseNotifications( TootApiResult result ){ + TootApiResult parseNotifications( TootApiClient client, String path_base ){ + TootApiResult result = client.request( path_base ); if( result != null ){ saveRange( result, true, true ); TootNotification.List src = TootNotification.parseList( log, access_info, result.array ); - if( src != null ){ - list_tmp = new ArrayList<>(); - list_tmp.addAll( src ); - // + + list_tmp = new ArrayList<>(); + list_tmp.addAll( src ); + // + if( ! src.isEmpty() ){ AlarmService.injectData( activity, access_info.db_id, src ); } @@ -538,49 +644,55 @@ class Column { default: case TYPE_HOME: - return parseStatuses( client.request( PATH_HOME ) ); + return getStatuses( client, PATH_HOME ); case TYPE_LOCAL: - return parseStatuses( client.request( PATH_LOCAL ) ); + return getStatuses( client, PATH_LOCAL ); case TYPE_FEDERATE: - return parseStatuses( client.request( PATH_FEDERATE ) ); + return getStatuses( client, PATH_FEDERATE ); case TYPE_PROFILE: if( who_account == null ){ - parseAccount1( client.request( - String.format( Locale.JAPAN, PATH_ACCOUNT, profile_id ) ) ); + parseAccount1( client, String.format( Locale.JAPAN, PATH_ACCOUNT, profile_id ) ); client.callback.publishApiProgress( "" ); } switch( profile_tab ){ default: case TAB_STATUS: - return parseStatuses( client.request( - String.format( Locale.JAPAN, PATH_ACCOUNT_STATUSES, profile_id ) ) ); + String s = String.format( Locale.JAPAN, PATH_ACCOUNT_STATUSES, profile_id ); + if( with_attachment ) s = s + "&only_media"; + return getStatuses( client, s ); case TAB_FOLLOWING: - return parseAccountList( client.request( - String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWING, profile_id ) ) ); + return parseAccountList( client, + String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWING, profile_id ) ); case TAB_FOLLOWERS: - return parseAccountList( client.request( - String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWERS, profile_id ) ) ); + return parseAccountList( client, + String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWERS, profile_id ) ); } + case TYPE_MUTES: + return parseAccountList( client, PATH_MUTES ); + + case TYPE_BLOCKS: + return parseAccountList( client, PATH_BLOCKS ); + case TYPE_FAVOURITES: - return parseStatuses( client.request( PATH_FAVOURITES ) ); + return getStatuses( client, PATH_FAVOURITES ); case TYPE_HASHTAG: - return parseStatuses( client.request( - String.format( Locale.JAPAN, PATH_HASHTAG, Uri.encode( hashtag ) ) ) ); + return getStatuses( client, + String.format( Locale.JAPAN, PATH_HASHTAG, Uri.encode( hashtag ) ) ); case TYPE_REPORTS: - return parseReports( client.request( PATH_REPORTS ) ); + return parseReports( client, PATH_REPORTS ); case TYPE_NOTIFICATIONS: - return parseNotifications( client.request( PATH_NOTIFICATIONS ) ); + return parseNotifications( client, PATH_NOTIFICATIONS ); case TYPE_CONVERSATION: @@ -663,10 +775,11 @@ class Column { private String max_id; private String since_id; + int scroll_hack; private void saveRange( TootApiResult result, boolean bBottom, boolean bTop ){ - // Link: ; rel="next", - // ; rel="prev" + // Link: ; rel="next", + // ; rel="prev" if( result.response != null ){ String sv = result.response.header( "Link" ); @@ -689,6 +802,21 @@ class Column { } } + private boolean saveRangeEnd( TootApiResult result ){ + if( result.response != null ){ + String sv = result.response.header( "Link" ); + if( ! TextUtils.isEmpty( sv ) ){ + Matcher m = reMaxId.matcher( sv ); + if( m.find() ){ + max_id = m.group( 1 ); + //log.d( "col=%s,max_id=%s", this.hashCode(), max_id ); + return true; + } + } + } + return false; + } + private String addRange( boolean bBottom, String path ){ char delm = ( - 1 != path.indexOf( '?' ) ? '&' : '?' ); if( bBottom ){ @@ -699,17 +827,15 @@ class Column { return path; } - boolean startRefresh( final boolean bBottom ){ + String startRefresh( final boolean bBottom ){ if( last_task != null ){ - log.d( "busy" ); - return false; + return activity.getString( R.string.column_is_busy ); }else if( bBottom && max_id == null ){ - log.d( "startRefresh failed. missing max_id" ); - return false; + return activity.getString( R.string.end_of_list ); }else if( ! bBottom && since_id == null ){ - log.d( "startRefresh failed. missing since_id" ); - return false; + return "startRefresh failed. missing since_id"; } + bRefreshLoading = true; mRefreshLoadingError = null; @@ -722,45 +848,263 @@ class Column { return result; } - TootApiResult parseAccountList( TootApiResult result ){ - if( result != null ){ + TootApiResult getAccountList( TootApiClient client, String path_base ){ + long time_start = SystemClock.elapsedRealtime(); + char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); + String last_since_id = since_id; + + TootApiResult result = client.request( addRange( bBottom, path_base ) ); + if( result != null && result.array != null ){ saveRange( result, bBottom, ! bBottom ); list_tmp = new ArrayList<>(); - list_tmp.addAll( TootAccount.parseList( log, access_info, result.array ) ); + TootAccount.List src = TootAccount.parseList( log, access_info, result.array ); + list_tmp.addAll( src ); + if( ! bBottom ){ + for( ; ; ){ + // max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない + // 直前のデータが0個なら終了とみなすしかなさそう + if( src.isEmpty() ){ + log.d( "refresh-account-top: previous size == 0." ); + break; + } + + // 隙間の最新のステータスIDは取得データ末尾のステータスIDである + String max_id = Long.toString( src.get( src.size() - 1 ).id ); + + if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ + // タイムアウト + // 隙間ができるかもしれない。後ほど手動で試してもらうしかない + TootGap gap = new TootGap( max_id, last_since_id ); + list_tmp.add( gap ); + break; + } + + String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id; + TootApiResult result2 = client.request( path ); + if( result2 == null || result2.array == null ){ + // エラー + // 隙間ができるかもしれない。後ほど手動で試してもらうしかない + TootGap gap = new TootGap( max_id, last_since_id ); + list_tmp.add( gap ); + break; + } + + src = TootAccount.parseList( log, access_info, result2.array ); + list_tmp.addAll( src ); + } + } + } + return result; + } + + TootApiResult getReportList( TootApiClient client, String path_base ){ + long time_start = SystemClock.elapsedRealtime(); + char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); + String last_since_id = since_id; + TootApiResult result = client.request( addRange( bBottom, path_base ) ); + if( result != null && result.array != null ){ + saveRange( result, bBottom, ! bBottom ); + list_tmp = new ArrayList<>(); + TootReport.List src = TootReport.parseList( log, result.array ); + list_tmp.addAll( src ); + if( ! bBottom ){ + for( ; ; ){ + // max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない + // 直前のデータが0個なら終了とみなすしかなさそう + if( src.isEmpty() ){ + log.d( "refresh-report-top: previous size == 0." ); + break; + } + + // 隙間の最新のステータスIDは取得データ末尾のステータスIDである + String max_id = Long.toString( src.get( src.size() - 1 ).id ); + + if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ + // タイムアウト + // 隙間ができるかもしれない。後ほど手動で試してもらうしかない + TootGap gap = new TootGap( max_id, last_since_id ); + list_tmp.add( gap ); + break; + } + + String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id; + TootApiResult result2 = client.request( path ); + if( result2 == null || result2.array == null ){ + // エラー + // 隙間ができるかもしれない。後ほど手動で試してもらうしかない + TootGap gap = new TootGap( max_id, last_since_id ); + list_tmp.add( gap ); + break; + } + + src = TootReport.parseList( log, result2.array ); + list_tmp.addAll( src ); + } + } + } + return result; + } + + TootApiResult getNotificationList( TootApiClient client, String path_base ){ + long time_start = SystemClock.elapsedRealtime(); + char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); + String last_since_id = since_id; + + TootApiResult result = client.request( addRange( bBottom, path_base ) ); + if( result != null && result.array != null ){ + saveRange( result, bBottom, ! bBottom ); + list_tmp = new ArrayList<>(); + TootNotification.List src = TootNotification.parseList( log, access_info, result.array ); + list_tmp.addAll( src ); + + if( ! src.isEmpty() ){ + AlarmService.injectData( activity, access_info.db_id, src ); + } + + if( ! bBottom ){ + for( ; ; ){ + // max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない + // 直前のデータが0個なら終了とみなすしかなさそう + if( src.isEmpty() ){ + log.d( "refresh-notification-top: previous size == 0." ); + break; + } + + // 隙間の最新のステータスIDは取得データ末尾のステータスIDである + String max_id = Long.toString( src.get( src.size() - 1 ).id ); + + if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ + // タイムアウト + // 隙間ができるかもしれない。後ほど手動で試してもらうしかない + TootGap gap = new TootGap( max_id, last_since_id ); + list_tmp.add( gap ); + break; + } + + String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id; + TootApiResult result2 = client.request( path ); + if( result2 == null || result2.array == null ){ + // エラー + // 隙間ができるかもしれない。後ほど手動で試してもらうしかない + TootGap gap = new TootGap( max_id, last_since_id ); + list_tmp.add( gap ); + break; + } + + src = TootNotification.parseList( log, access_info, result2.array ); + if( ! src.isEmpty() ){ + list_tmp.addAll( src ); + AlarmService.injectData( activity, access_info.db_id, src ); + } + } + } } return result; } ArrayList< Object > list_tmp; - TootApiResult parseStatuses( TootApiResult result ){ - if( result != null ){ + TootApiResult getStatusList( TootApiClient client, String path_base ){ + long time_start = SystemClock.elapsedRealtime(); + + char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); + final String last_since_id = since_id; + + TootApiResult result = client.request( addRange( bBottom, path_base ) ); + if( result != null && result.array != null ){ saveRange( result, bBottom, ! bBottom ); + TootStatus.List src = TootStatus.parseList( log, access_info, result.array ); list_tmp = new ArrayList<>(); - list_tmp.addAll( TootStatus.parseList( log, access_info, result.array ) ); - } - return result; - } - - TootApiResult parseReports( TootApiResult result ){ - if( result != null ){ - saveRange( result, bBottom, ! bBottom ); - list_tmp = new ArrayList<>(); - list_tmp.addAll( TootReport.parseList( log, result.array ) ); - } - return result; - } - - TootApiResult parseNotifications( TootApiResult result ){ - if( result != null ){ - saveRange( result, bBottom, ! bBottom ); - TootNotification.List src = TootNotification.parseList( log, access_info, result.array ); - if( src != null ){ - list_tmp = new ArrayList<>(); + if( ! with_attachment ){ list_tmp.addAll( src ); - // - AlarmService.injectData( activity, access_info.db_id, src ); + }else{ + for( TootStatus status : src ){ + if( hasMedia( status ) || hasMedia( status.reblog ) ) + list_tmp.add( status ); + } + } + + if( bBottom ){ + if( with_attachment ){ + for( ; ; ){ + // max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない + // 直前のデータが0個なら終了とみなすしかなさそう + if( src.isEmpty() ){ + log.d( "refresh-status-bottom: previous size == 0." ); + break; + } + + if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ + // タイムアウト + log.d( "refresh-status-bottom: loop timeout." ); + break; + } + + String path = path_base + delimiter + "max_id=" + max_id; + TootApiResult result2 = client.request( path ); + if( result2 == null || result2.array == null ){ + log.d( "refresh-status-bottom: error or cancelled." ); + break; + } + + src = TootStatus.parseList( log, access_info, result2.array ); + + if( ! with_attachment ){ + list_tmp.addAll( src ); + }else{ + for( TootStatus status : src ){ + if( hasMedia( status ) || hasMedia( status.reblog ) ) + list_tmp.add( status ); + } + } + + if( ! saveRangeEnd( result2 ) ){ + log.d( "refresh-status-bottom: saveRangeEnd failed." ); + break; + } + } + } + }else{ + for( ; ; ){ + // max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない + // 直前のデータが0個なら終了とみなすしかなさそう + if( src.isEmpty() ){ + log.d( "refresh-status-top: previous size == 0." ); + break; + } + + // 隙間の最新のステータスIDは取得データ末尾のステータスIDである + String max_id = Long.toString( src.get( src.size() - 1 ).id ); + + if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ + // タイムアウト + // 隙間ができるかもしれない。後ほど手動で試してもらうしかない + TootGap gap = new TootGap( max_id, last_since_id ); + list_tmp.add( gap ); + break; + } + + String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id; + TootApiResult result2 = client.request( path ); + if( result2 == null || result2.array == null ){ + // エラー + // 隙間ができるかもしれない。後ほど手動で試してもらうしかない + TootGap gap = new TootGap( max_id, last_since_id ); + list_tmp.add( gap ); + break; + } + + src = TootStatus.parseList( log, access_info, result2.array ); + if( ! with_attachment ){ + list_tmp.addAll( src ); + }else{ + for( TootStatus status : src ){ + if( hasMedia( status ) || hasMedia( status.reblog ) ) + list_tmp.add( status ); + } + } + } } } return result; @@ -793,22 +1137,22 @@ class Column { default: case TYPE_HOME: - return parseStatuses( client.request( addRange( bBottom, PATH_HOME ) ) ); + return getStatusList( client, PATH_HOME ); case TYPE_LOCAL: - return parseStatuses( client.request( addRange( bBottom, PATH_LOCAL ) ) ); + return getStatusList( client, PATH_LOCAL ); case TYPE_FEDERATE: - return parseStatuses( client.request( addRange( bBottom, PATH_FEDERATE ) ) ); + return getStatusList( client, PATH_FEDERATE ); case TYPE_FAVOURITES: - return parseStatuses( client.request( addRange( bBottom, PATH_FAVOURITES ) ) ); + return getStatusList( client, PATH_FAVOURITES ); case TYPE_REPORTS: - return parseReports( client.request( addRange( bBottom, PATH_REPORTS ) ) ); + return getReportList( client, PATH_REPORTS ); case TYPE_NOTIFICATIONS: - return parseNotifications( client.request( addRange( bBottom, PATH_NOTIFICATIONS ) ) ); + return getNotificationList( client, PATH_NOTIFICATIONS ); case TYPE_PROFILE: if( who_account == null ){ @@ -821,22 +1165,27 @@ class Column { default: case TAB_STATUS: - return parseStatuses( client.request( addRange( bBottom, - String.format( Locale.JAPAN, PATH_ACCOUNT_STATUSES, profile_id ) ) ) ); + return getStatusList( client, + String.format( Locale.JAPAN, PATH_ACCOUNT_STATUSES, profile_id ) ); case TAB_FOLLOWING: - return parseAccountList( client.request( addRange( bBottom, - String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWING, profile_id ) ) ) ); + return getAccountList( client, + String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWING, profile_id ) ); case TAB_FOLLOWERS: - return parseAccountList( client.request( addRange( bBottom, - String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWERS, profile_id ) ) ) ); + return getAccountList( client, + String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWERS, profile_id ) ); } + case TYPE_MUTES: + return getAccountList( client, PATH_MUTES ); + + case TYPE_BLOCKS: + return getAccountList( client, PATH_BLOCKS ); case TYPE_HASHTAG: - return parseStatuses( client.request( addRange( bBottom, - String.format( Locale.JAPAN, PATH_HASHTAG, Uri.encode( hashtag ) ) ) ) ); + return getStatusList( client, + String.format( Locale.JAPAN, PATH_HASHTAG, Uri.encode( hashtag ) ) ); } } @@ -890,6 +1239,8 @@ class Column { } if( ! bBottom ){ + // リフレッシュ開始時はリストの先頭を見ていたのだからスクロール範囲を調整したい + scroll_hack = list_new.size(); // 新しいデータの後に今のデータが並ぶ list_new.addAll( list_data ); list_data.clear(); @@ -907,7 +1258,332 @@ class Column { }; AsyncTaskCompat.executeParallel( task ); - return true; + + return null; } + String startGap( final TootGap gap ){ + if( last_task != null ){ + return activity.getString( R.string.column_is_busy ); + } + + bRefreshLoading = true; + mRefreshLoadingError = null; + + AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() { + String max_id = gap.max_id; + String since_id = gap.since_id; + ArrayList< Object > list_tmp; + + TootApiResult getAccountList( TootApiClient client, String path_base ){ + long time_start = SystemClock.elapsedRealtime(); + char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); + list_tmp = new ArrayList<>(); + + TootApiResult result = null; + for( ; ; ){ + if( result != null && SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ + // タイムアウト + // 隙間が残る + list_tmp.add( new TootGap( max_id, since_id ) ); + break; + } + + String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + since_id; + TootApiResult r2 = client.request( path ); + if( r2 == null || r2.array == null ){ + if( result == null ) result = r2; + // 隙間が残る + list_tmp.add( new TootGap( max_id, since_id ) ); + break; + } + result = r2; + TootAccount.List src = TootAccount.parseList( log, access_info, r2.array ); + + if( src.isEmpty() ){ + break; + } + + // 隙間の最新のステータスIDは取得データ末尾のステータスIDである + max_id = Long.toString( src.get( src.size() - 1 ).id ); + + list_tmp.addAll( src ); + } + return result; + } + + TootApiResult getReportList( TootApiClient client, String path_base ){ + long time_start = SystemClock.elapsedRealtime(); + char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); + list_tmp = new ArrayList<>(); + + TootApiResult result = null; + for( ; ; ){ + if( result != null && SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ + // タイムアウト + // 隙間が残る + list_tmp.add( new TootGap( max_id, since_id ) ); + break; + } + + String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + since_id; + TootApiResult r2 = client.request( path ); + if( r2 == null || r2.array == null ){ + if( result == null ) result = r2; + // 隙間が残る + list_tmp.add( new TootGap( max_id, since_id ) ); + break; + } + + result = r2; + TootReport.List src = TootReport.parseList( log, r2.array ); + if( src.isEmpty() ){ + // コレ以上取得する必要はない + break; + } + + // 隙間の最新のステータスIDは取得データ末尾のステータスIDである + max_id = Long.toString( src.get( src.size() - 1 ).id ); + + list_tmp.addAll( src ); + } + return result; + } + + TootApiResult getNotificationList( TootApiClient client, String path_base ){ + long time_start = SystemClock.elapsedRealtime(); + char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); + list_tmp = new ArrayList<>(); + + TootApiResult result = null; + for( ; ; ){ + if( result != null && SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ + // タイムアウト + // 隙間が残る + list_tmp.add( new TootGap( max_id, since_id ) ); + break; + } + String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + since_id; + TootApiResult r2 = client.request( path ); + if( r2 == null || r2.array == null ){ + // エラー + log.d( "gap-notification: got error." ); + + if( result == null ) result = r2; + + // 隙間が残る + list_tmp.add( new TootGap( max_id, since_id ) ); + break; + } + + result = r2; + TootNotification.List src = TootNotification.parseList( log, access_info, r2.array ); + + if( src.isEmpty() ){ + log.d( "gap-notification: got empty list." ); + break; + } + + // 隙間の最新のステータスIDは取得データ末尾のステータスIDである + max_id = Long.toString( src.get( src.size() - 1 ).id ); + + list_tmp.addAll( src ); + + AlarmService.injectData( activity, access_info.db_id, src ); + + } + return result; + } + + TootApiResult getStatusList( TootApiClient client, String path_base ){ + long time_start = SystemClock.elapsedRealtime(); + char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' ); + list_tmp = new ArrayList<>(); + + TootApiResult result = null; + for( ; ; ){ + if( result != null && SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){ + // タイムアウト + // 隙間が残る + list_tmp.add( new TootGap( max_id, since_id ) ); + break; + } + + String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + since_id; + + TootApiResult r2 = client.request( path ); + if( r2 == null || r2.array == null ){ + // エラー + log.d( "gap-status: got error." ); + + // 成功データがない場合だけ、今回のエラーを返すようにする + if( result == null ) result = r2; + + // 隙間が残る + list_tmp.add( new TootGap( max_id, since_id ) ); + + break; + } + + // 成功した場合はそれを返したい + result = r2; + + TootStatus.List src = TootStatus.parseList( log, access_info, r2.array ); + if( src.size() == 0 ){ + // 直前の取得でカラのデータが帰ってきたら終了 + log.d( "gap-status: got empty list." ); + break; + } + // 隙間の最新のステータスIDは取得データ末尾のステータスIDである + max_id = Long.toString( src.get( src.size() - 1 ).id ); + + if( ! with_attachment ){ + list_tmp.addAll( src ); + }else{ + for( TootStatus status : src ){ + if( hasMedia( status ) || hasMedia( status.reblog ) ) + list_tmp.add( status ); + } + } + } + return result; + } + + @Override protected TootApiResult doInBackground( Void... params ){ + TootApiClient client = new TootApiClient( activity, new TootApiClient.Callback() { + @Override public boolean isApiCancelled(){ + return isCancelled() || is_dispose.get(); + } + + @Override public void publishApiProgress( final String s ){ + Utils.runOnMainThread( new Runnable() { + @Override + public void run(){ + if( isCancelled() ) return; + task_progress = s; + fireVisualCallback(); + } + } ); + } + } ); + + client.setAccount( access_info ); + + switch( type ){ + + default: + case TYPE_HOME: + return getStatusList( client, PATH_HOME ); + + case TYPE_LOCAL: + return getStatusList( client, PATH_LOCAL ); + + case TYPE_FEDERATE: + return getStatusList( client, PATH_FEDERATE ); + + case TYPE_FAVOURITES: + return getStatusList( client, PATH_FAVOURITES ); + + case TYPE_REPORTS: + return getReportList( client, PATH_REPORTS ); + + case TYPE_NOTIFICATIONS: + return getNotificationList( client, PATH_NOTIFICATIONS ); + + case TYPE_HASHTAG: + return getStatusList( client, + String.format( Locale.JAPAN, PATH_HASHTAG, Uri.encode( hashtag ) ) ); + + case TYPE_MUTES: + return getAccountList( client, PATH_MUTES ); + + case TYPE_BLOCKS: + return getAccountList( client, PATH_BLOCKS ); + + case TYPE_PROFILE: + switch( profile_tab ){ + + default: + case TAB_STATUS: + return getStatusList( client, + String.format( Locale.JAPAN, PATH_ACCOUNT_STATUSES, profile_id ) ); + + case TAB_FOLLOWING: + return getAccountList( client, + String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWING, profile_id ) ); + + case TAB_FOLLOWERS: + return getAccountList( client, + String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWERS, profile_id ) ); + } + + } + } + + @Override + protected void onCancelled( TootApiResult result ){ + onPostExecute( null ); + } + + @Override + protected void onPostExecute( TootApiResult result ){ + + if( isCancelled() || result == null ){ + return; + } + last_task = null; + bRefreshLoading = false; + + if( result.error != null ){ + Column.this.mRefreshLoadingError = result.error; + }else{ + if( list_tmp != null ){ + // 古いリストにある要素のIDの集合 + HashSet< Long > set_status_id = new HashSet<>(); + HashSet< Long > set_notification_id = new HashSet<>(); + HashSet< Long > set_report_id = new HashSet<>(); + HashSet< Long > set_account_id = new HashSet<>(); + for( Object o : list_data ){ + if( o instanceof TootStatus ){ + set_status_id.add( ( (TootStatus) o ).id ); + }else if( o instanceof TootNotification ){ + set_notification_id.add( ( (TootNotification) o ).id ); + }else if( o instanceof TootReport ){ + set_report_id.add( ( (TootReport) o ).id ); + }else if( o instanceof TootAccount ){ + set_account_id.add( ( (TootAccount) o ).id ); + } + } + // list_tmp をフィルタしてlist_newを作成 + ArrayList< Object > list_new = new ArrayList<>(); + for( Object o : list_tmp ){ + if( o instanceof TootStatus ){ + if( set_status_id.contains( ( (TootStatus) o ).id ) ) continue; + }else if( o instanceof TootNotification ){ + if( set_notification_id.contains( ( (TootNotification) o ).id ) ) + continue; + }else if( o instanceof TootReport ){ + if( set_report_id.contains( ( (TootReport) o ).id ) ) continue; + }else if( o instanceof TootAccount ){ + if( set_account_id.contains( ( (TootAccount) o ).id ) ) + continue; + } + list_new.add( o ); + } + + int pos = list_data.indexOf( gap ); + if( pos != - 1 ){ + list_data.remove( pos ); + list_data.addAll( pos, list_new ); + } + } + } + + fireVisualCallback(); + } + }; + + AsyncTaskCompat.executeParallel( task ); + return null; + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java index f7951790..b72a55dd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java @@ -14,6 +14,7 @@ import android.view.inputmethod.InputMethodManager; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.CheckBox; +import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; @@ -30,6 +31,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import jp.juggler.subwaytooter.api.entity.TootAccount; import jp.juggler.subwaytooter.api.entity.TootAttachment; +import jp.juggler.subwaytooter.api.entity.TootGap; import jp.juggler.subwaytooter.api.entity.TootNotification; import jp.juggler.subwaytooter.api.entity.TootStatus; import jp.juggler.subwaytooter.table.ContentWarning; @@ -39,7 +41,7 @@ import jp.juggler.subwaytooter.util.Emojione; import jp.juggler.subwaytooter.util.LogCategory; import jp.juggler.subwaytooter.util.Utils; -class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, SwipyRefreshLayout.OnRefreshListener { +class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, SwipyRefreshLayout.OnRefreshListener, CompoundButton.OnCheckedChangeListener { private static final LogCategory log = new LogCategory( "ColumnViewHolder" ); public final ActMain activity; @@ -72,10 +74,14 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S private View btnSearch; private EditText etSearch; private CheckBox cbResolve; + private View llColumnSetting; + void onPageCreate( View root, int page_idx, int page_count ){ log.d( "onPageCreate:%s", column.getColumnName( true ) ); + + ( (TextView) root.findViewById( R.id.tvColumnIndex ) ) .setText( activity.getString( R.string.column_index, page_idx + 1, page_count ) ); @@ -86,7 +92,6 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S root.findViewById( R.id.btnColumnReload ).setOnClickListener( this ); root.findViewById( R.id.llColumnHeader ).setOnClickListener( this ); - tvLoading = (TextView) root.findViewById( R.id.tvLoading ); listView = (ListView) root.findViewById( R.id.listView ); status_adapter = new StatusListAdapter(); @@ -94,14 +99,42 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S this.swipyRefreshLayout = (SwipyRefreshLayout) root.findViewById( R.id.swipyRefreshLayout ); swipyRefreshLayout.setOnRefreshListener( this ); - swipyRefreshLayout.setDistanceToTriggerSync( (int) ( 0.5f + 20 * activity.getResources().getDisplayMetrics().density ) ); + swipyRefreshLayout.setDistanceToTriggerSync( (int) ( 0.5f + 20f * activity.density ) ); View llSearch = root.findViewById( R.id.llSearch ); btnSearch = root.findViewById( R.id.btnSearch ); etSearch = (EditText) root.findViewById( R.id.etSearch ); cbResolve = (CheckBox) root.findViewById( R.id.cbResolve ); - listView.setFastScrollEnabled( !Pref.pref(activity).getBoolean( Pref.KEY_DISABLE_FAST_SCROLLER,false) ); + listView.setFastScrollEnabled( ! Pref.pref( activity ).getBoolean( Pref.KEY_DISABLE_FAST_SCROLLER, false ) ); + + boolean bAllowColumnSetting; + switch( column.type ){ + default: + bAllowColumnSetting = true; + break; + case Column.TYPE_SEARCH: + case Column.TYPE_CONVERSATION: + case Column.TYPE_REPORTS: + case Column.TYPE_BLOCKS: + case Column.TYPE_MUTES: + case Column.TYPE_NOTIFICATIONS: + bAllowColumnSetting = false; + break; + } + View btnColumnSetting = root.findViewById( R.id.btnColumnSetting ); + llColumnSetting = root.findViewById( R.id.llColumnSetting ); + if( ! bAllowColumnSetting ){ + btnColumnSetting.setVisibility( View.GONE ); + llColumnSetting.setVisibility( View.GONE ); + }else{ + btnColumnSetting.setVisibility( View.VISIBLE ); + btnColumnSetting.setOnClickListener( this ); + llColumnSetting.setVisibility( View.GONE ); + CheckBox cbWithAttachment = (CheckBox) root.findViewById( R.id.cbWithAttachment ); + cbWithAttachment.setChecked( column.with_attachment ); + cbWithAttachment.setOnCheckedChangeListener( this ); + } if( column.type != Column.TYPE_SEARCH ){ llSearch.setVisibility( View.GONE ); @@ -138,7 +171,16 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S onVisualColumn(); } - + @Override public void onCheckedChanged( CompoundButton view, boolean isChecked ){ + switch( view.getId() ){ + case R.id.cbWithAttachment: + column.with_attachment = isChecked; + activity.saveColumnList(); + column.reload(); + break; + } + } + @Override public void onClick( View v ){ switch( v.getId() ){ @@ -163,7 +205,11 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S break; case R.id.llColumnHeader: - if( status_adapter.getCount() > 0 ) listView.setSelectionFromTop( 0,0); + if( status_adapter.getCount() > 0 ) listView.setSelectionFromTop( 0, 0 ); + break; + + case R.id.btnColumnSetting: + llColumnSetting.setVisibility( llColumnSetting.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE ); break; } @@ -177,16 +223,15 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S private void showError( String message ){ tvLoading.setVisibility( View.VISIBLE ); tvLoading.setText( message ); - + swipyRefreshLayout.setVisibility( View.GONE ); // ロード完了後に先頭から表示させる if( status_adapter.getCount() > 0 ){ - listView.setSelectionFromTop( 0,0 ); + listView.setSelectionFromTop( 0, 0 ); } } - - + // final RelationshipMap.UpdateCallback callback_relation = new RelationshipMap.UpdateCallback() { // @Override public void onRelationShipUpdate(){ // onVisualColumn(); @@ -229,28 +274,32 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S } } - if( column. list_data.isEmpty() && vh_header == null ){ + if( column.list_data.isEmpty() && vh_header == null ){ showError( activity.getString( R.string.list_empty ) ); }else{ tvLoading.setVisibility( View.GONE ); swipyRefreshLayout.setVisibility( View.VISIBLE ); status_adapter.set( column.list_data ); + if( column.scroll_hack > 0 ){ + listView.setSelectionFromTop( column.scroll_hack-1,-(int)(0.5f + 80f * activity.density )); + column.scroll_hack = 0; + } } proc_restore_scroll.run(); } - @Override - public void onRefresh( SwipyRefreshLayoutDirection direction ){ - if( ! column.startRefresh( direction == SwipyRefreshLayoutDirection.BOTTOM ) ){ + @Override public void onRefresh( SwipyRefreshLayoutDirection direction ){ + String error = column.startRefresh( direction == SwipyRefreshLayoutDirection.BOTTOM ); + if( ! TextUtils.isEmpty( error ) ){ swipyRefreshLayout.setRefreshing( false ); + Utils.showToast( activity, true, error ); } } private final Runnable proc_restore_scroll = new Runnable() { - @Override - public void run(){ + @Override public void run(){ if( column.scroll_pos == 0 && column.scroll_y == 0 ){ return; } @@ -357,29 +406,29 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S @Override public void onClick( View v ){ switch( v.getId() ){ - + case R.id.ivBackground: if( who != null ){ // 強制的にブラウザで開く activity.openChromeTab( access_info, who.url, true ); } break; - + case R.id.btnFollowing: column.profile_tab = Column.TAB_FOLLOWING; column.reload(); break; - + case R.id.btnFollowers: column.profile_tab = Column.TAB_FOLLOWERS; column.reload(); break; - + case R.id.btnStatusCount: column.profile_tab = Column.TAB_STATUS; column.reload(); break; - + case R.id.btnMore: activity.openAccountMoreMenu( access_info, who ); break; @@ -483,6 +532,8 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S View llSearchTag; Button btnSearchTag; + TootGap gap; + StatusViewHolder( View view ){ this.llBoosted = view.findViewById( R.id.llBoosted ); @@ -490,7 +541,7 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S this.tvBoosted = (TextView) view.findViewById( R.id.tvBoosted ); this.tvBoostedTime = (TextView) view.findViewById( R.id.tvBoostedTime ); this.tvBoostedAcct = (TextView) view.findViewById( R.id.tvBoostedAcct ); - + this.llFollow = view.findViewById( R.id.llFollow ); this.ivFollow = (NetworkImageView) view.findViewById( R.id.ivFollow ); this.tvFollowerName = (TextView) view.findViewById( R.id.tvFollowerName ); @@ -502,8 +553,8 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S this.ivThumbnail = (NetworkImageView) view.findViewById( R.id.ivThumbnail ); this.tvName = (TextView) view.findViewById( R.id.tvName ); this.tvTime = (TextView) view.findViewById( R.id.tvTime ); - this.tvAcct =(TextView) view.findViewById( R.id.tvAcct ); - + this.tvAcct = (TextView) view.findViewById( R.id.tvAcct ); + this.llContentWarning = view.findViewById( R.id.llContentWarning ); this.tvContentWarning = (TextView) view.findViewById( R.id.tvContentWarning ); this.btnContentWarning = (Button) view.findViewById( R.id.btnContentWarning ); @@ -565,7 +616,7 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S if( item == null ) return; if( item instanceof String ){ - showSearchTag( (String) item); + showSearchTag( (String) item ); }else if( item instanceof TootAccount ){ showFollow( (TootAccount) item ); }else if( item instanceof TootNotification ){ @@ -619,13 +670,23 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S }else{ showStatus( activity, status, access_info ); } + }else if( item instanceof TootGap ){ + showGap( (TootGap) item ); } } private void showSearchTag( String tag ){ + this.gap = null; search_tag = tag; llSearchTag.setVisibility( View.VISIBLE ); - btnSearchTag.setText( "#"+tag); + btnSearchTag.setText( "#" + tag ); + } + + private void showGap( TootGap gap ){ + this.gap = gap; + search_tag = null; + llSearchTag.setVisibility( View.VISIBLE ); + btnSearchTag.setText( activity.getString( R.string.read_gap ) ); } void showBoost( TootAccount who, long time, int icon_id, CharSequence text ){ @@ -633,7 +694,7 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S llBoosted.setVisibility( View.VISIBLE ); ivBoosted.setImageResource( icon_id ); tvBoostedTime.setText( TootStatus.formatTime( time ) ); - tvBoostedAcct.setText(access_info.getFullAcct( who ) ); + tvBoostedAcct.setText( access_info.getFullAcct( who ) ); tvBoosted.setText( text ); } @@ -645,21 +706,21 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S tvFollowerAcct.setText( access_info.getFullAcct( who ) ); btnFollow.setImageResource( R.drawable.btn_follow ); - + } private void showStatus( ActMain activity, TootStatus status, SavedAccount account ){ this.status = status; account_thumbnail = status.account; llStatus.setVisibility( View.VISIBLE ); - - tvAcct.setText( account.getFullAcct( status.account ) ); - tvTime.setText( TootStatus.formatTime( status.time_created_at ) ); - + + tvAcct.setText( account.getFullAcct( status.account ) ); + tvTime.setText( TootStatus.formatTime( status.time_created_at ) ); + tvName.setText( status.account.display_name ); ivThumbnail.setImageUrl( status.account.avatar_static, App1.getImageLoader() ); tvContent.setText( status.decoded_content ); - + // if( status.decoded_tags == null ){ // tvTags.setVisibility( View.GONE ); // }else{ @@ -812,9 +873,18 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S case R.id.btnFollow: activity.openAccountMoreMenu( access_info, account_follow ); break; - + case R.id.btnSearchTag: - activity.openHashTag( access_info,search_tag ); + if( search_tag != null ){ + activity.openHashTag( access_info, search_tag ); + }else if( gap != null ){ + String error = column.startGap( gap ); + if( ! TextUtils.isEmpty( error ) ){ + Utils.showToast( activity, true, error ); + }else{ + swipyRefreshLayout.setRefreshing( true ); + } + } break; } } @@ -824,7 +894,7 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S TootAttachment a = status.media_attachments.get( i ); String sv; - if( Pref.pref(activity).getBoolean( Pref.KEY_PRIOR_LOCAL_URL,false )){ + if( Pref.pref( activity ).getBoolean( Pref.KEY_PRIOR_LOCAL_URL, false ) ){ sv = a.url; if( TextUtils.isEmpty( sv ) ){ sv = a.remote_url; @@ -841,7 +911,6 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S } } - } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java index 42c00c86..0f7b2c1f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.java @@ -1,9 +1,9 @@ package jp.juggler.subwaytooter.api.entity; +import android.support.annotation.NonNull; import android.text.Spannable; import android.text.TextUtils; -import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.Emojione; import org.json.JSONArray; @@ -113,14 +113,16 @@ public class TootAccount { return parse( log, account, src, new TootAccount() ); } - public static List parseList( LogCategory log, LinkClickContext account,JSONArray array ){ + @NonNull public static List parseList( LogCategory log, LinkClickContext account, JSONArray array ){ List result = new List(); if( array != null ){ - for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + int array_size = array.length(); + result.ensureCapacity( array_size ); + for( int i=0;i= 0 ; -- i ){ - JSONObject src = array.optJSONObject( i ); - if( src == null ) continue; - TootAttachment item = parse( log, src ); - if( item != null ) result.add( 0, item ); + if( array != null ){ + int array_size = array.length(); + result.ensureCapacity( array_size ); + for( int i=0;i { } + // URL of user's profile (can be remote) public String url; @@ -41,15 +44,17 @@ public class TootMention { } } - + @NonNull public static List parseList( LogCategory log, JSONArray array ){ List result = new List(); if( array != null ){ - for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + int array_size = array.length(); + result.ensureCapacity( array_size ); + for( int i = 0 ; i < array_size ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; TootMention item = parse( log, src ); - if( item != null ) result.add( 0, item ); + if( item != null ) result.add( item ); } } return result; diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java index c2be4e53..a9098aba 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java @@ -1,5 +1,7 @@ package jp.juggler.subwaytooter.api.entity; +import android.support.annotation.NonNull; + import org.json.JSONArray; import org.json.JSONObject; @@ -43,8 +45,8 @@ public class TootNotification extends TootId { dst.id = src.optLong( "id" ); dst.type = Utils.optStringX( src, "type" ); dst.created_at = Utils.optStringX( src, "created_at" ); - dst.account = TootAccount.parse( log, accopunt, src.optJSONObject( "account" ) ); - dst.status = TootStatus.parse( log, accopunt, src.optJSONObject( "status" ) ); + dst.account = TootAccount.parse( log, accopunt, src.optJSONObject( "account" ) ); + dst.status = TootStatus.parse( log, accopunt, src.optJSONObject( "status" ) ); dst.time_created_at = TootStatus.parseTime( log, dst.created_at ); @@ -66,14 +68,17 @@ public class TootNotification extends TootId { } } - public static List parseList( LogCategory log, LinkClickContext accopunt,JSONArray array ){ + @NonNull + public static List parseList( LogCategory log, LinkClickContext account, JSONArray array ){ List result = new List(); if( array != null ){ - for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + int array_size = array.length(); + result.ensureCapacity( array_size ); + for( int i = 0 ; i < array_size ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; - TootNotification item = parse( log, accopunt,src ); - if( item != null ) result.add( 0, item ); + TootNotification item = parse( log, account, src ); + if( item != null ) result.add( item ); } } return result; diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java index 17ee2bb3..b1079be7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootRelationShip.java @@ -1,5 +1,7 @@ package jp.juggler.subwaytooter.api.entity; +import android.support.annotation.NonNull; + import org.json.JSONArray; import org.json.JSONObject; @@ -40,7 +42,7 @@ public class TootRelationShip { return dst; }catch( Throwable ex ){ ex.printStackTrace(); - log.e(ex,"TootRelationShip.parse failed."); + log.e( ex, "TootRelationShip.parse failed." ); return null; } } @@ -55,17 +57,19 @@ public class TootRelationShip { } } + @NonNull public static List parseList( LogCategory log, JSONArray array ){ List result = new List(); if( array != null ){ - for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + int array_size = array.length(); + result.ensureCapacity( array_size ); + for( int i = 0 ; i < array_size ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; TootRelationShip item = parse( log, src ); - if( item != null ) result.add( 0, item ); + if( item != null ) result.add( item ); } } return result; } - } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java index 073ed61a..11895f29 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReport.java @@ -1,5 +1,7 @@ package jp.juggler.subwaytooter.api.entity; +import android.support.annotation.NonNull; + import org.json.JSONArray; import org.json.JSONObject; @@ -8,7 +10,7 @@ import java.util.ArrayList; import jp.juggler.subwaytooter.util.LogCategory; import jp.juggler.subwaytooter.util.Utils; -public class TootReport extends TootId{ +public class TootReport extends TootId { // The ID of the report //TootId public long id; @@ -34,14 +36,17 @@ public class TootReport extends TootId{ } + @NonNull public static List parseList( LogCategory log, JSONArray array ){ List result = new List(); if( array != null ){ - for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + int array_size = array.length(); + result.ensureCapacity( array_size ); + for( int i = 0 ; i < array_size ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; TootReport item = parse( log, src ); - if( item != null ) result.add( 0, item ); + if( item != null ) result.add( item ); } } return result; diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java index 33d300cf..89bb9f8b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootResults.java @@ -12,15 +12,15 @@ public class TootResults { // An array of matched Accounts public TootAccount.List accounts; - // An array of matchhed Statuses + // An array of matched Statuses public TootStatus.List statuses; // An array of matched hashtags, as strings public ArrayList< String > hashtags; - public static TootResults parse( LogCategory log, LinkClickContext account,JSONObject src ){ - if( src == null ) return null; + public static TootResults parse( LogCategory log, LinkClickContext account, JSONObject src ){ try{ + if( src == null ) return null; TootResults dst = new TootResults(); dst.accounts = TootAccount.parseList( log, account, src.optJSONArray( "accounts" ) ); dst.statuses = TootStatus.parseList( log, account, src.optJSONArray( "statuses" ) ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java index dd520716..18dc9f5c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.java @@ -1,5 +1,6 @@ package jp.juggler.subwaytooter.api.entity; +import android.support.annotation.NonNull; import android.text.Spannable; import android.text.TextUtils; @@ -117,10 +118,10 @@ public class TootStatus extends TootId { status.id = src.optLong( "id" ); status.uri = Utils.optStringX( src, "uri" ); status.url = Utils.optStringX( src, "url" ); - status.account = TootAccount.parse( log, account,src.optJSONObject( "account" ) ); + status.account = TootAccount.parse( log, account, src.optJSONObject( "account" ) ); status.in_reply_to_id = Utils.optStringX( src, "in_reply_to_id" ); // null status.in_reply_to_account_id = Utils.optStringX( src, "in_reply_to_account_id" ); // null - status.reblog = TootStatus.parse( log, account,src.optJSONObject( "reblog" ) ); + status.reblog = TootStatus.parse( log, account, src.optJSONObject( "reblog" ) ); status.content = Utils.optStringX( src, "content" ); status.created_at = Utils.optStringX( src, "created_at" ); // "2017-04-16T09:37:14.000Z" status.reblogs_count = src.optLong( "reblogs_count" ); @@ -136,12 +137,12 @@ public class TootStatus extends TootId { status.application = Utils.optStringX( src, "application" ); // null status.time_created_at = parseTime( log, status.created_at ); - status.decoded_content = HTMLDecoder.decodeHTML( account,status.content ); + status.decoded_content = HTMLDecoder.decodeHTML( account, status.content ); // status.decoded_tags = HTMLDecoder.decodeTags( account,status.tags ); - status.decoded_mentions = HTMLDecoder.decodeMentions(account, status.mentions ); - - if( !TextUtils.isEmpty( status.spoiler_text ) ){ - status.decoded_spoiler_text = HTMLDecoder.decodeHTML(account, status.spoiler_text ); + status.decoded_mentions = HTMLDecoder.decodeMentions( account, status.mentions ); + + if( ! TextUtils.isEmpty( status.spoiler_text ) ){ + status.decoded_spoiler_text = HTMLDecoder.decodeHTML( account, status.spoiler_text ); } return status; }catch( Throwable ex ){ @@ -151,14 +152,17 @@ public class TootStatus extends TootId { } } - public static List parseList( LogCategory log, LinkClickContext account,JSONArray array ){ + @NonNull + public static List parseList( LogCategory log, LinkClickContext account, JSONArray array ){ List result = new List(); if( array != null ){ - for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + int array_size = array.length(); + result.ensureCapacity( array_size ); + for( int i = 0 ; i < array_size ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; - TootStatus item = parse( log,account, src ); - if( item != null ) result.add( 0, item ); + TootStatus item = parse( log, account, src ); + if( item != null ) result.add( item ); } } return result; diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootTag.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootTag.java index 2d86c270..3386d693 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootTag.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootTag.java @@ -1,5 +1,7 @@ package jp.juggler.subwaytooter.api.entity; +import android.support.annotation.NonNull; + import org.json.JSONArray; import org.json.JSONObject; @@ -16,36 +18,37 @@ public class TootTag { // The URL of the hashtag public String url; - public static TootTag parse( LogCategory log, JSONObject src ){ if( src == null ) return null; try{ TootTag dst = new TootTag(); - dst.name = Utils.optStringX(src, "name" ); - dst.url = Utils.optStringX(src, "url" ); + dst.name = Utils.optStringX( src, "name" ); + dst.url = Utils.optStringX( src, "url" ); return dst; }catch( Throwable ex ){ ex.printStackTrace(); - log.e(ex,"TootTag.parse failed."); + log.e( ex, "TootTag.parse failed." ); return null; } } - public static class List extends ArrayList{ + public static class List extends ArrayList< TootTag > { } - public static TootTag.List parseList( LogCategory log, JSONArray array ){ - TootTag.List result = new TootTag.List(); + @NonNull + public static List parseList( LogCategory log, JSONArray array ){ + List result = new List(); if( array != null ){ - for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + int array_size = array.length(); + result.ensureCapacity( array_size ); + for( int i = 0 ; i < array_size ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; TootTag item = parse( log, src ); - if( item != null ) result.add( 0, item ); + if( item != null ) result.add( item ); } } return result; } - } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java b/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java index 101f8875..19799509 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/Emojione.java @@ -24,8 +24,10 @@ public abstract class Emojione void closeSpan(){ if( last_span_start >= 0 ){ - CalligraphyTypefaceSpan typefaceSpan = new CalligraphyTypefaceSpan( App1.typeface_emoji ); - sb.setSpan(typefaceSpan, last_span_start,last_span_end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if( last_span_end > last_span_start ){ + CalligraphyTypefaceSpan typefaceSpan = new CalligraphyTypefaceSpan( App1.typeface_emoji ); + sb.setSpan( typefaceSpan, last_span_start, last_span_end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); + } last_span_start = -1; } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.java b/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.java index 959842c1..9e828473 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.java @@ -148,12 +148,14 @@ public class HTMLDecoder { if( DEBUG_HTML_PARSER ) sb.append( "(start " + tag + ")" ); int start = sb.length(); - + for( Node child : child_nodes ){ child.encodeSpan( account,sb ); } + + int end = sb.length(); - if( "a".equals( tag ) ){ + if( end >start && "a".equals( tag ) ){ Matcher m = reHref.matcher( text ); if( m.find() ){ final String href = decodeEntity( m.group( 1 ) ).toString(); @@ -165,7 +167,7 @@ public class HTMLDecoder { link_callback.onClickLink( account,href ); } } - }, start, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); + }, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); } } } @@ -216,14 +218,17 @@ public class HTMLDecoder { int start = sb.length(); sb.append('#'); sb.append(item.name); - final String item_url = item.url; - sb.setSpan( new ClickableSpan() { - @Override public void onClick( View widget ){ - if( link_callback != null ){ - link_callback.onClickLink( account,item_url ); + int end = sb.length(); + if( end > start ){ + final String item_url = item.url; + sb.setSpan( new ClickableSpan() { + @Override public void onClick( View widget ){ + if( link_callback != null ){ + link_callback.onClickLink( account,item_url ); + } } - } - }, start, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); + }, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); + } } return sb; } @@ -236,14 +241,17 @@ public class HTMLDecoder { int start = sb.length(); sb.append('@'); sb.append( item.acct ); - final String item_url = item.url; - sb.setSpan( new ClickableSpan() { - @Override public void onClick( View widget ){ - if( link_callback != null ){ - link_callback.onClickLink( account,item_url ); + int end = sb.length(); + if( end > start){ + final String item_url = item.url; + sb.setSpan( new ClickableSpan() { + @Override public void onClick( View widget ){ + if( link_callback != null ){ + link_callback.onClickLink( account,item_url ); + } } - } - }, start, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); + }, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); + } } return sb; } diff --git a/app/src/main/res/drawable-hdpi/ic_block.png b/app/src/main/res/drawable-hdpi/ic_block.png new file mode 100644 index 0000000000000000000000000000000000000000..1eb2069402c65ea6e81b1f986b50469caff9a28a GIT binary patch literal 971 zcmV;+12p`JP)<_p>-H^F%Qs!Idzx6zj zN;o9<=H@2Xf(-;pnw&g2dCp7DU09g6d7I5P91d@kN~Psyv$=%dGY__2uRkjmi!JK= z1pK&ME{~{>(P`|Q#*QkL3jSL!6besg;!r^lJS0Gy1oAWW-!VJZbZGs9)PZNS*?XkL zZQA^&1#sSf&<$68wIGQ)1{>$(nBbrXc%T7(O8>25j;TBx3Ww3z z4$182a=Ba1G_L|rW8dL*;3{op10=0)t858}aB>qQXji3mqQr`;8-_R1P--iw;HrZ2 zAHiwIPGO?632VcKtE9!3T=O2lN4esWm5b%n0q0536QAJ4O?3EpY$`PxjYkU2y!J^O z?1yooyfzPZu_88|_$?Fu2}zkp&Hn{I^-c;G+c9>VB=C$ae&v%kGI+^>GmJ-ADI~Nr5p)u^K@I<$PGc0#e?%uLx!=!Xl4H!C(Cp}8-*du?}>4!I~Unuq5~wQ zjp6{ZtZ}GEOiCdavSFMscL8EsJ z*oo|aebPpXN2g5wrm=u(wfdMBq^60KPufV@Z*k?Ij2YL3_4t!glt8@EM(n$Sy;wfB zZ52ca+}DWGp;Pc;Pfnc+6c}u;vK~@OoVW&$k_|7bB;JFB^fU)(y8@4D2iLslJBfu0 zw|b~5EyA#qJod2`)eg>iA%B3}%!!6XR(Pnni4(+$IPYf6MT&xIho&Mouu0=1wFNrq zY(jia+5<3n5@fLefsWsJ+2KFDv02K_ONqPAG_N!4DDN}{gZG5^0&{KfF$8IRHI=V4 z;)6vm#@ClnR}8INyUHXD5gTa-&z%f`pP>SXGUmd+Zo*R`i`@|Ki6JN=5bjm2RqI31Ruu6;q7ecize6yDJd2_)5q%L9AAK;SNwKXETM$d7MW{6WZ~Kkh z!7|R??WWn=d$@t&V6&OqZ|B>Y*}0{qTyn`Jm&{7H+g-2KYG13>>c%4M_51y+u+?d` zT6a=ee7#=((P%Wz@%&jXmp3nKvrs5phrLtSJHvAbtxl)YWhVgek`XOFQUYP?33%_N z1c2GygGZja0B|1*IAkyTWe6gm#Y1OF07z*bp6_7)+Co8Ku@3|giFAqby9+l}hCa<{t>& zn0TWT?VbuE$u7+gBN7Mk-pRo>p_2;2#4dQh(rh-jgo9o57M&sxod2QzC-CGg%-`qd z!AYL5p_33szv!YE?KnT!cvr(!qO*!lY97EMk3pHo8W5kD&5_P)u_DE36OBuDERmf^ zp1}Vfsa~M6W3Jk1UI1~)PRPVVVRr-ke_OQ&Z}n0Iprg}ZFt~!<-ARfx7=Ohg(#O;R zBt<9XkbV$o^o#-6@dCgk*-^D*8cJAv^IKu0H(*sGeW>?{)$vQ2hTM&`FRp8%|Hc9`=b6Ja!}MlEl4l*qoZdZ_0W zfKBa0*G~MltCkYbyNj;n0vLa717My~{8Gi72?!W}qr0H%Ze+GMJ7~>i|LY$J8vr(R z!q`n`jW1I=IqEy5M<*A3r&Q^bl(1;mO=_pqKyW%qb7<#vjjhblNr>^ousba+`YdyF zGHvcc-%`s_Iul;$WYS)z7GpH0-EQCG=Yl@6`1;5b=ss{zhNUDW#d$Tv(v6ME>r8yI s3eUjvuuT#e^qoeTS$rQ*vHU%JrIB=Ek=3D?Ne`vibsk7$6pu8*8WZDIp>j=3Il003(O*mu&L-LIrmV$A?xiG;kQX_^ZK!0A%6 zxKLhbw+=W7};sQ+h?*kVq(S=4`WdtA}7ouFnw~6gXP&O`vIRF3zL6BVX Y10(06qh=%~F#rGn07*qoM6N<$g3nckivR!s literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_block.png b/app/src/main/res/drawable-mdpi/ic_block.png new file mode 100644 index 0000000000000000000000000000000000000000..d2dcdbde1122a2bc9e1f6f7656cf406031dd6947 GIT binary patch literal 604 zcmV-i0;BzjP)?;)MjmnSkXeQTPeQGa@&{o1p*J`I6ma(rmZe?sC#5 z6WX#f-|Xx+kJf7cSB1;-ypu2t>zz)=hu|LX_6q9z{w0Bp2y7We(T_^#zc-R0$%QlE zMx${`2B##uJYa<*yC4W2p=$-6BuSHuePoQ|c}Bkjibw*%J;|KzKQn9BBsZ17Z|NfC zEi<;nyEIJ?^K^Q>-Vv>f$&nS)`=aNt6gVj~IXK}?VBA!nbdSl#D|f;;5^)@#@vzfb z_RB_(^zO)k@9c3Hae@?|m{Mv3j$a-L5VL6aSCZ{WsWM?#f#X>m$4X|0HbrUCn0Xu7 zf-_FUEUH5tdERBJEf$Ld7vM5GWQroPx#W{xf(Nd^+2-cbzOrn$+wE(XcK0s9g#!b2 z40V;UfTc~>CAi40ghklbI)IAj65PlEegwMWQw7}U2uw&=6R}t$xb$?69}(hcNe__?^FXU#yhLY7@Pnxvm;i~SPq9!+?;(MAVpCp_{6IK#yl~` q+P!`yVP~Rupn(Orce>j@b^QVh`FXi}nqIE}0000((^e=vCn(o5yye!M@r2r7P!9+j!eKkQ)Q0X}p zeqT`(T@41wvTPrdyqO>Mjc>mcdfDVH6(w1G(->m~^AdVG)Tk+n9ixn`YZ4#!lEifHXA;M25BIHzTNLHSd zmM?~0(Nn&B+5q}wOgf57)e1U@z8O=l9jrnOttYHOv|Yzo@|&)-Z54uTzVELg@S$9g z{0l@^Nrryud^=1@y}Y=H%qCT$D{nNeOB5 zeVd`DaQ#BbqQ?{5_AQ#vT=3*;Q3-p4kep>I+tHf>mfI&xPG#TEyy0nV!iCTU(g~TF znl64b4QFw$V4HB6A#i2&35$jsH9ga~o!m|{&YH%2XVBrP4VSPdZZ|`LQ z{HO|Cxb}w;u)zGX)qFxAjuFg@3V}ay?GMBJAnTFCm14oDV2o~$YUDBH`z!-r)-7jB za1!lMG5Sw!1YZu8jF!?E45l;#*ETtXEY|CVjV*-3RSW-lcyPiz7r>EzE!n;1U z4SdBc7*Xh)@}kJBLV=`>;LCdl*#?T4B`?ZsOBgNL3ciBRn&kUUx5_jqrqZCz;Ds3! zWC5G2L@Pd!+J6um4U^zSPO?c66STNdYDm(HC#C}*)8H#suTu%X0bkJ{7`(R~yv~2Q ziVE?;oNTXO3*ZI+bxA-pn+XfGPC4 z7QhQx8-7h-%A^wv6TBF}l0?q9uVs*1OE1e{*x*&mpsOGb)8(OwKqzeRLia|QJf2iL zX2R6ABAV*68N9JjSOD1Zx*3WJyy$=xiOel8ncx?)aJbrbI7;v$g?A>=d)hU#Y&M&A zn-GZ-ycqpq-}YbWO=V~P0P#td5{nuH@Eo8q z#qrnyOL$D#SHGJYps>JGFHQYn-sm&~sAZeLGb$BF^Rk(_7)0hasVjaIMG~1XY@Vmm z8uQyRHuWXJ(H#i+5@Cr<15c$j@{d`)7WC9>ku8mtOC;k2ljn!zMV`7URGwn9dMy!> z{k7<3LOEh&*zD&hjo%6$`&FtzIP%SjFp1{iq##;spqA*gL_R?mme%ndQBuF;q$-4k zTdZD7L_xwaF1sywW_pb|MkZRBZKm(wZ@M}_MJuv;EfF!*aXqe9&1tw;`jCZN37bg# zPheW-APoR#xz423{~O>2O6(#JOqR#9T*pt{ap=T+0+TAtEIFwvmw;yZ{46U~bQR_` zLky6ZZDz_{oIA|%1WFvk&55Z!#H0BzD$rtzDW;fW0{sDPT37C{?i)e?0000|3Dy+c6(SJjv5a|{15kMsbNiy_Ok+R>Ed=Un|0#HK0y|kY>;q^s4ggFbd{eCRu^0)_tX1pv0Z2Vg0IT9Rai8{Px3 zkwwa+Foe^H1%RdyaQ`{CWJp*Qwpnea-)ByXA+$JJWD@{3At3zMoL7YEKxm`TN{VrA zX9`Rqv{7hHlH^G=PcwngQjKvbnz!jfa9M5k5`ycJyLS*wO74{3FB}A&=Sk3@?;G0* zlot>xo>_*>eI(NP3j;x-?XRHkQ$@%x4g`b$0{;~|sOZTaLLC6~+bd1eOLTA&eLu5? zU0KL#{d8T literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_tune.png b/app/src/main/res/drawable-xhdpi/ic_tune.png new file mode 100644 index 0000000000000000000000000000000000000000..5408366ebb4069ff00e5f8792433fac4f9f4fa1b GIT binary patch literal 287 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=&pcfmLn>~)y>XHAkbwy6g+7(c z%?%0`9rm&gg5m=11<{Y!M|fz~nic6i(O>dVWYV7NrAj~`eb#{I{P~H+_ZUG2di>gF z@7gov(mt^TXKb&p+kHwtBH1Um?Byp^P?9+Y6U}6fHDfp7j)Jgm{V25^kAL*0x1@T1vf$5Khh3fFE5tZAFu|gW^i$FS+aBwgCn=2 UmBsE2Zi6a<2%P8g9PS|kB@&gHa2#g-p`DVj{ZWgi&P#`S)sB)Wu4#SvwI^W zBXjh-%k+9|XlUr@#Kgqgj*}V;r!qhcoTk_N#Kgbp{XcOEJ?9k<%%#D>!9F)2KI-f1 zdlNI27&MC+ZG!2$6+9a&hTdPL*KWX8VHr=j{RUuyn6Z>3(}lsnDyyL_MT40WnC)pZ zJ*2E*GC-K;T6-aZI!GN|ZpDmOToDiGeShw>DPsHq>f8?yh_*34NNCew+#$^PiCVJCtQ1+rjGOP7EgOu&#fH%0(K?31UiDwOTzy>g0a< z>^+pp$;tQWyMwGv)#$(H>GQk%-&pn2>xG`4p0_f^{NCQ)cZm_d#=&BGog(P^G*0L7 zUp#}Jb&l~p>WY|y@{k$4hoFrx+cxQcS7;_5rpL4;(6lzH;|SGtgX;M+!XPYJFBizt z{19$|2#h}q7)glnK?BhVc%-tWxjL5tf41uPojS~C7r{-em{zqJ%%rybtfLlV5Ug7X zjQPtN%zs3`@3Ex$SP=u%p2s?HWQ|OKb4yj=vK{a}&Ah#q+A51Wg4qh~h~30W+6}3Y zCue46UbhiKq25CG*A&bbTH0u8z&H#dg_-}HO4U~70nFDG$W1$;WyTtWu8KMbOBFt# zlr$V|g7Fq%=Ks%PPF*O&Sq1Gu(Wt80%KV716V%UJsXFke(Ib-T27yaP)mM3goe<@s zKb3};%jI{dvW7y#Zrdtwbzld-Q_+s#>PTfj0h)|L!yB*-asWG9_&vK<9!qeGO`W8n zr(J41mLnRSb=YBdbeE^^0}2-NTB6&VR6$*#@i+1SJNUidVpdcRny5$g{m}P676ol|5>3^XNm$l0qAgEsP}T*I-v8; z3!_{q3hV?-&YHlRITPN`P<5+ZE`L-c*zr#SNfU~-Q^uzcR|HAFzeup-vyTo6^*@ZI zpzvvy^{{A%H2ps|z>b&T`4v>rl~}uE>;u{ii|b5l=gSt@@l8;K$3F}63B`(_vtg|MiGCglgR6?OvJ z7}l=ExB**myDkU=FjOSi(J{o3+rmU8*98WAT_o7?A;d*ta$Ygv?>Hrx+dmZvc6^; z9S<^0Nda0WvF@5s*+LOu$EO`i2fwFHj&?-~$Ss%4ALJc&JYLYlQkV{cc}-e;d&7J} zx+9)d*lEswoah(YykYzpD-K*pm8$rKG7LLj7xbo}n9Lfl7?ECagLAVCJ03VbD@Da6 zGHE#dOhcNnyE*4F4m(ZN^MC>rFCh_OQqCvwcoO~Qdfs5iqYd6vIG~Z-F(tPCHWiX^ zbms|n7xQRC?0eMkdy-cs=1$F?fNHC%y-==Mt zEqkcJziAA@yp1W#pxwdWrMjY#rp=mU-y9*fs<8@I5;@EqUDN4O%mDMB%4uCP5)M*Y z)5);|w?!*c8=!8S`PGqQm^`gawHZo;pP%RqLQ^Kdi<%}Z@jmxyobK0b>k5+7mTV?g zPgf(TrHC+?*4oVSB>2{W~K#&QVg4yrPTYv?Mt-m-SF9@wf=p@cD^F=<9 ztak`pTN6-)YDZ8}<6PQ+kQb~b1b-m{4`GmCD6kul1*V_?QV^+(<5IkTeFmpD84h2> zyrWA`ZeUh%iQHWDF|z%#;LG}wPA|sFrtuj3izHmeZG|h{hrS!oWe;R7-$HJ zWoO>HM-6RWX^Vi_c|@jh2u;6@p4Ti>oA)y3rXl9N)VXA7rgle;96562$dMyQjx3^o Y0eBACEXD959RL6T07*qoM6N<$f|*ImS^xk5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_mute.png b/app/src/main/res/drawable-xxhdpi/ic_mute.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab226d8ca56019e46972603314c66a40181e5b9 GIT binary patch literal 1483 zcmV;+1vL7JP)1K2T@RK%N8$G1VvhJE7l*xS}j$I*4D1af9rda zfuU@ZmrRn*@jeLM!ffVEzIk7g_mXT7Fc=I5gTY`h7z_r3!C){LG!!b8%6TZm_&JI8 zb$55K&urtD%jFC3fK4Fa5Bwdf*X!$&WMD@}#}=^beNRu%1F*3+Ffgzqp)ZP2;}62a z{f3A8izOr>Ne|f9*S8gHn-6Wo&tT`HYPH(cXf)QQ{muU!d9Wx64UZ%gGzo~>`nk8a zcd%G2ZcbI}hqBY^p`xDPT8^fsrq;myE}Ie%vGr4_R63oSz5)oii-xcCP@d$dfQ^q{ z7>9sp0dJV7)oRBQPXNBzVC)?t2`=Oa8~<7;fwl+`WeLjF&d$yai6#NvRf7a;aumu= zwEwY8Kxp%8@bo>2_XIHZzDGGKEN^@l1VqXV-0L{YQ486j5+GiZ8tS`j;GmGCa-!!B zuH~qe>`)0<_M#ky>qnrTIUN$<`UUt~O*9D(<)|QQd@2D^;A|M|xD9pw(&!g{3Qx$F z9CdYd9aL$!slDhCV8<(!y-^Yp-zPYbBb;^j2pO zQxmpuEl0L(#)hTH5!n1D@gWNza^%8hsP`1qzewm0>ZypJx|Ac;b%t>$OtoHA*^8lU zD4SA2f-50ySR4Yi}uJXt7$Y?#d(qapXusKa3 zo8P#Y=N7hnFfUX>grHOO0x~L-t0p@ZvP4xgMKP2~_X95i5h&qEM^Z~O>fWu89P~S<} zC@7c1P@kJbR1-0{&sZJ_@U}?=Q>hAjMsi1h&qP1eGqvhTnWPcmE731K>~$v$8t22*0YDK z$Pv}+T*(m*PlL3=h8$@uDX}g`aNUX2Y|wBeN4kW2K42vKa%AFlt9;&3w&cj=8?v9K|6( zyt&i32UxaWSj?D5RqKNA=UjrEp%8-rJR7b~jpvKq@-2_<$6fd0+@M0hSeXE^8Dk7L zXPb1Itzt)_@dvr_TR@zc^#Ol32G?!kEI4jMMMK$QIU>LLiq6{lA*<*KC`a7*3V4Ume;uunK>+Ky||7Hi70}so- z<{BUS!6@O8;pO?GM2}A|Z0+p7+lqY67puyshu74dT+3Fu>fr-_ac<8^DnOLE%+z=D z|IA#o+0*l4ExZ{RIDm#SOo&a5jC;Ck)v8@@_zEQ%H^i>LzTNQn1epgvG*6b=$_cVD zUMc-6DUj5cc**|Z%5V$D<#w$r`x$}G0s({MhVL=q>>o@HrM}R6CNYV5OK$b#*IHeV ztjd(H{hhOWMsmUWgL@Sim>3*@_8s_c>++p@pWm*@f6l{9V3?C{Xl#7<@{cl5Y5yl5I4WNcAXdXxbs?m2Yf7T>)_Uf0L9j((H&amzj lWdwN#7<>~z0qZ%5ua - + + + + + + + + - - - - - - - - @@ -292,6 +296,7 @@ android:layout_height="match_parent" android:layout_marginStart="8dp" android:layout_weight="1" + android:background="@drawable/btn_bg_ddd" android:contentDescription="@string/thumbnail" android:scaleType="centerCrop" /> @@ -302,6 +307,7 @@ android:layout_height="match_parent" android:layout_marginStart="8dp" android:layout_weight="1" + android:background="@drawable/btn_bg_ddd" android:contentDescription="@string/thumbnail" android:scaleType="centerCrop" /> @@ -312,6 +318,7 @@ android:layout_height="match_parent" android:layout_marginStart="8dp" android:layout_weight="1" + android:background="@drawable/btn_bg_ddd" android:contentDescription="@string/thumbnail" android:scaleType="centerCrop" /> @@ -342,8 +349,8 @@ - + + + + + + + + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1be25bfc..263cd05e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -156,4 +156,11 @@ 添付画像を開く時に自タンスURLを重視 FastScrollerを無効にする(アプリ再起動が必要) 削除できました + 開発者 + 添付データあり + ギャップを読む + 別の処理を行っています + ブロックしたユーザ + リストの終端 + ミュートしたユーザ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index edcb0ad4..e59c168d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -156,4 +156,10 @@ disable fast scroller (app restart required) delete succeeded developer + with attachment + column is busy + read gap + mutes users + blocked users + end of list