diff --git a/.idea/dictionaries/tateisu.xml b/.idea/dictionaries/tateisu.xml index b74115e2..f8ff9336 100644 --- a/.idea/dictionaries/tateisu.xml +++ b/.idea/dictionaries/tateisu.xml @@ -2,6 +2,7 @@ adamrocker + apikey dont emoji emojione @@ -30,6 +31,7 @@ unfollow unmute unreblog + utoken \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 4bf913df..8eaf2eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "jp.juggler.subwaytooter" minSdkVersion 21 targetSdkVersion 25 - versionCode 93 - versionName "0.9.3" + versionCode 95 + versionName "0.9.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 414c74ba..013cec8c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java @@ -64,6 +64,7 @@ import jp.juggler.subwaytooter.api.entity.TootApplication; import jp.juggler.subwaytooter.api.entity.TootRelationShip; import jp.juggler.subwaytooter.api.entity.TootResults; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.entity.TootStatusLike; import jp.juggler.subwaytooter.dialog.AccountPicker; import jp.juggler.subwaytooter.dialog.DlgConfirm; import jp.juggler.subwaytooter.dialog.LoginForm; @@ -198,11 +199,16 @@ public class ActMain extends AppCompatActivity boolean bRemoved = false; for( int i = 0, ie = app_state.column_list.size() ; i < ie ; ++ i ){ Column column = app_state.column_list.get( i ); - SavedAccount sa = SavedAccount.loadAccount( log, column.access_info.db_id ); - if( sa == null ){ - bRemoved = true; + + if( column.access_info.isNA() ){ + // 検索カラムはアカウント削除とか無関係 }else{ - new_order.add( i ); + SavedAccount sa = SavedAccount.loadAccount( log, column.access_info.db_id ); + if( sa == null ){ + bRemoved = true; + }else{ + new_order.add( i ); + } } } if( bRemoved ){ @@ -607,6 +613,9 @@ public class ActMain extends AppCompatActivity }else if( id == R.id.nav_muted_word ){ startActivity( new Intent( this, ActMutedWord.class ) ); + }else if( id == R.id.mastodon_search_portal ){ + addColumn( getDefaultInsertPosition(), SavedAccount.getNA(),Column.TYPE_SEARCH_PORTAL , "" ); + // }else if( id == R.id.nav_translation ){ // Intent intent = new Intent(this, TransCommuActivity.class); // intent.putExtra(TransCommuActivity.APPLICATION_CODE_EXTRA, "FJlDoBKitg"); @@ -1356,7 +1365,7 @@ public class ActMain extends AppCompatActivity SavedAccount a = column.access_info; if( done_list.contains( a ) ) continue; done_list.add( a ); - a.reloadSetting(); + if( !a.isNA() ) a.reloadSetting(); column.fireShowColumnHeader(); } } @@ -1368,7 +1377,7 @@ public class ActMain extends AppCompatActivity if( ! Utils.equalsNullable( a.acct, account.acct ) ) continue; if( done_list.contains( a ) ) continue; done_list.add( a ); - a.reloadSetting(); + if( !a.isNA() ) a.reloadSetting(); column.fireShowColumnHeader(); } } @@ -1463,6 +1472,7 @@ public class ActMain extends AppCompatActivity } } ); } + public void performMuteApp( @NonNull TootApplication application ){ MutedApp.save( application.name ); @@ -1557,6 +1567,47 @@ public class ActMain extends AppCompatActivity try{ log.d( "openChromeTab url=%s", url ); + if( !noIntercept && access_info != null && access_info.isNA() ){ + // トゥート検索カラムではaccess_infoは何にも紐ついていない + + // ハッシュタグをアプリ内で開く + Matcher m = reHashTag.matcher( url ); + if( m.find() ){ + // https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0 + String host = m.group( 1 ); + String tag = Uri.decode( m.group( 2 ) ); + openHashTagOtherInstance( pos, access_info, url, host, tag ); + return; + } + + // ステータスページをアプリから開く + m = reStatusPage.matcher( url ); + if( m.find() ){ + try{ + // https://mastodon.juggler.jp/@SubwayTooter/(status_id) + final String host = m.group( 1 ); + final long status_id = Long.parseLong( m.group( 3 ), 10 ); + openStatusOtherInstance( pos, access_info, url, host, status_id ); + return; + }catch( Throwable ex ){ + Utils.showToast( this, ex, "can't parse status id." ); + } + return; + } + + // ユーザページをアプリ内で開く + m = reUserPage.matcher( url ); + if( m.find() ){ + // https://mastodon.juggler.jp/@SubwayTooter + final String host = m.group( 1 ); + final String user = Uri.decode( m.group( 2 ) ); + + openProfileByHostUser( pos,access_info,url,host,user ); + return; + } + + } + if( ! noIntercept && access_info != null ){ // ハッシュタグをアプリ内で開く Matcher m = reHashTag.matcher( url ); @@ -1635,15 +1686,19 @@ public class ActMain extends AppCompatActivity } } - public void openStatus( int pos, @NonNull SavedAccount access_info, @NonNull TootStatus status ){ - openStatus( pos, access_info, status.id ); + public void openStatus( int pos, @NonNull SavedAccount access_info, @NonNull TootStatusLike status ){ + if( access_info.host.equalsIgnoreCase( status.status_host ) ){ + openStatus( pos, access_info, status.id ); + }else{ + openStatusOtherInstance( pos, access_info,status.url,status.status_host,status.id); + } } public void openStatus( int pos, @NonNull SavedAccount access_info, long status_id ){ addColumn( pos, access_info, Column.TYPE_CONVERSATION, status_id ); } - private void openStatusOtherInstance( final int pos, final SavedAccount access_info, final String url, final String host, final long status_id ){ + void openStatusOtherInstance( final int pos, final SavedAccount access_info, final String url, final String host, final long status_id ){ ActionsDialog dialog = new ActionsDialog(); // ブラウザで表示する @@ -1851,7 +1906,7 @@ public class ActMain extends AppCompatActivity if( result != null && result.object != null ){ - TootResults tmp = TootResults.parse( log, access_info, result.object ); + TootResults tmp = TootResults.parse( log, access_info, access_info.host, result.object ); if( tmp != null ){ if( tmp.accounts != null && ! tmp.accounts.isEmpty() ){ who_local = tmp.accounts.get( 0 ); @@ -1974,7 +2029,7 @@ public class ActMain extends AppCompatActivity public void performFavourite( final SavedAccount access_info - , final TootStatus arg_status + , final TootStatusLike arg_status , final int nCrossAccountMode , final boolean bSet , final RelationChangedCallback callback @@ -2002,7 +2057,7 @@ public class ActMain extends AppCompatActivity client.setAccount( access_info ); TootApiResult result; - TootStatus target_status; + TootStatusLike target_status; if( nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE ){ // 検索APIに他タンスのステータスのURLを投げると、自タンスのステータスを得られる String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( arg_status.url ) ); @@ -2013,7 +2068,7 @@ public class ActMain extends AppCompatActivity return result; } target_status = null; - TootResults tmp = TootResults.parse( log, access_info, result.object ); + TootResults tmp = TootResults.parse( log, access_info, access_info.host,result.object ); if( tmp != null ){ if( tmp.statuses != null && ! tmp.statuses.isEmpty() ){ target_status = tmp.statuses.get( 0 ); @@ -2043,7 +2098,7 @@ public class ActMain extends AppCompatActivity ) , request_builder ); if( result != null && result.object != null ){ - new_status = TootStatus.parse( log, access_info, result.object ); + new_status = TootStatus.parse( log, access_info, access_info.host, result.object ); } return result; @@ -2108,7 +2163,7 @@ public class ActMain extends AppCompatActivity public void performBoost( final SavedAccount access_info - , final TootStatus arg_status + , final TootStatusLike arg_status , final int nCrossAccountMode , final boolean bSet , final boolean bConfirmed @@ -2172,7 +2227,7 @@ public class ActMain extends AppCompatActivity TootApiResult result; - TootStatus target_status; + TootStatusLike target_status; if( nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE ){ // 検索APIに他タンスのステータスのURLを投げると、自タンスのステータスを得られる String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( arg_status.url ) ); @@ -2183,7 +2238,7 @@ public class ActMain extends AppCompatActivity return result; } target_status = null; - TootResults tmp = TootResults.parse( log, access_info, result.object ); + TootResults tmp = TootResults.parse( log, access_info, access_info.host,result.object ); if( tmp != null ){ if( tmp.statuses != null && ! tmp.statuses.isEmpty() ){ target_status = tmp.statuses.get( 0 ); @@ -2213,7 +2268,7 @@ public class ActMain extends AppCompatActivity // reblog,unreblog のレスポンスは信用ならんのでステータスを再取得する result = client.request( "/api/v1/statuses/" + target_status.id ); if( result != null && result.object != null ){ - new_status = TootStatus.parse( log, access_info, result.object ); + new_status = TootStatus.parse( log, access_info, access_info.host,result.object ); } } @@ -2276,14 +2331,18 @@ public class ActMain extends AppCompatActivity public void performReply( final SavedAccount access_info , final TootStatus arg_status - , final boolean bRemote ){ - if( ! bRemote ){ - ActPost.open( this, REQUEST_CODE_POST, access_info.db_id, arg_status ); - return; - } + ActPost.open( this, REQUEST_CODE_POST, access_info.db_id, arg_status ); + } + + public void performReplyRemote( + final SavedAccount access_info + ,final String remote_status_url + ,final long remote_status_id + ){ + final ProgressDialog progress = new ProgressDialog( this ); - new AsyncTask< Void, Void, TootApiResult >() { + final AsyncTask< Void, Void, TootApiResult > task = new AsyncTask< Void, Void, TootApiResult >() { TootStatus target_status; @Override protected TootApiResult doInBackground( Void... params ){ @@ -2298,15 +2357,15 @@ public class ActMain extends AppCompatActivity client.setAccount( access_info ); // 検索APIに他タンスのステータスのURLを投げると、自タンスのステータスを得られる - String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( arg_status.url ) ); + String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( remote_status_url ) ); path = path + "&resolve=1"; TootApiResult result = client.request( path ); if( result != null && result.object != null ){ - TootResults tmp = TootResults.parse( log, access_info, result.object ); + TootResults tmp = TootResults.parse( log, access_info, access_info.host,result.object ); if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){ target_status = tmp.statuses.get( 0 ); - log.d( "status id conversion %s => %s", arg_status.id, target_status.id ); + log.d( "status id conversion %s => %s", remote_status_id, target_status.id ); } if( target_status == null ){ return new TootApiResult( getString( R.string.status_id_conversion_failed ) ); @@ -2322,6 +2381,7 @@ public class ActMain extends AppCompatActivity @Override protected void onPostExecute( TootApiResult result ){ + progress.dismiss(); if( result == null ){ // cancelled. }else if( target_status != null ){ @@ -2330,7 +2390,18 @@ public class ActMain extends AppCompatActivity Utils.showToast( ActMain.this, true, result.error ); } } - }.executeOnExecutor( App1.task_executor ); + }; + + progress.setIndeterminate( true ); + progress.setCancelable( true ); + progress.setMessage( getString(R.string.progress_synchronize_toot) ); + progress.setOnCancelListener( new DialogInterface.OnCancelListener() { + @Override public void onCancel( DialogInterface dialog ){ + task.cancel( true ); + } + } ); + progress.show(); + task.executeOnExecutor( App1.task_executor ); } //////////////////////////////////////// @@ -3251,7 +3322,7 @@ public class ActMain extends AppCompatActivity } } - void openBoostFromAnotherAccount( @NonNull final SavedAccount timeline_account, final TootStatus status ){ + void openBoostFromAnotherAccount( @NonNull final SavedAccount timeline_account, @Nullable final TootStatusLike status ){ if( status == null ) return; AccountPicker.pick( this, false, false , getString( R.string.account_picker_boost ) @@ -3270,7 +3341,7 @@ public class ActMain extends AppCompatActivity } ); } - void openFavouriteFromAnotherAccount( @NonNull final SavedAccount timeline_account, final TootStatus status ){ + void openFavouriteFromAnotherAccount( @NonNull final SavedAccount timeline_account, final TootStatusLike status ){ if( status == null ) return; AccountPicker.pick( this, false, false , getString( R.string.account_picker_favourite ) @@ -3288,25 +3359,38 @@ public class ActMain extends AppCompatActivity } ); } - void openReplyFromAnotherAccount( @NonNull final SavedAccount access_info, final TootStatus status ){ - if( status == null ) return; + + void openReplyFromAnotherAccount( final TootStatusLike status){ AccountPicker.pick( this, false, false , getString( R.string.account_picker_reply ) , makeAccountListNonPseudo( log ), new AccountPicker.AccountPickerCallback() { @Override public void onAccountPicked( @NonNull SavedAccount ai ){ - performReply( - ai - , status - , ! ai.host.equalsIgnoreCase( access_info.host ) - ); + if( (status instanceof TootStatus) && ai.host.equalsIgnoreCase( status.status_host ) ){ + performReply( ai, (TootStatus)status ); + }else{ + performReplyRemote( ai,status.url,status.id ); + } } } ); } - void openFollowFromAnotherAccount( @NonNull SavedAccount access_info, TootStatus status ){ - if( status == null ) return; - openFollowFromAnotherAccount( access_info, status.account ); - } +// void openReplyFromAnotherAccount( @NonNull final SavedAccount access_info, final String status_url,final long status_id ){ +// +// final String status_host = getHostFromStatusUrl(status_url); +// if( status_host ==null ) return; +// +// AccountPicker.pick( this, false, false +// , getString( R.string.account_picker_reply ) +// , makeAccountListNonPseudo( log ), new AccountPicker.AccountPickerCallback() { +// @Override public void onAccountPicked( @NonNull SavedAccount ai ){ +// performReplyRemote( ai,status_url,status_id ); +// } +// } ); +// } +// void openFollowFromAnotherAccount( @NonNull SavedAccount access_info, TootStatus status ){ +// if( status == null ) return; +// openFollowFromAnotherAccount( access_info, status.account ); +// } void openFollowFromAnotherAccount( @NonNull SavedAccount access_info, final TootAccount account ){ if( account == null ) return; diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.java b/app/src/main/java/jp/juggler/subwaytooter/ActPost.java index d8637f7b..4b19f5b4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.java @@ -1,6 +1,7 @@ package jp.juggler.subwaytooter; import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.ProgressDialog; import android.content.ClipData; @@ -25,14 +26,12 @@ import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.Editable; -import android.text.Html; import android.text.Spannable; import android.text.TextUtils; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.view.View; import android.view.ViewGroup; -import android.view.ViewParent; import android.view.ViewTreeObserver; import android.widget.Button; import android.widget.CheckBox; @@ -47,7 +46,6 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -397,7 +395,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, sv = intent.getStringExtra( KEY_REPLY_STATUS ); if( sv != null ){ try{ - TootStatus reply_status = TootStatus.parse( log, account, new JSONObject( sv ) ); + TootStatus reply_status = TootStatus.parse( log, account, account.host,new JSONObject( sv ) ); // CW をリプライ元に合わせる if( ! TextUtils.isEmpty( reply_status.spoiler_text ) ){ @@ -898,7 +896,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, TootApiResult result = client.request( path ); if( result != null && result.object != null ){ - TootResults tmp = TootResults.parse( log, access_info, result.object ); + TootResults tmp = TootResults.parse( log, access_info, access_info.host,result.object ); if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){ target_status = tmp.statuses.get( 0 ); } @@ -1607,7 +1605,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, TootApiResult result = client.request( "/api/v1/statuses", request_builder ); if( result != null && result.object != null ){ - status = TootStatus.parse( log, account, result.object ); + status = TootStatus.parse( log, account, account.host,result.object ); Spannable s = status.decoded_content; MyClickableSpan[] span_list = s.getSpans( 0, s.length(), MyClickableSpan.class ); @@ -1982,7 +1980,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, StringBuilder sb = new StringBuilder(); sb.append( src.substring( 0, mushroom_start ) ); - int new_sel_start = sb.length(); + // int new_sel_start = sb.length(); sb.append( text ); int new_sel_end = sb.length(); sb.append( src.substring( mushroom_end ) ); @@ -2028,6 +2026,8 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, } } + + private void showRecommendedPlugin( String title ){ String language_code = getString( R.string.language_code ); int res_id; @@ -2038,46 +2038,37 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, }else{ res_id = R.raw.recommended_plugin_en; } - try{ - InputStream is = getResources().openRawResource( res_id ); - try{ - ByteArrayOutputStream bao = new ByteArrayOutputStream(); - IOUtils.copy( is, bao ); - String text = Utils.decodeUTF8( bao.toByteArray() ); - - View viewRoot = getLayoutInflater().inflate( R.layout.dlg_plugin_missing, null, false ); - - TextView tvText = (TextView) viewRoot.findViewById( R.id.tvText ); - LinkClickContext lcc = new LinkClickContext() { - @Override public AcctColor findAcctColor( String url ){ - return null; - } - }; - CharSequence sv = HTMLDecoder.decodeHTML( lcc, text, false, null ); - tvText.setText( sv ); - tvText.setMovementMethod( LinkMovementMethod.getInstance() ); - - TextView tvTitle = (TextView) viewRoot.findViewById( R.id.tvTitle ); - if( TextUtils.isEmpty( title ) ){ - tvTitle.setVisibility( View.GONE ); - }else{ - tvTitle.setText( title ); - + byte[] data = Utils.loadRawResource(this,res_id); + if( data != null ){ + String text = Utils.decodeUTF8( data ); + @SuppressLint("InflateParams") + View viewRoot = getLayoutInflater().inflate( R.layout.dlg_plugin_missing, null, false ); + + TextView tvText = (TextView) viewRoot.findViewById( R.id.tvText ); + LinkClickContext lcc = new LinkClickContext() { + @Override public AcctColor findAcctColor( String url ){ + return null; } + }; + CharSequence sv = HTMLDecoder.decodeHTML( lcc, text, false, null ); + tvText.setText( sv ); + tvText.setMovementMethod( LinkMovementMethod.getInstance() ); + + TextView tvTitle = (TextView) viewRoot.findViewById( R.id.tvTitle ); + if( TextUtils.isEmpty( title ) ){ + tvTitle.setVisibility( View.GONE ); + }else{ + tvTitle.setText( title ); - new AlertDialog.Builder( this ) - .setView( viewRoot ) - .setCancelable( true ) - .setNeutralButton( R.string.close, null ) - .show(); - - }finally{ - IOUtils.closeQuietly( is ); } - }catch( Throwable ex ){ - ex.printStackTrace(); + new AlertDialog.Builder( this ) + .setView( viewRoot ) + .setCancelable( true ) + .setNeutralButton( R.string.close, null ) + .show(); } + } final MyClickableSpan.LinkClickCallback link_click_listener = new MyClickableSpan.LinkClickCallback() { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActText.java b/app/src/main/java/jp/juggler/subwaytooter/ActText.java index bcd3e577..93186917 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActText.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActText.java @@ -13,6 +13,8 @@ import android.view.View; import android.widget.EditText; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.entity.TootStatusLike; +import jp.juggler.subwaytooter.api_msp.entity.MSPToot; import jp.juggler.subwaytooter.table.MutedWord; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.HTMLDecoder; @@ -25,7 +27,7 @@ public class ActText extends AppCompatActivity implements View.OnClickListener { static final String EXTRA_TEXT = "text"; static final String EXTRA_CONTENT_START = "content_start"; - static void encodeStatus( Intent intent, Context context, SavedAccount access_info, TootStatus status ){ + static void encodeStatus( Intent intent, Context context, SavedAccount access_info, TootStatusLike status ){ StringBuilder sb = new StringBuilder(); sb.append( context.getString( R.string.send_header_url ) ); sb.append( ": " ); @@ -33,7 +35,15 @@ public class ActText extends AppCompatActivity implements View.OnClickListener { sb.append( "\n" ); sb.append( context.getString( R.string.send_header_date ) ); sb.append( ": " ); - sb.append( TootStatus.formatTime( status.time_created_at ) ); + + if( status instanceof TootStatus ){ + TootStatus ts = (TootStatus)status; + sb.append( TootStatus.formatTime( ts.time_created_at ) ); + }else if( status instanceof MSPToot ){ + MSPToot ts = (MSPToot)status; + sb.append( ts.created_at ); + } + sb.append( "\n" ); sb.append( context.getString( R.string.send_header_from_acct ) ); sb.append( ": " ); @@ -63,7 +73,7 @@ public class ActText extends AppCompatActivity implements View.OnClickListener { } - public static void open( ActMain activity, SavedAccount access_info, TootStatus status ){ + public static void open( ActMain activity, SavedAccount access_info, TootStatusLike status ){ Intent intent = new Intent( activity, ActText.class ); encodeStatus( intent,activity, access_info, status ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java b/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java index eb682094..86e50b2b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java +++ b/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java @@ -636,7 +636,7 @@ public class AlarmService extends IntentService { return; } - TootNotification notification = TootNotification.parse( log, account, src ); + TootNotification notification = TootNotification.parse( log, account,account.host ,src ); if( notification == null ){ return; } diff --git a/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.java b/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.java index 05feab33..23702f68 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.java +++ b/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.java @@ -328,6 +328,7 @@ public class AppDataExporter { case Pref.KEY_STREAM_LISTENER_SECRET: case Pref.KEY_STREAM_LISTENER_CONFIG_DATA: case Pref.KEY_CLIENT_NAME: + case Pref.KEY_MASTODON_SEARCH_PORTAL_USER_TOKEN: String sv = reader.nextString(); e.putString( k, sv ); break; diff --git a/app/src/main/java/jp/juggler/subwaytooter/AppState.java b/app/src/main/java/jp/juggler/subwaytooter/AppState.java index 56988d4a..1b3fb5cf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/AppState.java +++ b/app/src/main/java/jp/juggler/subwaytooter/AppState.java @@ -27,6 +27,7 @@ import java.util.LinkedList; import java.util.regex.Pattern; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.entity.TootStatusLike; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.LogCategory; import jp.juggler.subwaytooter.util.MyClickableSpan; @@ -134,34 +135,34 @@ class AppState { private final HashSet< String > map_busy_fav = new HashSet<>(); - boolean isBusyFav( SavedAccount account, TootStatus status ){ - String busy_key = account.host + ":" + status.id; - return map_busy_fav.contains( busy_key ); + boolean isBusyFav( SavedAccount account, @NonNull TootStatusLike status ){ + final String key = account.acct +":" + status.status_host + ":" + status.id; + return map_busy_fav.contains( key ); } - boolean setBusyFav( SavedAccount account, TootStatus status ){ - final String busy_key = account.acct +":" + status.uri; - return map_busy_fav.add( busy_key ); + boolean setBusyFav( SavedAccount account, @NonNull TootStatusLike status ){ + final String key = account.acct +":" + status.status_host + ":" + status.id; + return map_busy_fav.add( key ); } - boolean resetBusyFav( SavedAccount account, TootStatus status ){ - final String busy_key = account.acct +":" + status.uri; - return map_busy_fav.remove( busy_key ); + boolean resetBusyFav( SavedAccount account, @NonNull TootStatusLike status ){ + final String key = account.acct +":" + status.status_host + ":" + status.id; + return map_busy_fav.remove( key ); } ////////////////////////////////////////////////////// private final HashSet< String > map_busy_boost = new HashSet<>(); - boolean isBusyBoost( @NonNull SavedAccount account, @NonNull TootStatus status ){ - final String busy_key = account.acct +":" + status.uri; - return map_busy_boost.contains( busy_key ); + boolean isBusyBoost( @NonNull SavedAccount account, @NonNull TootStatusLike status ){ + final String key = account.acct +":" + status.status_host + ":" + status.id; + return map_busy_boost.contains( key ); } - boolean setBusyBoost( SavedAccount account, TootStatus status ){ - final String busy_key = account.acct +":" + status.uri; - return map_busy_boost.add( busy_key ); + boolean setBusyBoost( SavedAccount account, @NonNull TootStatusLike status ){ + final String key = account.acct +":" + status.status_host + ":" + status.id; + return map_busy_boost.add( key ); } - boolean resetBusyBoost( SavedAccount account, TootStatus status ){ - final String busy_key = account.acct +":" + status.uri; - return map_busy_boost.remove( busy_key ); + boolean resetBusyBoost( SavedAccount account, @NonNull TootStatusLike status ){ + final String key = account.acct +":" + status.status_host + ":" + status.id; + return map_busy_boost.remove( key ); } ////////////////////////////////////////////////////// diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.java b/app/src/main/java/jp/juggler/subwaytooter/Column.java index 2fef09c9..f2546378 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Column.java +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.java @@ -35,6 +35,9 @@ import jp.juggler.subwaytooter.api.entity.TootReport; import jp.juggler.subwaytooter.api.entity.TootResults; import jp.juggler.subwaytooter.api.entity.TootStatus; import jp.juggler.subwaytooter.api.entity.TootTag; +import jp.juggler.subwaytooter.api_msp.MSPApiResult; +import jp.juggler.subwaytooter.api_msp.MSPClient; +import jp.juggler.subwaytooter.api_msp.entity.MSPToot; import jp.juggler.subwaytooter.table.AcctColor; import jp.juggler.subwaytooter.table.AcctSet; import jp.juggler.subwaytooter.table.MutedApp; @@ -162,6 +165,7 @@ class Column implements StreamReader.Callback { static final int TYPE_BOOSTED_BY = 14; static final int TYPE_FAVOURITED_BY = 15; static final int TYPE_DOMAIN_BLOCKS = 16; + static final int TYPE_SEARCH_PORTAL = 17; @NonNull final Context context; @NonNull private final AppState app_state; @@ -229,7 +233,10 @@ class Column implements StreamReader.Callback { this.search_query = (String) getParamAt( params, 0 ); this.search_resolve = (Boolean) getParamAt( params, 1 ); break; - + + case TYPE_SEARCH_PORTAL: + this.search_query = (String) getParamAt( params, 0 ); + break; } init(); } @@ -271,6 +278,9 @@ class Column implements StreamReader.Callback { item.put( KEY_SEARCH_QUERY, search_query ); item.put( KEY_SEARCH_RESOLVE, search_resolve ); break; + case TYPE_SEARCH_PORTAL: + item.put( KEY_SEARCH_QUERY, search_query ); + break; } // 以下は保存には必要ないが、カラムリスト画面で使う @@ -286,9 +296,15 @@ class Column implements StreamReader.Callback { this.app_state = app_state; this.context = app_state.context; - SavedAccount ac = SavedAccount.loadAccount( log, src.optLong( KEY_ACCOUNT_ROW_ID ) ); - if( ac == null ) throw new RuntimeException( "missing account" ); - this.access_info = ac; + long account_db_id = src.optLong( KEY_ACCOUNT_ROW_ID ); + if( account_db_id >= 0 ){ + SavedAccount ac = SavedAccount.loadAccount( log, account_db_id ); + if( ac == null ) throw new RuntimeException( "missing account" ); + this.access_info = ac; + }else{ + this.access_info = SavedAccount.getNA(); + } + this.column_type = src.optInt( KEY_TYPE ); this.dont_close = src.optBoolean( KEY_DONT_CLOSE ); this.with_attachment = src.optBoolean( KEY_WITH_ATTACHMENT ); @@ -328,7 +344,9 @@ class Column implements StreamReader.Callback { this.search_query = src.optString( KEY_SEARCH_QUERY ); this.search_resolve = src.optBoolean( KEY_SEARCH_RESOLVE, false ); break; - + case TYPE_SEARCH_PORTAL: + this.search_query = src.optString( KEY_SEARCH_QUERY ); + break; } init(); } @@ -375,7 +393,13 @@ class Column implements StreamReader.Callback { }catch( Throwable ex ){ return false; } - + case TYPE_SEARCH_PORTAL: + try{ + String q = (String) getParamAt( params, 0 ); + return Utils.equalsNullable( q, this.search_query ); + }catch( Throwable ex ){ + return false; + } } } @@ -417,6 +441,13 @@ class Column implements StreamReader.Callback { return getColumnTypeName( context, column_type ); } + case TYPE_SEARCH_PORTAL: + if( bLong ){ + return context.getString( R.string.toot_search_of, search_query ); + }else{ + return getColumnTypeName( context, column_type ); + } + } } @@ -471,6 +502,9 @@ class Column implements StreamReader.Callback { case TYPE_SEARCH: return context.getString( R.string.search ); + case TYPE_SEARCH_PORTAL: + return context.getString( R.string.toot_search ); + case TYPE_FOLLOW_REQUESTS: return context.getString( R.string.follow_requests ); } @@ -525,6 +559,9 @@ class Column implements StreamReader.Callback { case TYPE_SEARCH: return R.attr.ic_search; + case TYPE_SEARCH_PORTAL: + return R.attr.ic_search; + case TYPE_FOLLOW_REQUESTS: return R.attr.ic_account_add; } @@ -580,7 +617,7 @@ class Column implements StreamReader.Callback { for( Object o : list_data ){ if( o instanceof TootStatus ){ TootStatus item = (TootStatus) o; - if( (item.account != null && item.account.id == who_id) + if( ( item.account != null && item.account.id == who_id ) || ( item.reblog != null && item.reblog.account != null && item.reblog.account.id == who_id ) ){ continue; @@ -590,7 +627,8 @@ class Column implements StreamReader.Callback { TootNotification item = (TootNotification) o; if( item.account.id == who_id ) continue; if( item.status != null ){ - if( (item.status.account != null && item.status.account.id == who_id) ) continue; + if( ( item.status.account != null && item.status.account.id == who_id ) ) + continue; if( item.status.reblog != null && item.status.reblog.account != null && item.status.reblog.account.id == who_id ) continue; } @@ -739,8 +777,9 @@ class Column implements StreamReader.Callback { for( Object o : list_data ){ if( o instanceof TootStatus ){ TootStatus item = (TootStatus) o; - if( item.account != null && reDomain.matcher( item.account.acct ).find() ) continue; - if( item.reblog != null && item.reblog.account !=null && reDomain.matcher( item.reblog.account.acct ).find() ) + if( item.account != null && reDomain.matcher( item.account.acct ).find() ) + continue; + if( item.reblog != null && item.reblog.account != null && reDomain.matcher( item.reblog.account.acct ).find() ) continue; }else if( o instanceof TootNotification ){ TootNotification item = (TootNotification) o; @@ -748,8 +787,9 @@ class Column implements StreamReader.Callback { if( reDomain.matcher( item.account.acct ).find() ) continue; } if( item.status != null ){ - if( item.status.account != null && reDomain.matcher( item.status.account.acct ).find() ) continue; - if( item.status.reblog != null && item.status.reblog.account !=null && reDomain.matcher( item.status.reblog.account.acct ).find() ) + if( item.status.account != null && reDomain.matcher( item.status.account.acct ).find() ) + continue; + if( item.status.reblog != null && item.status.reblog.account != null && reDomain.matcher( item.status.reblog.account.acct ).find() ) continue; } } @@ -1022,7 +1062,7 @@ class Column implements StreamReader.Callback { if( result != null && result.array != null ){ saveRange( result, true, true ); // - TootStatus.List src = TootStatus.parseList( log, access_info, result.array ); + TootStatus.List src = TootStatus.parseList( log, access_info, access_info.host,result.array ); list_tmp = new ArrayList<>( src.size() ); addWithFilter( list_tmp, src ); // @@ -1059,7 +1099,7 @@ class Column implements StreamReader.Callback { break; } - src = TootStatus.parseList( log, access_info, result2.array ); + src = TootStatus.parseList( log, access_info, access_info.host,result2.array ); addWithFilter( list_tmp, src ); @@ -1106,7 +1146,7 @@ class Column implements StreamReader.Callback { TootApiResult result = client.request( path_base ); if( result != null ){ saveRange( result, true, true ); - TootNotification.List src = TootNotification.parseList( log, access_info, result.array ); + TootNotification.List src = TootNotification.parseList( log, access_info, access_info.host, result.array ); list_tmp = new ArrayList<>(); addWithFilter( list_tmp, src ); @@ -1223,7 +1263,7 @@ class Column implements StreamReader.Callback { result = client.request( String.format( Locale.JAPAN, PATH_STATUSES, status_id ) ); if( result == null || result.object == null ) return result; - TootStatus target_status = TootStatus.parse( log, access_info, result.object ); + TootStatus target_status = TootStatus.parse( log, access_info, access_info.host,result.object ); target_status.conversation_main = true; // 前後の会話 @@ -1232,13 +1272,13 @@ class Column implements StreamReader.Callback { if( result == null || result.object == null ) return result; // 一つのリストにまとめる - TootContext context = TootContext.parse( log, access_info, result.object ); - list_tmp = new ArrayList<>( 1 + context.ancestors.size() + context.descendants.size() ); - if( context.ancestors != null ) - addWithFilter( list_tmp, context.ancestors ); + TootContext conversation_context = TootContext.parse( log, access_info, access_info.host,result.object ); + list_tmp = new ArrayList<>( 1 + conversation_context.ancestors.size() + conversation_context.descendants.size() ); + if( conversation_context.ancestors != null ) + addWithFilter( list_tmp, conversation_context.ancestors ); list_tmp.add( target_status ); - if( context.descendants != null ) - addWithFilter( list_tmp, context.descendants ); + if( conversation_context.descendants != null ) + addWithFilter( list_tmp, conversation_context.descendants ); // return result; @@ -1250,7 +1290,7 @@ class Column implements StreamReader.Callback { result = client.request( path ); if( result == null || result.object == null ) return result; - TootResults tmp = TootResults.parse( log, access_info, result.object ); + TootResults tmp = TootResults.parse( log, access_info, access_info.host,result.object ); if( tmp != null ){ list_tmp = new ArrayList<>(); list_tmp.addAll( tmp.hashtags ); @@ -1258,7 +1298,45 @@ class Column implements StreamReader.Callback { list_tmp.addAll( tmp.statuses ); } return result; - + + case TYPE_SEARCH_PORTAL: + + max_id = ""; + String q = search_query.trim(); + if( q.length() <= 0 ){ + list_tmp = new ArrayList<>(); + result = new TootApiResult( context.getString( R.string.list_empty ) ); + }else{ + result = MSPClient.search( context, search_query, max_id, new MSPClient.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; + fireShowContent(); + } + } ); + } + } ); + if( result != null && result.array != null ){ + // max_id の更新 + max_id = MSPClient.getMaxId( result.array, max_id ); + // リストデータの用意 + MSPToot.List search_result = MSPToot.parseList( log, access_info, result.array ); + if( search_result != null ){ + list_tmp = new ArrayList<>(); + list_tmp.addAll( search_result ); + } + } + } + return result; } }finally{ try{ @@ -1749,7 +1827,7 @@ class Column implements StreamReader.Callback { if( result != null && result.array != null ){ saveRange( result, bBottom, ! bBottom ); list_tmp = new ArrayList<>(); - TootNotification.List src = TootNotification.parseList( log, access_info, result.array ); + TootNotification.List src = TootNotification.parseList( log, access_info,access_info.host, result.array ); addWithFilter( list_tmp, src ); if( ! src.isEmpty() ){ @@ -1793,7 +1871,7 @@ class Column implements StreamReader.Callback { break; } - src = TootNotification.parseList( log, access_info, result2.array ); + src = TootNotification.parseList( log, access_info, access_info.host,result2.array ); if( ! src.isEmpty() ){ addWithFilter( list_tmp, src ); AlarmService.injectData( context, access_info.db_id, src ); @@ -1815,7 +1893,7 @@ class Column implements StreamReader.Callback { 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 ); + TootStatus.List src = TootStatus.parseList( log, access_info, access_info.host,result.array ); list_tmp = new ArrayList<>(); addWithFilter( list_tmp, src ); @@ -1859,7 +1937,7 @@ class Column implements StreamReader.Callback { break; } - src = TootStatus.parseList( log, access_info, result2.array ); + src = TootStatus.parseList( log, access_info, access_info.host,result2.array ); addWithFilter( list_tmp, src ); @@ -1915,7 +1993,7 @@ class Column implements StreamReader.Callback { break; } - src = TootStatus.parseList( log, access_info, result2.array ); + src = TootStatus.parseList( log, access_info, access_info.host,result2.array ); addWithFilter( list_tmp, src ); } } @@ -2016,6 +2094,49 @@ class Column implements StreamReader.Callback { case TYPE_HASHTAG: return getStatusList( client, String.format( Locale.JAPAN, PATH_HASHTAG, Uri.encode( hashtag ) ) ); + + case TYPE_SEARCH_PORTAL: + + if(!bBottom){ + return new TootApiResult( "head of list."); + } + + TootApiResult result; + String q = search_query.trim(); + if( q.length() <= 0 ){ + list_tmp = new ArrayList<>(); + result = new TootApiResult( context.getString( R.string.list_empty ) ); + }else{ + result = MSPClient.search( context, search_query, max_id, new MSPClient.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; + fireShowContent(); + } + } ); + } + } ); + if( result != null && result.array != null ){ + // max_id の更新 + max_id = MSPClient.getMaxId( result.array, max_id ); + // リストデータの用意 + MSPToot.List search_result = MSPToot.parseList( log, access_info, result.array ); + if( search_result != null ){ + list_tmp = new ArrayList<>(); + list_tmp.addAll( search_result ); + } + } + } + return result; } }finally{ try{ @@ -2271,7 +2392,7 @@ class Column implements StreamReader.Callback { } result = r2; - TootNotification.List src = TootNotification.parseList( log, access_info, r2.array ); + TootNotification.List src = TootNotification.parseList( log, access_info, access_info.host,r2.array ); if( src.isEmpty() ){ log.d( "gap-notification: empty." ); @@ -2327,7 +2448,7 @@ class Column implements StreamReader.Callback { // 成功した場合はそれを返したい result = r2; - TootStatus.List src = TootStatus.parseList( log, access_info, r2.array ); + TootStatus.List src = TootStatus.parseList( log, access_info,access_info.host, r2.array ); if( src.size() == 0 ){ // 直前の取得でカラのデータが帰ってきたら終了 log.d( "gap-statuses: empty." ); @@ -2698,7 +2819,8 @@ class Column implements StreamReader.Callback { }else if( o instanceof TootStatus ){ TootStatus status = (TootStatus) o; if( column_type == TYPE_NOTIFICATIONS ) return; - if( column_type == TYPE_LOCAL && status.account != null && status.account.acct.indexOf( '@' ) != - 1 ) return; + if( column_type == TYPE_LOCAL && status.account != null && status.account.acct.indexOf( '@' ) != - 1 ) + return; if( isFiltered( status ) ) return; if( this.enable_speech ){ @@ -2788,6 +2910,9 @@ class Column implements StreamReader.Callback { // リフレッシュしてからストリーミング開始 log.d( "onResume: start auto refresh." ); startRefresh( true, false, - 1L, - 1 ); + }else if( column_type == TYPE_SEARCH || column_type == TYPE_SEARCH_PORTAL ){ + // 検索カラムはリフレッシュもストリーミングもないが、resumeのタイミングでリストの再描画を行いたい + fireShowContent(); }else{ // ギャップつきでストリーミング開始 log.d( "onResume: start streaming with gap." ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java index 577f07d9..5f138b73 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java @@ -9,6 +9,8 @@ import android.support.v4.view.ViewCompat; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.method.LinkMovementMethod; +import android.text.method.MovementMethod; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; @@ -27,7 +29,9 @@ import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirec import java.util.regex.Pattern; import jp.juggler.subwaytooter.table.AcctColor; +import jp.juggler.subwaytooter.util.HTMLDecoder; import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.view.MyLinkMovementMethod; import jp.juggler.subwaytooter.view.MyListView; import jp.juggler.subwaytooter.util.ScrollPosition; import jp.juggler.subwaytooter.util.Utils; @@ -81,6 +85,8 @@ class ColumnViewHolder private final View llRegexFilter; private final Button btnDeleteNotification; + private final TextView tvSearchDesc; + ColumnViewHolder( ActMain arg_activity, View root ){ this.activity = arg_activity; @@ -137,8 +143,8 @@ class ColumnViewHolder etRegexFilter = (EditText) root.findViewById( R.id.etRegexFilter ); llRegexFilter = root.findViewById( R.id.llRegexFilter ); tvRegexFilterError = (TextView) root.findViewById( R.id.tvRegexFilterError ); - + tvSearchDesc = (TextView) root.findViewById( R.id.tvSearchDesc ); btnDeleteNotification = (Button) root.findViewById( R.id.btnDeleteNotification ); @@ -223,7 +229,7 @@ class ColumnViewHolder private boolean loading_busy; - void onPageCreate( Column column, int page_idx, int page_count ){ + void onPageCreate( @NonNull Column column, int page_idx, int page_count ){ loading_busy = true; try{ this.column = column; @@ -250,6 +256,7 @@ class ColumnViewHolder bAllowFilter = true; break; case Column.TYPE_SEARCH: + case Column.TYPE_SEARCH_PORTAL: case Column.TYPE_CONVERSATION: case Column.TYPE_REPORTS: case Column.TYPE_BLOCKS: @@ -299,7 +306,8 @@ class ColumnViewHolder vg( llRegexFilter, bAllowFilter ); vg( btnDeleteNotification, column.column_type == Column.TYPE_NOTIFICATIONS ); - vg( llSearch, column.column_type == Column.TYPE_SEARCH ); + vg( llSearch, (column.column_type == Column.TYPE_SEARCH || column.column_type == Column.TYPE_SEARCH_PORTAL ) ); + vg( cbResolve, (column.column_type == Column.TYPE_SEARCH ) ); // tvRegexFilterErrorの表示を更新 if( bAllowFilter ){ @@ -307,12 +315,32 @@ class ColumnViewHolder } switch( column.column_type ){ + default: + swipyRefreshLayout.setEnabled( true ); + swipyRefreshLayout.setDirection( SwipyRefreshLayoutDirection.BOTH ); + break; + case Column.TYPE_CONVERSATION: case Column.TYPE_SEARCH: swipyRefreshLayout.setEnabled( false ); break; - default: + + case Column.TYPE_SEARCH_PORTAL: swipyRefreshLayout.setEnabled( true ); + swipyRefreshLayout.setDirection( SwipyRefreshLayoutDirection.BOTTOM ); + break; + + } + + switch( column.column_type ){ + default: + tvSearchDesc.setVisibility( View.GONE ); + break; + case Column.TYPE_SEARCH: + showSearchDesc( activity.getString( R.string.search_desc_mastodon_api ) ); + break; + case Column.TYPE_SEARCH_PORTAL: + showSearchDesc( getSearchDescPortal() ); break; } @@ -331,6 +359,27 @@ class ColumnViewHolder } } + private String getSearchDescPortal(){ + String language_code = activity.getString( R.string.language_code ); + int res_id; + if( "ja".equals( language_code ) ){ + res_id = R.raw.search_desc_portal_ja; + }else{ + res_id = R.raw.search_desc_portal_en; + } + byte[] data = Utils.loadRawResource(activity,res_id); + return data == null ? null : Utils.decodeUTF8( data ); + } + + private void showSearchDesc( String html ){ + if( column==null) return; + log.d("showSearchDesc: html=%s",html); + tvSearchDesc.setVisibility( View.VISIBLE ); + tvSearchDesc.setMovementMethod( MyLinkMovementMethod.getInstance() ); + CharSequence sv = HTMLDecoder.decodeHTML( column.access_info, html, false, null ); + tvSearchDesc.setText( sv ); + } + void showColumnColor(){ if( column == null ) return; @@ -595,7 +644,7 @@ class ColumnViewHolder break; case R.id.btnColumnReload: - if( column.column_type == Column.TYPE_SEARCH ){ + if( column.column_type == Column.TYPE_SEARCH || column.column_type == Column.TYPE_SEARCH_PORTAL ){ Utils.hideKeyboard( activity, etSearch ); etSearch.setText( column.search_query ); cbResolve.setChecked( column.search_resolve ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java index ae788991..ea67be72 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java +++ b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import jp.juggler.subwaytooter.api.entity.TootAccount; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.entity.TootStatusLike; import jp.juggler.subwaytooter.dialog.DlgQRCode; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.table.UserRelation; @@ -30,7 +31,7 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener { @NonNull final ActMain activity; @NonNull private final SavedAccount access_info; @Nullable private final TootAccount who; - @Nullable private final TootStatus status; + @Nullable private final TootStatusLike status; @NonNull private final UserRelation relation; @NonNull private final Column column; @@ -43,7 +44,7 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener { @NonNull ActMain activity , @NonNull Column column , @Nullable TootAccount who - , @Nullable TootStatus status + , @Nullable TootStatusLike status ){ this.activity = activity; this.column = column; @@ -123,11 +124,11 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener { }else{ btnDelete.setVisibility( View.GONE ); btnReport.setOnClickListener( this ); - if( status.application == null || TextUtils.isEmpty( status.application.name ) ){ - btnMuteApp.setVisibility( View.GONE ); - }else{ + if( status.application != null && !TextUtils.isEmpty( status.application.name ) ){ btnMuteApp.setText( activity.getString( R.string.mute_app_of, status.application.name ) ); btnMuteApp.setOnClickListener( this ); + }else{ + btnMuteApp.setVisibility( View.GONE ); } } } @@ -225,11 +226,20 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener { v = viewRoot.findViewById( R.id.btnCancel ); v.setOnClickListener( this ); - v = viewRoot.findViewById( R.id.btnBoostedBy ); - v.setOnClickListener( this ); - - v = viewRoot.findViewById( R.id.btnFavouritedBy ); - v.setOnClickListener( this ); + if( access_info.isNA() ){ + v = viewRoot.findViewById( R.id.btnBoostedBy ); + v.setVisibility( View.GONE); + + v = viewRoot.findViewById( R.id.btnFavouritedBy ); + v.setVisibility( View.GONE); + }else{ + v = viewRoot.findViewById( R.id.btnBoostedBy ); + v.setOnClickListener( this ); + + v = viewRoot.findViewById( R.id.btnFavouritedBy ); + v.setOnClickListener( this ); + + } v = viewRoot.findViewById( R.id.btnAccountQrCode ); v.setOnClickListener( this ); @@ -275,7 +285,7 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener { break; case R.id.btnReplyAnotherAccount: - activity.openReplyFromAnotherAccount( access_info, status ); + activity.openReplyFromAnotherAccount( status ); break; case R.id.btnDelete: @@ -293,8 +303,8 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener { break; case R.id.btnReport: - if( status != null && who != null ){ - activity.openReportForm( access_info, who, status ); + if( who != null && status instanceof TootStatus ){ + activity.openReportForm( access_info, who, (TootStatus)status ); } break; diff --git a/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java b/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java index ecd834a5..e7be8224 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java @@ -75,7 +75,7 @@ class ItemListAdapter extends BaseAdapter implements AdapterView.OnItemClickList }else{ holder = (ItemViewHolder) view.getTag(); } - holder.bind( o, position ); + holder.bind( o ); return view; } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java index 5d78adc4..bcfb9d3d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java @@ -19,6 +19,8 @@ import jp.juggler.subwaytooter.api.entity.TootDomainBlock; 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.api.entity.TootStatusLike; +import jp.juggler.subwaytooter.api_msp.entity.MSPToot; import jp.juggler.subwaytooter.table.AcctColor; import jp.juggler.subwaytooter.table.ContentWarning; import jp.juggler.subwaytooter.table.MediaShown; @@ -79,18 +81,17 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { private final TextView tvApplication; - private TootStatus status; + private TootStatusLike status; private TootAccount account_thumbnail; private TootAccount account_boost; private TootAccount account_follow; private String search_tag; private TootGap gap; private TootDomainBlock domain_block; - private int position; private final boolean bSimpleList; - ItemViewHolder( ActMain arg_activity, Column column, ItemListAdapter list_adapter, View view ,boolean bSimpleList ){ + ItemViewHolder( ActMain arg_activity, Column column, ItemListAdapter list_adapter, View view, boolean bSimpleList ){ this.activity = arg_activity; this.column = column; this.access_info = column.access_info; @@ -101,7 +102,6 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { this.tvFollowerName = (TextView) view.findViewById( R.id.tvFollowerName ); this.tvBoosted = (TextView) view.findViewById( R.id.tvBoosted ); - if( activity.timeline_font != null ){ Utils.scanView( view, new Utils.ScanViewCallback() { @Override public void onScanView( View v ){ @@ -111,7 +111,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { }else if( v instanceof TextView ){ ( (TextView) v ).setTypeface( activity.timeline_font ); } - }catch(Throwable ex){ + }catch( Throwable ex ){ ex.printStackTrace(); } } @@ -147,7 +147,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { this.tvContent = (MyTextView) view.findViewById( R.id.tvContent ); this.tvMentions = (MyTextView) view.findViewById( R.id.tvMentions ); - this.buttons_for_status = bSimpleList ? null : new StatusButtons( activity, column, view , false ); + this.buttons_for_status = bSimpleList ? null : new StatusButtons( activity, column, view, false ); this.flMedia = view.findViewById( R.id.flMedia ); this.btnShowMedia = view.findViewById( R.id.btnShowMedia ); @@ -193,8 +193,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { lp.height = activity.app_state.media_thumb_height; } - void bind( Object item, int position ){ - this.position = position; + void bind( Object item ){ this.status = null; this.account_thumbnail = null; this.account_boost = null; @@ -210,7 +209,9 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { if( item == null ) return; - if( item instanceof String ){ + if( item instanceof MSPToot ){ + showStatus( activity, (MSPToot) item ); + }else if( item instanceof String ){ showSearchTag( (String) item ); }else if( item instanceof TootAccount ){ showFollow( (TootAccount) item ); @@ -270,7 +271,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { }else if( item instanceof TootGap ){ showGap( (TootGap) item ); }else if( item instanceof TootDomainBlock ){ - showDomainBlock( (TootDomainBlock)item ); + showDomainBlock( (TootDomainBlock) item ); } } @@ -316,24 +317,30 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { Styler.setFollowIcon( activity, btnFollow, ivFollowedBy, relation ); } - private void showStatus( @NonNull ActMain activity, @NonNull TootStatus status ){ + private void showStatus( @NonNull ActMain activity, @NonNull TootStatusLike status ){ this.status = status; llStatus.setVisibility( View.VISIBLE ); - tvTime.setText( TootStatus.formatTime( status.time_created_at ) ); + if( status instanceof TootStatus ){ + tvTime.setText( TootStatus.formatTime( ( (TootStatus) status ).time_created_at ) ); + + }else if( status instanceof MSPToot ){ + tvTime.setText( ( (MSPToot) status ).created_at ); + + } ivThumbnail.setCornerRadius( activity.pref, 16f ); account_thumbnail = status.account; - setAcct( tvAcct, access_info.getFullAcct( status.account ), R.attr.colorAcctSmall ); + setAcct( tvAcct, access_info.getFullAcct( status.account ), R.attr.colorAcctSmall ); - if(status.account == null ){ + if( status.account == null ){ tvName.setText( "?" ); - ivThumbnail.setImageUrl(null,null); + ivThumbnail.setImageUrl( null, null ); }else{ tvName.setText( status.account.display_name ); ivThumbnail.setImageUrl( access_info.supplyBaseUrl( status.account.avatar_static ) - ,access_info.supplyBaseUrl( status.account.avatar ) + , access_info.supplyBaseUrl( status.account.avatar ) ); } tvContent.setText( status.decoded_content ); @@ -345,11 +352,16 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { // tvTags.setText( status.decoded_tags ); // } - if( status.decoded_mentions == null ){ - tvMentions.setVisibility( View.GONE ); + if( status instanceof TootStatus ){ + TootStatus ts = (TootStatus) status; + if( ts.decoded_mentions == null ){ + tvMentions.setVisibility( View.GONE ); + }else{ + tvMentions.setVisibility( View.VISIBLE ); + tvMentions.setText( ts.decoded_mentions ); + } }else{ - tvMentions.setVisibility( View.VISIBLE ); - tvMentions.setText( status.decoded_mentions ); + tvMentions.setVisibility( View.GONE ); } // Content warning @@ -359,28 +371,52 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { }else{ llContentWarning.setVisibility( View.VISIBLE ); tvContentWarning.setText( status.decoded_spoiler_text ); - boolean cw_shown = ContentWarning.isShown( access_info.host, status.id, false ); + boolean cw_shown = ContentWarning.isShown( status, false ); showContent( cw_shown ); } - if( status.media_attachments == null || status.media_attachments.isEmpty() ){ - flMedia.setVisibility( View.GONE ); - }else{ - flMedia.setVisibility( View.VISIBLE ); - setMedia( ivMedia1, status, 0 ); - setMedia( ivMedia2, status, 1 ); - setMedia( ivMedia3, status, 2 ); - setMedia( ivMedia4, status, 3 ); - - @SuppressWarnings("SimplifiableConditionalExpression") - boolean default_shown = - column.hide_media_default ? false : - access_info.dont_hide_nsfw ? true : - ! status.sensitive; - - // hide sensitive media - boolean is_shown = MediaShown.isShown( access_info.host, status.id, default_shown ); - btnShowMedia.setVisibility( ! is_shown ? View.VISIBLE : View.GONE ); + if( status instanceof TootStatus ){ + TootStatus ts = (TootStatus) status; + if( ts.media_attachments == null || ts.media_attachments.isEmpty() ){ + flMedia.setVisibility( View.GONE ); + }else{ + flMedia.setVisibility( View.VISIBLE ); + setMedia( ivMedia1, ts, 0 ); + setMedia( ivMedia2, ts, 1 ); + setMedia( ivMedia3, ts, 2 ); + setMedia( ivMedia4, ts, 3 ); + + @SuppressWarnings("SimplifiableConditionalExpression") + boolean default_shown = + column.hide_media_default ? false : + access_info.dont_hide_nsfw ? true : + ! status.sensitive; + + // hide sensitive media + boolean is_shown = MediaShown.isShown( status, default_shown ); + btnShowMedia.setVisibility( ! is_shown ? View.VISIBLE : View.GONE ); + } + }else if( status instanceof MSPToot ){ + MSPToot ts = (MSPToot) status; + if( ts.media_attachments == null || ts.media_attachments.isEmpty() ){ + flMedia.setVisibility( View.GONE ); + }else{ + flMedia.setVisibility( View.VISIBLE ); + setMedia( ivMedia1, ts, 0 ); + setMedia( ivMedia2, ts, 1 ); + setMedia( ivMedia3, ts, 2 ); + setMedia( ivMedia4, ts, 3 ); + + @SuppressWarnings("SimplifiableConditionalExpression") + boolean default_shown = + column.hide_media_default ? false : + access_info.dont_hide_nsfw ? true : + ! status.sensitive; + + // hide sensitive media + boolean is_shown = MediaShown.isShown( status, default_shown ); + btnShowMedia.setVisibility( ! is_shown ? View.VISIBLE : View.GONE ); + } } if( buttons_for_status != null ){ @@ -432,11 +468,11 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { }else{ iv.setVisibility( View.VISIBLE ); iv.setScaleType( activity.dont_crop_media_thumbnail ? ImageView.ScaleType.FIT_CENTER : ImageView.ScaleType.CENTER_CROP ); - + TootAttachment ta = status.media_attachments.get( idx ); - - if( TextUtils.isEmpty( ta.type )){ - iv.setMediaType(0); + + if( TextUtils.isEmpty( ta.type ) ){ + iv.setMediaType( 0 ); }else{ switch( ta.type ){ default: @@ -463,22 +499,36 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { url = ta.url; } } - iv.setCornerRadius( activity.pref,0f ); // 正方形じゃないせいか、うまく動かない activity.density * 4f ); + iv.setCornerRadius( activity.pref, 0f ); // 正方形じゃないせいか、うまく動かない activity.density * 4f ); + iv.setImageUrl( access_info.supplyBaseUrl( url ) ); + } + } + + private void setMedia( MyNetworkImageView iv, MSPToot msp_toot, int idx ){ + if( idx >= msp_toot.media_attachments.size() ){ + iv.setVisibility( View.GONE ); + }else{ + iv.setVisibility( View.VISIBLE ); + iv.setScaleType( activity.dont_crop_media_thumbnail ? ImageView.ScaleType.FIT_CENTER : ImageView.ScaleType.CENTER_CROP ); + + String url = msp_toot.media_attachments.get( idx ); + iv.setMediaType( 0 ); + iv.setCornerRadius( activity.pref, 0f ); // 正方形じゃないせいか、うまく動かない activity.density * 4f ); iv.setImageUrl( access_info.supplyBaseUrl( url ) ); } } @Override public void onClick( View v ){ - int pos = activity.nextPosition( column ) ; + int pos = activity.nextPosition( column ); switch( v.getId() ){ case R.id.btnHideMedia: - MediaShown.save( access_info.host, status.id, false ); + MediaShown.save( status, false ); btnShowMedia.setVisibility( View.VISIBLE ); break; case R.id.btnShowMedia: - MediaShown.save( access_info.host, status.id, true ); + MediaShown.save( status, true ); btnShowMedia.setVisibility( View.GONE ); break; case R.id.ivMedia1: @@ -495,7 +545,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { break; case R.id.btnContentWarning:{ boolean new_shown = ( llContents.getVisibility() == View.GONE ); - ContentWarning.save( access_info.host, status.id, new_shown ); + ContentWarning.save( status, new_shown ); list_adapter.notifyDataSetChanged(); break; } @@ -504,7 +554,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { if( access_info.isPseudo() ){ new DlgContextMenu( activity, column, account_thumbnail, null ).show(); }else{ - activity.openProfile( pos,access_info, account_thumbnail ); + activity.openProfile( pos, access_info, account_thumbnail ); } break; @@ -512,14 +562,14 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { if( access_info.isPseudo() ){ new DlgContextMenu( activity, column, account_boost, null ).show(); }else{ - activity.openProfile( pos,access_info, account_boost ); + activity.openProfile( pos, access_info, account_boost ); } break; case R.id.llFollow: if( access_info.isPseudo() ){ new DlgContextMenu( activity, column, account_follow, null ).show(); }else{ - activity.openProfile( pos,access_info, account_follow ); + activity.openProfile( pos, access_info, account_follow ); } break; case R.id.btnFollow: @@ -528,13 +578,13 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { case R.id.btnSearchTag: if( search_tag != null ){ - activity.openHashTag( activity.nextPosition( column ),access_info, search_tag ); + activity.openHashTag( activity.nextPosition( column ), access_info, search_tag ); }else if( gap != null ){ column.startGap( gap ); }else if( domain_block != null ){ - final String domain = domain_block.domain; + final String domain = domain_block.domain; new AlertDialog.Builder( activity ) - .setMessage( activity.getString( R.string.confirm_unblock_domain, domain) ) + .setMessage( activity.getString( R.string.confirm_unblock_domain, domain ) ) .setNegativeButton( R.string.cancel, null ) .setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick( DialogInterface dialog, int which ){ @@ -544,7 +594,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { .show(); } break; - + } } @@ -566,22 +616,28 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { private void clickMedia( int i ){ try{ - TootAttachment a = status.media_attachments.get( i ); - - String sv; - if( Pref.pref( activity ).getBoolean( Pref.KEY_PRIOR_LOCAL_URL, false ) ){ - sv = a.url; - if( TextUtils.isEmpty( sv ) ){ - sv = a.remote_url; - } - }else{ - sv = a.remote_url; - if( TextUtils.isEmpty( sv ) ){ + if( status instanceof MSPToot ){ + activity.openStatus( activity.nextPosition( column ), access_info, status ); + }else if( status instanceof TootStatus ){ + TootStatus ts = (TootStatus) status; + + TootAttachment a = ts.media_attachments.get( i ); + + String sv; + if( Pref.pref( activity ).getBoolean( Pref.KEY_PRIOR_LOCAL_URL, false ) ){ sv = a.url; + if( TextUtils.isEmpty( sv ) ){ + sv = a.remote_url; + } + }else{ + sv = a.remote_url; + if( TextUtils.isEmpty( sv ) ){ + sv = a.url; + } } + int pos = activity.nextPosition( column ); + activity.openChromeTab( pos, access_info, sv, false ); } - int pos = activity.nextPosition( column ) ; - activity.openChromeTab(pos, access_info, sv, false ); }catch( Throwable ex ){ ex.printStackTrace(); } @@ -590,11 +646,9 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { // 簡略ビューの時だけ呼ばれる // StatusButtonsPopupを表示する void onItemClick( MyListView listView, View anchor ){ - if( status != null ){ - activity.closeListItemPopup(); - activity.list_item_popup = new StatusButtonsPopup( activity, column ,bSimpleList); - activity.list_item_popup.show( listView, anchor, status ); - } + activity.closeListItemPopup(); + activity.list_item_popup = new StatusButtonsPopup( activity, column, bSimpleList ); + activity.list_item_popup.show( listView, anchor, status ); } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/Pref.java b/app/src/main/java/jp/juggler/subwaytooter/Pref.java index 040730e8..5343b5ba 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Pref.java +++ b/app/src/main/java/jp/juggler/subwaytooter/Pref.java @@ -6,7 +6,8 @@ import android.preference.PreferenceManager; public class Pref { - static SharedPreferences pref( Context context ){ + + public static SharedPreferences pref( Context context ){ return PreferenceManager.getDefaultSharedPreferences( context ); } @@ -60,6 +61,8 @@ public class Pref { static final String KEY_CLIENT_NAME = "client_name"; + public static final String KEY_MASTODON_SEARCH_PORTAL_USER_TOKEN = "mastodon_search_portal_user_token"; + // 項目を追加したらAppDataExporter#importPref のswitch文も更新すること } diff --git a/app/src/main/java/jp/juggler/subwaytooter/PrefDevice.java b/app/src/main/java/jp/juggler/subwaytooter/PrefDevice.java index 6ae53969..3ebcbc8b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/PrefDevice.java +++ b/app/src/main/java/jp/juggler/subwaytooter/PrefDevice.java @@ -7,7 +7,7 @@ public class PrefDevice { private static String file_name = "device"; - static SharedPreferences prefDevice( Context context ){ + public static SharedPreferences prefDevice( Context context ){ return context.getSharedPreferences( file_name, Context.MODE_PRIVATE ); } diff --git a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java index a4a74450..2be93e1a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java +++ b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java @@ -10,6 +10,8 @@ import android.widget.ImageView; import android.widget.PopupWindow; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.entity.TootStatusLike; +import jp.juggler.subwaytooter.api_msp.entity.MSPToot; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.table.UserRelation; import jp.juggler.subwaytooter.util.LogCategory; @@ -27,7 +29,7 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { private final ImageView ivFollowedBy2; private final View llFollow2; - final boolean bSimpleList; + private final boolean bSimpleList; StatusButtons( @NonNull ActMain activity, @NonNull Column column, @NonNull View viewRoot ,boolean bSimpleList){ this.activity = activity; @@ -63,32 +65,40 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { } - private TootStatus status; private UserRelation relation; + private TootStatusLike status; - void bind( @NonNull TootStatus status ){ + void bind( @NonNull TootStatusLike status ){ this.status = status; int color_normal = Styler.getAttributeColor( activity, R.attr.colorImageButton ); int color_accent = Styler.getAttributeColor( activity, R.attr.colorImageButtonAccent ); - if( TootStatus.VISIBILITY_DIRECT.equals( status.visibility ) ){ - setButton( btnBoost, false, color_accent, R.attr.ic_mail, "" ); - }else if( TootStatus.VISIBILITY_PRIVATE.equals( status.visibility ) ){ - setButton( btnBoost, false, color_accent, R.attr.ic_lock, "" ); - }else if( activity.app_state.isBusyBoost( access_info, status ) ){ - setButton( btnBoost, false, color_normal, R.attr.btn_refresh, "?" ); - }else{ - int color = ( status.reblogged ? color_accent : color_normal ); - setButton( btnBoost, true, color, R.attr.btn_boost, Long.toString( status.reblogs_count ) ); + if( status instanceof MSPToot ){ + setButton( btnBoost, true, color_normal, R.attr.btn_boost, "" ); + setButton( btnFavourite, true, color_normal, R.attr.btn_favourite, ""); + }else if( status instanceof TootStatus ){ + TootStatus ts = (TootStatus)status; + + if( TootStatus.VISIBILITY_DIRECT.equals( ts.visibility ) ){ + setButton( btnBoost, false, color_accent, R.attr.ic_mail, "" ); + }else if( TootStatus.VISIBILITY_PRIVATE.equals( ts.visibility ) ){ + setButton( btnBoost, false, color_accent, R.attr.ic_lock, "" ); + }else if( activity.app_state.isBusyBoost( access_info, status ) ){ + setButton( btnBoost, false, color_normal, R.attr.btn_refresh, "?" ); + }else{ + int color = ( ts.reblogged ? color_accent : color_normal ); + setButton( btnBoost, true, color, R.attr.btn_boost, Long.toString( ts.reblogs_count ) ); + } + + if( activity.app_state.isBusyFav( access_info, status ) ){ + setButton( btnFavourite, false, color_normal, R.attr.btn_refresh, "?" ); + }else{ + int color = ( ts.favourited ? color_accent : color_normal ); + setButton( btnFavourite, true, color, R.attr.btn_favourite, Long.toString( ts.favourites_count ) ); + } } - if( activity.app_state.isBusyFav( access_info, status ) ){ - setButton( btnFavourite, false, color_normal, R.attr.btn_refresh, "?" ); - }else{ - int color = ( status.favourited ? color_accent : color_normal ); - setButton( btnFavourite, true, color, R.attr.btn_favourite, Long.toString( status.favourites_count ) ); - } if( status.account == null || ! activity.pref.getBoolean( Pref.KEY_SHOW_FOLLOW_BUTTON_IN_BUTTON_BAR, false ) ){ llFollow2.setVisibility( View.GONE ); @@ -101,6 +111,7 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { } + private void setButton( Button b, boolean enabled, int color, int icon_attr, String text ){ Drawable d = Styler.getAttributeDrawable( activity, icon_attr ).mutate(); d.setColorFilter( color, PorterDuff.Mode.SRC_ATOP ); @@ -119,12 +130,13 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { activity.openStatus( activity.nextPosition( column ), access_info, status ); break; case R.id.btnReply: - if( access_info.isPseudo() ){ - activity.openReplyFromAnotherAccount( access_info, status ); + if( status instanceof TootStatus && !access_info.isPseudo() ){ + activity.performReply( access_info, (TootStatus)status ); }else{ - activity.performReply( access_info, status ,false); + activity.openReplyFromAnotherAccount( status ); } break; + case R.id.btnBoost: if( access_info.isPseudo() ){ activity.openBoostFromAnotherAccount( access_info, status ); @@ -159,8 +171,10 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { break; case R.id.btnFollow2: - if( access_info.isPseudo() ){ - activity.openFollowFromAnotherAccount( access_info, status ); + if( status == null || status.account == null ){ + // 何もしない + }else if( access_info.isPseudo() ){ + activity.openFollowFromAnotherAccount( access_info, status.account ); }else if( relation.blocking || relation.muting ){ // 何もしない }else if( relation.following || relation.requested ){ @@ -184,11 +198,13 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { break; case R.id.btnFollow2: - activity.openFollowFromAnotherAccount( access_info, status ); + if( status != null && status.account != null ){ + activity.openFollowFromAnotherAccount( access_info, status.account ); + } break; case R.id.btnReply: - activity.openReplyFromAnotherAccount( access_info, status ); + activity.openReplyFromAnotherAccount( status ); break; } diff --git a/app/src/main/java/jp/juggler/subwaytooter/StatusButtonsPopup.java b/app/src/main/java/jp/juggler/subwaytooter/StatusButtonsPopup.java index fb4e116d..b6354f16 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/StatusButtonsPopup.java +++ b/app/src/main/java/jp/juggler/subwaytooter/StatusButtonsPopup.java @@ -8,10 +8,11 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; -import android.widget.ListView; import android.widget.PopupWindow; import jp.juggler.subwaytooter.api.entity.TootStatus; +import jp.juggler.subwaytooter.api.entity.TootStatusLike; +import jp.juggler.subwaytooter.api_msp.entity.MSPToot; import jp.juggler.subwaytooter.view.MyListView; class StatusButtonsPopup { @@ -34,7 +35,7 @@ class StatusButtonsPopup { } } - void show( final MyListView listView, View anchor, TootStatus status ){ + void show( final MyListView listView, View anchor, TootStatusLike status ){ // window = new PopupWindow( activity ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/StreamReader.java b/app/src/main/java/jp/juggler/subwaytooter/StreamReader.java index dfb9d482..9e32cac2 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/StreamReader.java +++ b/app/src/main/java/jp/juggler/subwaytooter/StreamReader.java @@ -119,9 +119,9 @@ class StreamReader { private Object parsePayload( String event, JSONObject obj ){ try{ if( "update".equals( event ) ){ - return TootStatus.parse( log, access_info, new JSONObject( obj.optString( "payload" ) ) ); + return TootStatus.parse( log, access_info, access_info.host,new JSONObject( obj.optString( "payload" ) ) ); }else if( "notification".equals( event ) ){ - return TootNotification.parse( log, access_info, new JSONObject( obj.optString( "payload" ) ) ); + return TootNotification.parse( log, access_info, access_info.host,new JSONObject( obj.optString( "payload" ) ) ); }else if( "delete".equals( event ) ){ return obj.optLong( "payload", - 1L ); } 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 bb3b2b7f..3953f880 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 @@ -143,7 +143,7 @@ public class TootAccount { return result; } - private static CharSequence filterDisplayName( String sv ){ + public static CharSequence filterDisplayName( String sv ){ // decode HTML entity sv = HTMLDecoder.decodeEntity(sv ); @@ -155,4 +155,14 @@ public class TootAccount { return Emojione.decodeEmoji( sv ) ; } + public String getAcctHost(){ + try{ + int pos = acct.indexOf( '@' ); + if( pos != - 1 ) return acct.substring( pos + 1 ); + }catch(Throwable ignored){ + + } + return null; + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java index 45102adf..baed18cd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootContext.java @@ -13,12 +13,12 @@ public class TootContext { // descendants The descendants of the status in the conversation, as a list of Statuses public TootStatus.List descendants; - public static TootContext parse( LogCategory log, LinkClickContext account,JSONObject src ){ + public static TootContext parse( LogCategory log, LinkClickContext lcc,String status_host,JSONObject src ){ if( src==null) return null; try{ TootContext dst = new TootContext(); - dst.ancestors = TootStatus.parseList( log, account,src.optJSONArray( "ancestors" ) ); - dst.descendants = TootStatus.parseList(log, account, src.optJSONArray( "descendants" ) ); + dst.ancestors = TootStatus.parseList( log, lcc,status_host,src.optJSONArray( "ancestors" ) ); + dst.descendants = TootStatus.parseList(log, lcc, status_host,src.optJSONArray( "descendants" ) ); return dst; }catch( Throwable ex ){ ex.printStackTrace(); 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 a9098aba..34c50d09 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 @@ -37,7 +37,7 @@ public class TootNotification extends TootId { public JSONObject json; - public static TootNotification parse( LogCategory log, LinkClickContext accopunt, JSONObject src ){ + public static TootNotification parse( LogCategory log, LinkClickContext lcc,String status_host, JSONObject src ){ if( src == null ) return null; try{ TootNotification dst = new TootNotification(); @@ -45,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, lcc, src.optJSONObject( "account" ) ); + dst.status = TootStatus.parse( log, lcc, status_host,src.optJSONObject( "status" ) ); dst.time_created_at = TootStatus.parseTime( log, dst.created_at ); @@ -69,7 +69,7 @@ public class TootNotification extends TootId { } @NonNull - public static List parseList( LogCategory log, LinkClickContext account, JSONArray array ){ + public static List parseList( LogCategory log, LinkClickContext lcc,String status_host, JSONArray array ){ List result = new List(); if( array != null ){ int array_size = array.length(); @@ -77,7 +77,7 @@ public class TootNotification extends TootId { for( int i = 0 ; i < array_size ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; - TootNotification item = parse( log, account, src ); + TootNotification item = parse( log, lcc,status_host, src ); if( item != null ) result.add( item ); } } 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 89bb9f8b..74ad3235 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 @@ -18,12 +18,12 @@ public class TootResults { // An array of matched hashtags, as strings public ArrayList< String > hashtags; - public static TootResults parse( LogCategory log, LinkClickContext account, JSONObject src ){ + public static TootResults parse( LogCategory log, LinkClickContext lcc,String status_host, 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" ) ); + dst.accounts = TootAccount.parseList( log, lcc, src.optJSONArray( "accounts" ) ); + dst.statuses = TootStatus.parseList( log, lcc, status_host,src.optJSONArray( "statuses" ) ); dst.hashtags = Utils.parseStringArray( log, src.optJSONArray( "hashtags" ) ); return dst; }catch( Throwable ex ){ 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 5fd14cf4..2088937b 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 @@ -2,6 +2,7 @@ package jp.juggler.subwaytooter.api.entity; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.design.widget.NavigationView; import android.text.Spannable; import android.text.TextUtils; @@ -19,13 +20,14 @@ import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; +import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.HTMLDecoder; import jp.juggler.subwaytooter.util.LinkClickContext; import jp.juggler.subwaytooter.util.LogCategory; import jp.juggler.subwaytooter.util.Utils; import jp.juggler.subwaytooter.util.WordTrieTree; -public class TootStatus extends TootId { +public class TootStatus extends TootStatusLike { public static class List extends ArrayList< TootStatus > { @@ -44,12 +46,9 @@ public class TootStatus extends TootId { // A Fediverse-unique resource ID public String uri; - //URL to the status page (can be remote) - public String url; - - // The TootAccount which posted the status - @Nullable public TootAccount account; + + // null or the ID of the status it replies to public String in_reply_to_id; @@ -58,31 +57,12 @@ public class TootStatus extends TootId { // null or the reblogged Status public TootStatus reblog; - - // Body of the status; this will contain HTML (remote HTML already sanitized) - public String content; - + // The time the status was created public String created_at; - //The number of reblogs for the status - public long reblogs_count; - - //The number of favourites for the status - public long favourites_count; - - // Whether the authenticated user has reblogged the status - public boolean reblogged; - - // Whether the authenticated user has favourited the status - public boolean favourited; - - //Whether media attachments should be hidden by default - public boolean sensitive; - - //If not empty, warning text that should be displayed before the actual content - public String spoiler_text; - + + //One of: public, unlisted, private, direct public String visibility; public static final String VISIBILITY_PUBLIC = "public"; @@ -99,13 +79,9 @@ public class TootStatus extends TootId { //An array of Tags public TootTag.List tags; - //Application from which the status was posted - public TootApplication application; - + public long time_created_at; - public Spannable decoded_content; - public Spannable decoded_spoiler_text; // public Spannable decoded_tags; public Spannable decoded_mentions; @@ -113,7 +89,7 @@ public class TootStatus extends TootId { public boolean conversation_main; - public static TootStatus parse( LogCategory log, LinkClickContext account, JSONObject src ){ + public static TootStatus parse( @NonNull LogCategory log, @NonNull LinkClickContext lcc, @NonNull String status_host, JSONObject src ){ if( src == null ) return null; @@ -122,12 +98,13 @@ public class TootStatus extends TootId { status.json = src; // log.d( "parse: %s", src.toString() ); status.id = src.optLong( "id" ); + status.status_host = status_host; 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, lcc, 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, lcc,status_host, 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" ); @@ -143,12 +120,12 @@ public class TootStatus extends TootId { status.application = TootApplication.parse( log, src.optJSONObject( "application" ) ); // null status.time_created_at = parseTime( log, status.created_at ); - status.decoded_content = HTMLDecoder.decodeHTML( account, status.content ,true,status.media_attachments ); + status.decoded_content = HTMLDecoder.decodeHTML( lcc, status.content ,true,status.media_attachments ); // status.decoded_tags = HTMLDecoder.decodeTags( account,status.tags ); - status.decoded_mentions = HTMLDecoder.decodeMentions( account, status.mentions ); + status.decoded_mentions = HTMLDecoder.decodeMentions( lcc, status.mentions ); if( ! TextUtils.isEmpty( status.spoiler_text ) ){ - status.decoded_spoiler_text = HTMLDecoder.decodeHTML( account, status.spoiler_text ,true,status.media_attachments); + status.decoded_spoiler_text = HTMLDecoder.decodeHTML( lcc, status.spoiler_text ,true,status.media_attachments); } return status; }catch( Throwable ex ){ @@ -159,7 +136,7 @@ public class TootStatus extends TootId { } @NonNull - public static List parseList( LogCategory log, LinkClickContext account, JSONArray array ){ + public static List parseList( @NonNull LogCategory log, @NonNull LinkClickContext lcc, @NonNull String status_host, JSONArray array ){ List result = new List(); if( array != null ){ int array_size = array.length(); @@ -167,7 +144,7 @@ public class TootStatus extends TootId { for( int i = 0 ; i < array_size ; ++ i ){ JSONObject src = array.optJSONObject( i ); if( src == null ) continue; - TootStatus item = parse( log, account, src ); + TootStatus item = parse( log, lcc,status_host, src ); if( item != null ) result.add( item ); } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatusLike.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatusLike.java new file mode 100644 index 00000000..af92b156 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatusLike.java @@ -0,0 +1,45 @@ +package jp.juggler.subwaytooter.api.entity; + +import android.support.annotation.Nullable; +import android.text.Spannable; + + +public abstract class TootStatusLike extends TootId{ + + //URL to the status page (can be remote) + public String url; + + public String status_host; + + // The TootAccount which posted the status + @Nullable public TootAccount account; + + + //The number of reblogs for the status + public long reblogs_count; + + //The number of favourites for the status + public long favourites_count; + + // Whether the authenticated user has reblogged the status + public boolean reblogged; + + // Whether the authenticated user has favourited the status + public boolean favourited; + + //Whether media attachments should be hidden by default + public boolean sensitive; + + //If not empty, warning text that should be displayed before the actual content + public String spoiler_text; + public Spannable decoded_spoiler_text; + + // Body of the status; this will contain HTML (remote HTML already sanitized) + public String content; + public Spannable decoded_content; + + //Application from which the status was posted + public TootApplication application; + + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api_msp/MSPApiResult.java b/app/src/main/java/jp/juggler/subwaytooter/api_msp/MSPApiResult.java new file mode 100644 index 00000000..427ae369 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api_msp/MSPApiResult.java @@ -0,0 +1,21 @@ +package jp.juggler.subwaytooter.api_msp; + +import org.json.JSONArray; + +import jp.juggler.subwaytooter.api.TootApiResult; +import okhttp3.Response; + + +public class MSPApiResult extends TootApiResult { + + MSPApiResult( String error ){ + super( error ); + } + + MSPApiResult( Response response, String json, JSONArray array ){ + super(null); + this.json = json; + this.array = array; + this.response = response; + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api_msp/MSPClient.java b/app/src/main/java/jp/juggler/subwaytooter/api_msp/MSPClient.java new file mode 100644 index 00000000..98e7fb28 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api_msp/MSPClient.java @@ -0,0 +1,176 @@ +package jp.juggler.subwaytooter.api_msp; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +import jp.juggler.subwaytooter.App1; +import jp.juggler.subwaytooter.Pref; +import jp.juggler.subwaytooter.R; +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class MSPClient { + static final LogCategory log = new LogCategory("MSPClient"); + + private static final String url_token = "http://mastodonsearch.jp/api/v1.0.1/utoken"; + private static final String url_search = "http://mastodonsearch.jp/api/v1.0.1/cross"; + private static final String api_key = "e53de7f66130208f62d1808672bf6320523dcd0873dc69bc"; + + private static final OkHttpClient ok_http_client = App1.ok_http_client; + + + public interface Callback { + boolean isApiCancelled(); + + void publishApiProgress( String s ); + } + + public static MSPApiResult search( @NonNull Context context, @NonNull String query, @NonNull String max_id ,@NonNull Callback callback ){ + // ユーザトークンを読む + SharedPreferences pref = Pref.pref(context); + String user_token = pref.getString(Pref.KEY_MASTODON_SEARCH_PORTAL_USER_TOKEN,null); + + Response response; + + for(;;){ + // ユーザトークンがなければ取得する + if( TextUtils.isEmpty( user_token ) ){ + + callback.publishApiProgress( "get MSP user token..." ); + + String url = url_token + "?apikey=" + Uri.encode( api_key ); + + try{ + Request request = new Request.Builder() + .url( url ) + .build(); + + Call call = ok_http_client.newCall( request ); + response = call.execute(); + }catch( Throwable ex ){ + ex.printStackTrace(); + return new MSPApiResult( Utils.formatError( ex, context.getResources(), R.string.network_error ) ); + } + + if( callback.isApiCancelled() ) return null; + + if( ! response.isSuccessful() ){ + if( response.code() >= 400 ){ + try{ + String json = response.body().string(); + JSONObject object = new JSONObject( json ); + JSONObject error = object.getJSONObject( "error" ); + return new MSPApiResult( String.format( "API returns error. %s: %s" + , error.optString( "type" ) + , error.optString( "detail" ) + ) ); + }catch( Throwable ex ){ + ex.printStackTrace(); + return new MSPApiResult( Utils.formatError( ex, "API returns error response %s, but can't parse response body.", response.code() ) ); + } + }else{ + return new MSPApiResult( context.getString( R.string.network_error_arg, response ) ); + } + } + + try{ + //noinspection ConstantConditions + String json = response.body().string(); + JSONObject object = new JSONObject( json ); + user_token = object.getJSONObject( "result" ).getString( "token" ); + if( TextUtils.isEmpty( user_token ) ){ + return new MSPApiResult( String.format( "Can't get MSP user token. response=%s", json ) ); + }else{ + pref.edit().putString( Pref.KEY_MASTODON_SEARCH_PORTAL_USER_TOKEN, user_token ).apply(); + } + }catch( Throwable ex ){ + ex.printStackTrace(); + return new MSPApiResult( Utils.formatError( ex, "API data error" ) ); + } + } + // ユーザトークンを使って検索APIを呼び出す + { + callback.publishApiProgress( "waiting search result..." ); + String url = url_search + + "?apikey=" + Uri.encode( api_key ) + + "&utoken=" + Uri.encode( user_token ) + + "&max=" + Uri.encode( max_id ) + + "&q=" + Uri.encode( query ); + + try{ + Request request = new Request.Builder() + .url( url ) + .build(); + + Call call = ok_http_client.newCall( request ); + response = call.execute(); + }catch( Throwable ex ){ + ex.printStackTrace(); + return new MSPApiResult( Utils.formatError( ex, context.getResources(), R.string.network_error ) ); + } + + if( callback.isApiCancelled() ) return null; + + if( ! response.isSuccessful() ){ + if( response.code() >= 400 ){ + try{ + String json = response.body().string(); + JSONObject object = new JSONObject( json ); + JSONObject error = object.getJSONObject( "error" ); + // ユーザトークンがダメなら生成しなおす + if( "utoken".equals( error.optString( "detail" ) ) ){ + user_token = null; + continue; + } + return new MSPApiResult( String.format( "API returns error. %s: %s" + , error.optString( "type" ) + , error.optString( "detail" ) + ) ); + }catch( Throwable ex ){ + ex.printStackTrace(); + return new MSPApiResult( Utils.formatError( ex, "API returns error response %s, but can't parse response body.", response.code() ) ); + } + }else{ + return new MSPApiResult( context.getString( R.string.network_error_arg, response ) ); + } + } + + try{ + //noinspection ConstantConditions + String json = response.body().string(); + JSONArray array = new JSONArray( json ); + return new MSPApiResult( response, json, array ); + }catch( Throwable ex ){ + ex.printStackTrace(); + return new MSPApiResult( Utils.formatError( ex, "API data error" ) ); + } + } + } + } + + public static String getMaxId( JSONArray array, String max_id ){ + // max_id の更新 + int size = array.length(); + if( size > 0 ){ + JSONObject item = array.optJSONObject( size - 1 ); + if( item != null ){ + String sv = item.optString( "msp_id" ); + if( ! TextUtils.isEmpty( sv ) ){ + return sv; + } + } + } + return max_id; + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPToot.java b/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPToot.java new file mode 100644 index 00000000..fd784f16 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/api_msp/entity/MSPToot.java @@ -0,0 +1,159 @@ +package jp.juggler.subwaytooter.api_msp.entity; + +import android.text.TextUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jp.juggler.subwaytooter.api.entity.TootAccount; +import jp.juggler.subwaytooter.api.entity.TootStatusLike; +import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.util.HTMLDecoder; +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class MSPToot extends TootStatusLike { + + public static class List extends ArrayList { + + } + + private static final Pattern reAccountUrl = Pattern.compile("\\Ahttps://([^/#?]+)/@([^/#?]+)\\z"); + + + private static TootAccount parseAccount( LogCategory log, SavedAccount access_info, JSONObject src ){ + + if( src == null ) return null; + + TootAccount dst = new TootAccount(); + dst.url = Utils.optStringX( src, "url" ); + dst.username = Utils.optStringX( src, "username" ); + dst.avatar = dst.avatar_static = Utils.optStringX( src, "avatar" ); + + String sv = Utils.optStringX( src, "display_name" ); + if( TextUtils.isEmpty( sv ) ){ + dst.display_name = dst.username; + }else{ + dst.display_name = TootAccount.filterDisplayName( sv ); + } + + dst.id = src.optLong( "id" ); + dst.note = HTMLDecoder.decodeHTML( access_info, Utils.optStringX( src, "note" ), true, null ); + + if( TextUtils.isEmpty( dst.url ) ){ + log.e( "parseAccount: missing url" ); + return null; + } + Matcher m = reAccountUrl.matcher( dst.url ); + if( ! m.find() ){ + log.e( "parseAccount: not account url: %s", dst.url ); + return null; + }else{ + dst.acct = dst.username + "@" + m.group( 1 ); + } + + return dst; + } + +// private static final Pattern reTime = Pattern.compile( "\\A(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)" ); +// +// private static final TimeZone tz_tokyo = TimeZone.getTimeZone( "Asia/Tokyo" ); +// +// private static long parseMSPTime( LogCategory log, String strTime ){ +// if( ! TextUtils.isEmpty( strTime ) ){ +// try{ +// Matcher m = reTime.matcher( strTime ); +// if( ! m.find() ){ +// log.d( "!!invalid time format: %s", strTime ); +// }else{ +// GregorianCalendar g = new GregorianCalendar( tz_tokyo ); +// g.set( +// Utils.parse_int( m.group( 1 ), 1 ), +// Utils.parse_int( m.group( 2 ), 1 ) - 1, +// Utils.parse_int( m.group( 3 ), 1 ), +// Utils.parse_int( m.group( 4 ), 0 ), +// Utils.parse_int( m.group( 5 ), 0 ), +// Utils.parse_int( m.group( 6 ), 0 ) +// ); +// g.set( Calendar.MILLISECOND, 0 ); +// return g.getTimeInMillis(); +// } +// }catch( Throwable ex ){// ParseException, ArrayIndexOutOfBoundsException +// ex.printStackTrace(); +// log.e( ex, "parseMSPTime failed. src=%s", strTime ); +// } +// } +// return 0L; +// } + + public String created_at; + public ArrayList media_attachments; + public long msp_id; + + private static MSPToot parse( LogCategory log, SavedAccount access_info,JSONObject src ){ + if( src == null ) return null; + MSPToot dst = new MSPToot(); + + dst.account =parseAccount( log, access_info, src.optJSONObject( "account" )); + if( dst.account == null ){ + log.e("missing status account"); + return null; + } + + dst.url = Utils.optStringX( src, "url" ); + dst.status_host = dst.account.getAcctHost(); + dst.id = src.optLong( "id" ,-1L ); + + if( TextUtils.isEmpty( dst.url ) || TextUtils.isEmpty( dst.status_host ) || dst.id == -1L ){ + log.e("missing status url or host or id"); + return null; + } + + dst.created_at = Utils.optStringX( src, "created_at" ); + + JSONArray a = src.optJSONArray( "media_attachments" ); + if( a != null && a.length() > 0 ){ + dst.media_attachments = new ArrayList<>(); + for(int i=0,ie=a.length();i mMemoryCache = new LruCache<>( 2048 ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java index 1eefa8d1..f61d56e3 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.java @@ -6,6 +6,7 @@ import android.content.ContentResolver; import android.content.Context; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -1107,4 +1108,23 @@ public class Utils { } return sb.toString(); } + + public static byte[] loadRawResource(Context context,int res_id){ + try{ + InputStream is = context.getResources().openRawResource( res_id ); + try{ + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + IOUtils.copy( is, bao ); + + return bao.toByteArray(); + + }finally{ + IOUtils.closeQuietly( is ); + } + + }catch( Throwable ex ){ + ex.printStackTrace(); + } + return null; + } } diff --git a/app/src/main/res/layout/page_column.xml b/app/src/main/res/layout/page_column.xml index 6de009f9..e773d96f 100644 --- a/app/src/main/res/layout/page_column.xml +++ b/app/src/main/res/layout/page_column.xml @@ -244,6 +244,17 @@ + + --> + + + + + + + diff --git a/app/src/main/res/raw/search_desc_portal_en.txt b/app/src/main/res/raw/search_desc_portal_en.txt new file mode 100644 index 00000000..2b5d2fcb --- /dev/null +++ b/app/src/main/res/raw/search_desc_portal_en.txt @@ -0,0 +1 @@ +Toot search Powered by Mastodon Search Portal, it indexes instances in Japan.
If you want to remove your toot from search result, please contact to @mastodonsearch@mstdn.jp. diff --git a/app/src/main/res/raw/search_desc_portal_ja.txt b/app/src/main/res/raw/search_desc_portal_ja.txt new file mode 100644 index 00000000..53a65696 --- /dev/null +++ b/app/src/main/res/raw/search_desc_portal_ja.txt @@ -0,0 +1 @@ +powered by マストドン検索ポータル
検索結果からあなたのトゥートを除外したい場合、@mastodonsearch@mstdn.jpに連絡してください。 diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d61e35d5..7ed1e500 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -366,8 +366,13 @@ TextToSpeech shutdown… Show buttons bar at the top of posting screen Client name (access token update required) + Toot search + Mastodon Search Portal(JP) + Account/Hashtag search using Mastodon API. + Toot Rechercher \"%1$s\" + Syncing Toot… - + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 68113f52..01c34bad 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -653,5 +653,10 @@ TextToSpeechの後処理… 投稿画面のボタンバーを上端に表示 クライアント名(アクセストークンの更新が必要) + トゥート検索 + マストドン検索ポータル(JP) + アカウントやハッシュタグをマストドンのAPIで検索します。 + トゥート検索:%1$s + トゥートを同期してます… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da960d44..995def6d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -361,4 +361,10 @@ TextToSpeech shutdown… Show buttons bar at the top of posting screen Client name (access token update required) + Toot search + Mastodon Search Portal(JP) + Account/Hashtag search using Mastodon API. + Toot search \"%1$s\" + Syncing Toot… +