(試験実装)マストドン2.1のリスト機能に対応

This commit is contained in:
tateisu 2017-12-07 10:38:56 +09:00
parent ba6a8cb7d3
commit 797edbcd3d
40 changed files with 1666 additions and 68 deletions

View File

@ -67,6 +67,7 @@ import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootApplication;
import jp.juggler.subwaytooter.api.entity.TootList;
import jp.juggler.subwaytooter.api.entity.TootNotification;
import jp.juggler.subwaytooter.api.entity.TootRelationShip;
import jp.juggler.subwaytooter.api.entity.TootResults;
@ -110,7 +111,7 @@ public class ActMain extends AppCompatActivity
SharedPreferences pref;
public Handler handler;
AppState app_state;
public AppState app_state;
// onActivityResultで設定されてonResumeで消化される
// 状態保存の必要なし
@ -731,6 +732,9 @@ public class ActMain extends AppCompatActivity
}else if( id == R.id.nav_add_domain_blocks ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_DOMAIN_BLOCKS );
}else if( id == R.id.nav_add_list ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_LIST_LIST );
}else if( id == R.id.nav_follow_requests ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_FOLLOW_REQUESTS );
@ -784,8 +788,8 @@ public class ActMain extends AppCompatActivity
static final int COLUMN_WIDTH_MIN_DP = 300;
Typeface timeline_font;
Typeface timeline_font_bold;
public Typeface timeline_font;
public Typeface timeline_font_bold;
boolean dont_crop_media_thumbnail;
boolean mShortAcctLocalUser;
@ -1813,6 +1817,7 @@ public class ActMain extends AppCompatActivity
Utils.showToast( ActMain.this, false, R.string.app_was_muted );
}
//////////////////////////////////////////////////////////////
interface FindAccountCallback {
@ -3698,7 +3703,7 @@ public class ActMain extends AppCompatActivity
if( relation.muting ){
for( Column column : app_state.column_list ){
column.removeStatusByAccount( access_info, who.id );
column.removeAccountInTimeline( access_info, who.id );
}
}else{
for( Column column : app_state.column_list ){
@ -3778,7 +3783,7 @@ public class ActMain extends AppCompatActivity
for( Column column : app_state.column_list ){
if( relation.blocking ){
column.removeStatusByAccount( access_info, who.id );
column.removeAccountInTimeline( access_info, who.id );
}else{
column.removeFromBlockList( access_info, who.id );
}
@ -4278,7 +4283,7 @@ public class ActMain extends AppCompatActivity
llColumnStrip.setColor( c );
}
private ArrayList< SavedAccount > makeAccountList( @NonNull LogCategory log, boolean bAllowPseudo, @Nullable String pickup_host ){
public ArrayList< SavedAccount > makeAccountList( @NonNull LogCategory log, boolean bAllowPseudo, @Nullable String pickup_host ){
ArrayList< SavedAccount > list_same_host = new ArrayList<>();
ArrayList< SavedAccount > list_other_host = new ArrayList<>();
@ -4871,4 +4876,169 @@ public class ActMain extends AppCompatActivity
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////
public void createNewList( @NonNull final SavedAccount access_info, @NonNull final String title ){
new AsyncTask< Void, Void, TootApiResult >() {
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( String s ){
}
} );
client.setAccount( access_info );
JSONObject content = new JSONObject( );
try{
content.put("title",title);
}catch(Throwable ex){
return new TootApiResult( Utils.formatError( ex,"can't encoding json parameter." ) );
}
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_JSON
, content.toString()
) );
TootApiResult result = client.request( "/api/v1/lists" , request_builder );
if( result != null ){
if( result.object != null ){
list = TootList.parse( result.object );
}
}
return result;
}
TootList list;
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( list != null ){
for( Column column : app_state.column_list ){
column.onListListUpdated( access_info );
}
Utils.showToast(ActMain.this, false, R.string.list_created);
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
public void callDeleteList( @NonNull final SavedAccount access_info, final long list_id ){
new AsyncTask< Void, Void, TootApiResult >() {
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( String s ){
}
} );
client.setAccount( access_info );
Request.Builder request_builder = new Request.Builder().delete();
TootApiResult result = client.request( "/api/v1/lists/"+ list_id , request_builder );
return result;
}
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( result.object != null ){
for( Column column : app_state.column_list ){
column.onListListUpdated( access_info );
}
Utils.showToast(ActMain.this, false, R.string.delete_succeeded );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
public void callDeleteListMember( @NonNull final SavedAccount access_info, @NonNull final TootAccount who ,final long list_id ){
new AsyncTask< Void, Void, TootApiResult >() {
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActMain.this, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( String s ){
}
} );
client.setAccount( access_info );
Request.Builder request_builder = new Request.Builder().delete();
TootApiResult result = client.request( "/api/v1/lists/"+list_id+"/accounts?account_ids[]="+ who.id , request_builder );
return result;
}
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( result.object != null ){
for( Column column : app_state.column_list ){
column.onListMemberUpdated( access_info ,list_id,who,false);
}
Utils.showToast(ActMain.this, false, R.string.delete_succeeded );
}else{
Utils.showToast( ActMain.this, false, result.error );
}
}
}.executeOnExecutor( App1.task_executor );
}
}

View File

@ -384,7 +384,7 @@ public class App1 extends Application {
}
@SuppressLint("StaticFieldLeak")
private static AppState app_state;
public static AppState app_state;
static AppState getAppState( @NonNull Context context ){

View File

@ -41,7 +41,7 @@ import jp.juggler.subwaytooter.util.MyClickableSpan;
import jp.juggler.subwaytooter.util.PostAttachment;
import jp.juggler.subwaytooter.util.Utils;
class AppState {
public class AppState {
static final LogCategory log = new LogCategory( "AppState" );
final Context context;
final float density;
@ -98,7 +98,7 @@ class AppState {
}
private static final String FILE_COLUMN_LIST = "column_list";
final ArrayList< Column > column_list = new ArrayList<>();
public final ArrayList< Column > column_list = new ArrayList<>();
JSONArray encodeColumnList(){
JSONArray array = new JSONArray();

View File

@ -32,6 +32,7 @@ import jp.juggler.subwaytooter.api.entity.TootContext;
import jp.juggler.subwaytooter.api.entity.TootDomainBlock;
import jp.juggler.subwaytooter.api.entity.TootGap;
import jp.juggler.subwaytooter.api.entity.TootInstance;
import jp.juggler.subwaytooter.api.entity.TootList;
import jp.juggler.subwaytooter.api.entity.TootNotification;
import jp.juggler.subwaytooter.api.entity.TootRelationShip;
import jp.juggler.subwaytooter.api.entity.TootReport;
@ -56,9 +57,10 @@ import jp.juggler.subwaytooter.view.MyListView;
import jp.juggler.subwaytooter.util.ScrollPosition;
import jp.juggler.subwaytooter.util.Utils;
@SuppressWarnings("WeakerAccess") class Column implements StreamReader.Callback {
@SuppressWarnings("WeakerAccess") public class Column implements StreamReader.Callback {
private static final LogCategory log = new LogCategory( "Column" );
interface Callback {
boolean isActivityStart();
}
@ -85,7 +87,7 @@ import jp.juggler.subwaytooter.util.Utils;
return params[ idx ];
}
private static final int READ_LIMIT = 80; // API側の上限が80ですただし指定しても40しか返ってこないことが多い
public static final int READ_LIMIT = 80; // API側の上限が80ですただし指定しても40しか返ってこないことが多い
private static final long LOOP_TIMEOUT = 10000L;
private static final int LOOP_READ_ENOUGH = 30; // フィルタ後のデータ数がコレ以上ならループを諦めます
private static final int RELATIONSHIP_LOAD_STEP = 40;
@ -98,6 +100,7 @@ import jp.juggler.subwaytooter.util.Utils;
private static final String PATH_FAVOURITES = "/api/v1/favourites?limit=" + READ_LIMIT;
private static final String PATH_ACCOUNT_STATUSES = "/api/v1/accounts/%d/statuses?limit=" + READ_LIMIT; // 1:account_id
private static final String PATH_HASHTAG = "/api/v1/timelines/tag/%s?limit=" + READ_LIMIT; // 1: hashtag(url encoded)
private static final String PATH_LIST_TL = "/api/v1/timelines/list/%s?limit=" + READ_LIMIT;
// アカウントのリストを返すAPI
private static final String PATH_ACCOUNT_FOLLOWING = "/api/v1/accounts/%d/following?limit=" + READ_LIMIT; // 1:account_id
@ -107,11 +110,13 @@ import jp.juggler.subwaytooter.util.Utils;
private static final String PATH_FOLLOW_REQUESTS = "/api/v1/follow_requests?limit=" + READ_LIMIT; // 1:account_id
private static final String PATH_BOOSTED_BY = "/api/v1/statuses/%s/reblogged_by?limit=" + READ_LIMIT; // 1:status_id
private static final String PATH_FAVOURITED_BY = "/api/v1/statuses/%s/favourited_by?limit=" + READ_LIMIT; // 1:status_id
private static final String PATH_LIST_MEMBER = "/api/v1/lists/%s/accounts?limit=" + READ_LIMIT;
// 他のリストを返すAPI
private static final String PATH_REPORTS = "/api/v1/reports?limit=" + READ_LIMIT;
private static final String PATH_NOTIFICATIONS = "/api/v1/notifications?limit=" + READ_LIMIT;
private static final String PATH_DOMAIN_BLOCK = "/api/v1/domain_blocks?limit=" + READ_LIMIT;
private static final String PATH_LIST_LIST = "/api/v1/lists?limit=" + READ_LIMIT;
// リストではなくオブジェクトを返すAPI
private static final String PATH_ACCOUNT = "/api/v1/accounts/%d"; // 1:account_id
@ -119,6 +124,7 @@ import jp.juggler.subwaytooter.util.Utils;
private static final String PATH_STATUSES_CONTEXT = "/api/v1/statuses/%d/context"; // 1:status_id
static final String PATH_SEARCH = "/api/v1/search?q=%s"; // 1: query(urlencoded) , also, append "&resolve=1" if resolve non-local accounts
private static final String PATH_INSTANCE = "/api/v1/instance";
private static final String PATH_LIST_INFO = "/api/v1/lists/%s";
static final String KEY_ACCOUNT_ROW_ID = "account_id";
static final String KEY_TYPE = "type";
@ -175,6 +181,9 @@ import jp.juggler.subwaytooter.util.Utils;
static final int TYPE_DOMAIN_BLOCKS = 16;
static final int TYPE_SEARCH_PORTAL = 17;
static final int TYPE_INSTANCE_INFORMATION = 18;
static final int TYPE_LIST_LIST = 19;
static final int TYPE_LIST_TL = 20;
static final int TYPE_LIST_MEMBER = 21;
@NonNull final Context context;
@NonNull private final AppState app_state;
@ -211,6 +220,8 @@ import jp.juggler.subwaytooter.util.Utils;
static final int TAB_FOLLOWING = 1;
static final int TAB_FOLLOWERS = 2;
volatile TootList list_info;
private long status_id;
private String hashtag;
@ -241,6 +252,8 @@ import jp.juggler.subwaytooter.util.Utils;
break;
case TYPE_PROFILE:
case TYPE_LIST_TL:
case TYPE_LIST_MEMBER:
this.profile_id = (Long) getParamAt( params, 0 );
break;
@ -299,6 +312,12 @@ import jp.juggler.subwaytooter.util.Utils;
item.put( KEY_PROFILE_ID, profile_id );
item.put( KEY_PROFILE_TAB, profile_tab );
break;
case TYPE_LIST_MEMBER:
case TYPE_LIST_TL:
item.put( KEY_PROFILE_ID, profile_id );
break;
case TYPE_HASHTAG:
item.put( KEY_HASHTAG, hashtag );
break;
@ -329,7 +348,7 @@ import jp.juggler.subwaytooter.util.Utils;
this.app_state = app_state;
this.context = app_state.context;
long account_db_id = Utils.optLongX( src, KEY_ACCOUNT_ROW_ID );
long account_db_id = Utils.optLongX( src, KEY_ACCOUNT_ROW_ID );
if( account_db_id >= 0 ){
SavedAccount ac = SavedAccount.loadAccount( context, log, account_db_id );
if( ac == null ) throw new RuntimeException( "missing account" );
@ -365,14 +384,19 @@ import jp.juggler.subwaytooter.util.Utils;
case TYPE_CONVERSATION:
case TYPE_BOOSTED_BY:
case TYPE_FAVOURITED_BY:
this.status_id = Utils.optLongX( src , KEY_STATUS_ID );
this.status_id = Utils.optLongX( src, KEY_STATUS_ID );
break;
case TYPE_PROFILE:
this.profile_id = Utils.optLongX( src , KEY_PROFILE_ID );
this.profile_id = Utils.optLongX( src, KEY_PROFILE_ID );
this.profile_tab = src.optInt( KEY_PROFILE_TAB );
break;
case TYPE_LIST_MEMBER:
case TYPE_LIST_TL:
this.profile_id = Utils.optLongX( src, KEY_PROFILE_ID );
break;
case TYPE_HASHTAG:
this.hashtag = src.optString( KEY_HASHTAG );
break;
@ -399,6 +423,8 @@ import jp.juggler.subwaytooter.util.Utils;
return true;
case TYPE_PROFILE:
case TYPE_LIST_TL:
case TYPE_LIST_MEMBER:
try{
long who_id = (Long) getParamAt( params, 0 );
return who_id == this.profile_id;
@ -475,6 +501,16 @@ import jp.juggler.subwaytooter.util.Utils;
, who_account != null ? AcctColor.getNickname( access_info.getFullAcct( who_account ) ) : Long.toString( profile_id )
);
case TYPE_LIST_MEMBER:
return context.getString( R.string.list_member_of
, list_info != null ? list_info.title : Long.toString( profile_id )
);
case TYPE_LIST_TL:
return context.getString( R.string.list_tl_of
, list_info != null ? list_info.title : Long.toString( profile_id )
);
case TYPE_CONVERSATION:
return context.getString( R.string.conversation_around, status_id );
@ -563,6 +599,15 @@ import jp.juggler.subwaytooter.util.Utils;
case TYPE_FOLLOW_REQUESTS:
return context.getString( R.string.follow_requests );
case TYPE_LIST_LIST:
return context.getString( R.string.lists );
case TYPE_LIST_MEMBER:
return context.getString( R.string.list_member );
case TYPE_LIST_TL:
return context.getString( R.string.list_timeline );
}
}
@ -627,6 +672,15 @@ import jp.juggler.subwaytooter.util.Utils;
case TYPE_FOLLOW_REQUESTS:
return R.attr.ic_account_add;
case TYPE_LIST_LIST:
return R.attr.ic_list2;
case TYPE_LIST_MEMBER:
return R.attr.ic_list2;
case TYPE_LIST_TL:
return R.attr.ic_list2;
}
}
@ -673,7 +727,8 @@ import jp.juggler.subwaytooter.util.Utils;
}
// ミュートブロックが成功した時に呼ばれる
void removeStatusByAccount( SavedAccount target_account, long who_id ){
// リストメンバーカラムでメンバーをリストから除去した時に呼ばれる
void removeAccountInTimeline( SavedAccount target_account, long who_id ){
if( ! target_account.acct.equals( access_info.acct ) ) return;
ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() );
@ -685,8 +740,7 @@ import jp.juggler.subwaytooter.util.Utils;
){
continue;
}
}
if( o instanceof TootNotification ){
}else if( o instanceof TootNotification ){
TootNotification item = (TootNotification) o;
if( item.account.id == who_id ) continue;
if( item.status != null ){
@ -695,6 +749,9 @@ import jp.juggler.subwaytooter.util.Utils;
if( item.status.reblog != null && item.status.reblog.account != null && item.status.reblog.account.id == who_id )
continue;
}
}else if( o instanceof TootAccount ){
TootAccount item = (TootAccount) o;
if( item.id == who_id ) continue;
}
tmp_list.add( o );
@ -922,6 +979,27 @@ import jp.juggler.subwaytooter.util.Utils;
}
public void onListListUpdated( @NonNull SavedAccount account ){
if( column_type == TYPE_LIST_LIST && access_info.acct.equals( account.acct ) ){
startLoading();
ColumnViewHolder vh = getViewHolder();
if( vh != null ) vh.onListListUpdated();
}
}
public void onListMemberUpdated( SavedAccount account, long list_id, TootAccount who, boolean bAdd ){
if( column_type == TYPE_LIST_TL && access_info.acct.equals( account.acct ) && list_id == profile_id ){
if( ! bAdd ){
removeAccountInTimeline( account, who.id );
}
}else if( column_type == TYPE_LIST_MEMBER && access_info.acct.equals( account.acct ) && list_id == profile_id ){
if( ! bAdd ){
removeAccountInTimeline( account, who.id );
}
}
}
//////////////////////////////////////////////////////////////////////////////////////
// カラムを閉じた後のnotifyDataSetChangedのタイミングでadd/removeされる順序が期待通りにならないので
@ -1151,13 +1229,13 @@ import jp.juggler.subwaytooter.util.Utils;
}
}
// @Nullable String parseMaxId( TootApiResult result ){
// if( result != null && result.link_older != null ){
// Matcher m = reMaxId.matcher( result.link_older );
// if( m.find() ) return m.group( 1 );
// }
// return null;
// }
// @Nullable String parseMaxId( TootApiResult result ){
// if( result != null && result.link_older != null ){
// Matcher m = reMaxId.matcher( result.link_older );
// if( m.find() ) return m.group( 1 );
// }
// return null;
// }
@NonNull static final VersionString version_1_6 = new VersionString( "1.6" );
@ -1189,6 +1267,13 @@ import jp.juggler.subwaytooter.util.Utils;
}
}
void parseListInfo( TootApiClient client, String path_base ){
TootApiResult result = client.request( path_base );
if( result != null && result.object != null ){
Column.this.list_info = TootList.parse( result.object );
}
}
TootInstance instance_tmp;
TootApiResult getInstanceInformation( @NonNull TootApiClient client, @Nullable String instance_name ){
@ -1355,6 +1440,16 @@ import jp.juggler.subwaytooter.util.Utils;
return result;
}
TootApiResult parseListList( TootApiClient client, String path_base ){
TootApiResult result = client.request( path_base );
if( result != null ){
saveRange( result, true, true );
list_tmp = new ArrayList<>();
list_tmp.addAll( TootList.parseList( result.array ) );
}
return result;
}
TootApiResult parseNotifications( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
@ -1504,6 +1599,19 @@ import jp.juggler.subwaytooter.util.Utils;
case TYPE_DOMAIN_BLOCKS:
return parseDomainList( client, PATH_DOMAIN_BLOCK );
case TYPE_LIST_LIST:
return parseListList( client, PATH_LIST_LIST );
case TYPE_LIST_TL:
parseListInfo( client, String.format( Locale.JAPAN, PATH_LIST_INFO, profile_id ) );
client.callback.publishApiProgress( "" ); // カラムヘッダの表示を更新
return getStatuses( client, String.format( Locale.JAPAN, PATH_LIST_TL, profile_id ) );
case TYPE_LIST_MEMBER:
parseListInfo( client, String.format( Locale.JAPAN, PATH_LIST_INFO, profile_id ) );
client.callback.publishApiProgress( "" ); // カラムヘッダの表示を更新
return parseAccountList( client, String.format( Locale.JAPAN, PATH_LIST_MEMBER, profile_id ) );
case TYPE_FOLLOW_REQUESTS:
return parseAccountList( client, PATH_FOLLOW_REQUESTS );
@ -1693,7 +1801,7 @@ import jp.juggler.subwaytooter.util.Utils;
task.executeOnExecutor( App1.task_executor );
}
private static final Pattern reMaxId = Pattern.compile( "[&?]max_id=(\\d+)" ); // より古いデータの取得に使う
public static final Pattern reMaxId = Pattern.compile( "[&?]max_id=(\\d+)" ); // より古いデータの取得に使う
private static final Pattern reSinceId = Pattern.compile( "[&?]since_id=(\\d+)" ); // より新しいデータの取得に使う
private String max_id;
@ -1702,9 +1810,13 @@ import jp.juggler.subwaytooter.util.Utils;
private void saveRange( TootApiResult result, boolean bBottom, boolean bTop ){
if( result != null ){
if( bBottom && result.link_older != null ){
Matcher m = reMaxId.matcher( result.link_older );
if( m.find() ) max_id = m.group( 1 );
if( bBottom ){
if( result.link_older == null ){
max_id = null;
}else{
Matcher m = reMaxId.matcher( result.link_older );
if( m.find() ) max_id = m.group( 1 );
}
}
if( bTop && result.link_newer != null ){
Matcher m = reSinceId.matcher( result.link_newer );
@ -1715,7 +1827,9 @@ import jp.juggler.subwaytooter.util.Utils;
private boolean saveRangeEnd( TootApiResult result ){
if( result != null ){
if( result.link_older != null ){
if( result.link_older == null ){
max_id = null;
}else{
Matcher m = reMaxId.matcher( result.link_older );
if( m.find() ){
max_id = m.group( 1 );
@ -1964,6 +2078,14 @@ import jp.juggler.subwaytooter.util.Utils;
}
}
void parseListInfo( TootApiClient client, String path_base ){
TootApiResult result = client.request( path_base );
if( result != null && result.object != null ){
Column.this.list_info = TootList.parse( result.object );
}
}
TootApiResult getAccountList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
@ -2067,6 +2189,61 @@ import jp.juggler.subwaytooter.util.Utils;
return result;
}
TootApiResult getListList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
String last_since_id = since_id;
TootApiResult result = client.request( addRange( bBottom, path_base ) );
if( result != null && result.array != null ){
saveRange( result, bBottom, ! bBottom );
list_tmp = new ArrayList<>();
TootList.List src = TootList.parseList( result.array );
list_tmp.addAll( src );
if( ! bBottom ){
for( ; ; ){
if( isCancelled() ){
log.d( "refresh-list-top: cancelled." );
break;
}
// max_id だけを指定した場合必ずlimit個のデータが帰ってくるとは限らない
// 直前のデータが0個なら終了とみなすしかなさそう
if( src.isEmpty() ){
log.d( "refresh-list-top: previous size == 0." );
break;
}
// 隙間の最新のステータスIDは取得データ末尾のステータスIDである
String max_id = Long.toString( src.get( src.size() - 1 ).id );
if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
log.d( "refresh-list-top: timeout. make gap." );
// タイムアウト
// 隙間ができるかもしれない後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
String path = path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id;
TootApiResult result2 = client.request( path );
if( result2 == null || result2.array == null ){
log.d( "refresh-list-top: timeout. error or retry. make gap." );
// エラー
// 隙間ができるかもしれない後ほど手動で試してもらうしかない
TootGap gap = new TootGap( max_id, last_since_id );
list_tmp.add( gap );
break;
}
src = TootList.parseList( result2.array );
list_tmp.addAll( src );
}
}
}
return result;
}
TootApiResult getReportList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
@ -2432,6 +2609,24 @@ import jp.juggler.subwaytooter.util.Utils;
String.format( Locale.JAPAN, PATH_ACCOUNT_FOLLOWERS, profile_id ) );
}
case TYPE_LIST_LIST:
return getListList( client, PATH_LIST_LIST );
case TYPE_LIST_TL:
if( list_info == null ){
parseListInfo( client, String.format( Locale.JAPAN, PATH_LIST_INFO, profile_id ) );
client.callback.publishApiProgress( "" );
}
return getStatusList( client, String.format( Locale.JAPAN, PATH_LIST_TL, profile_id ) );
case TYPE_LIST_MEMBER:
if( list_info == null ){
parseListInfo( client, String.format( Locale.JAPAN, PATH_LIST_INFO, profile_id ) );
client.callback.publishApiProgress( "" );
}
return getAccountList( client, String.format( Locale.JAPAN, PATH_LIST_MEMBER, profile_id ) );
case TYPE_MUTES:
return getAccountList( client, PATH_MUTES );
@ -2848,6 +3043,9 @@ import jp.juggler.subwaytooter.util.Utils;
case TYPE_FEDERATE:
return getStatusList( client, PATH_FEDERATE );
case TYPE_LIST_TL:
return getStatusList( client, String.format( Locale.JAPAN, PATH_LIST_TL, profile_id ) );
case TYPE_FAVOURITES:
return getStatusList( client, PATH_FAVOURITES );
@ -3004,7 +3202,7 @@ import jp.juggler.subwaytooter.util.Utils;
private void setItemTop( @NonNull ColumnViewHolder holder, int idx, int y ){
MyListView listView = holder.getListView();
boolean hasHeader = holder.hasHeaderView();
boolean hasHeader = holder.getHeaderView() != null;
if( hasHeader ){
// Adapter中から見たpositionとListViewから見たpositionにズレができる
idx = idx + 1;
@ -3021,7 +3219,7 @@ import jp.juggler.subwaytooter.util.Utils;
private int getItemTop( @NonNull ColumnViewHolder holder, int idx ){
MyListView listView = holder.getListView();
boolean hasHeader = holder.hasHeaderView();
boolean hasHeader = holder.getHeaderView() != null;
if( hasHeader ){
// Adapter中から見たpositionとListViewから見たpositionにズレができる
@ -3220,10 +3418,19 @@ import jp.juggler.subwaytooter.util.Utils;
case TYPE_HASHTAG:
app_state.stream_reader.unregister(
access_info
, StreamReader.EP_HASHTAG + "?tag=" + Uri.encode( hashtag )
, StreamReader.EP_HASHTAG + Uri.encode( hashtag )
, this
);
break;
case TYPE_LIST_TL:
app_state.stream_reader.unregister(
access_info
, StreamReader.EP_LIST_TL + Long.toString( profile_id )
, this
);
break;
}
}
@ -3314,18 +3521,16 @@ import jp.juggler.subwaytooter.util.Utils;
case TYPE_CONVERSATION:
return true;
}
// static final int TYPE_FAVOURITES = 5;
// static final int TYPE_REPORTS = 6;
// static final int TYPE_MUTES = 11;
// static final int TYPE_BLOCKS = 12;
// static final int TYPE_FOLLOW_REQUESTS = 13;
// 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_FAVOURITES = 5;
// static final int TYPE_REPORTS = 6;
// static final int TYPE_MUTES = 11;
// static final int TYPE_BLOCKS = 12;
// static final int TYPE_FOLLOW_REQUESTS = 13;
// static final int TYPE_BOOSTED_BY = 14;
// static final int TYPE_FAVOURITED_BY = 15;
// static final int TYPE_DOMAIN_BLOCKS = 16;
}
boolean canStreaming(){
switch( column_type ){
default:
@ -3336,6 +3541,7 @@ import jp.juggler.subwaytooter.util.Utils;
case TYPE_LOCAL:
case TYPE_FEDERATE:
case TYPE_HASHTAG:
case TYPE_LIST_TL:
return ! access_info.isPseudo();
}
}
@ -3426,10 +3632,41 @@ import jp.juggler.subwaytooter.util.Utils;
case TYPE_HASHTAG:
app_state.stream_reader.register(
access_info
, StreamReader.EP_HASHTAG + "&tag=" + Uri.encode( hashtag )
, StreamReader.EP_HASHTAG + Uri.encode( hashtag )
, this
);
break;
case TYPE_LIST_TL:
app_state.stream_reader.register(
access_info
, StreamReader.EP_LIST_TL + Long.toString( profile_id )
, this
);
break;
}
}
public @NonNull String getListTitle(){
switch(column_type){
default:
return "?";
case TYPE_LIST_MEMBER:
case TYPE_LIST_TL:
String sv = list_info == null ? null : list_info.title;
return !TextUtils.isEmpty( sv ) ? sv : Long.toString( profile_id );
}
}
public long getListId(){
switch(column_type){
default:
return -1L;
case TYPE_LIST_MEMBER:
case TYPE_LIST_TL:
return profile_id;
}
}

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
@ -83,6 +84,11 @@ class ColumnViewHolder
private final View llRegexFilter;
private final Button btnDeleteNotification;
private final View llListList;
private final EditText etListName;
private final View btnListAdd;
ColumnViewHolder( ActMain arg_activity, View root ){
this.activity = arg_activity;
@ -124,6 +130,22 @@ class ColumnViewHolder
cbResolve = root.findViewById( R.id.cbResolve );
llSearch = root.findViewById( R.id.llSearch );
llListList = root.findViewById( R.id.llListList );
btnListAdd= root.findViewById( R.id.btnListAdd );
etListName= root.findViewById( R.id.etListName );
btnListAdd.setOnClickListener( this );
etListName.setOnEditorActionListener( new TextView.OnEditorActionListener() {
@Override public boolean onEditorAction( TextView v, int actionId, KeyEvent event ){
boolean handled = false;
if( actionId == EditorInfo.IME_ACTION_SEND ){
btnListAdd.performClick();
handled = true;
}
return handled;
}
} );
llColumnSetting = root.findViewById( R.id.llColumnSetting );
@ -261,6 +283,7 @@ class ColumnViewHolder
case Column.TYPE_INSTANCE_INFORMATION:
status_adapter.header = new HeaderViewHolderInstance(activity, column, listView );
break;
}
// 添付メディアや正規表現のフィルタ
@ -343,6 +366,7 @@ class ColumnViewHolder
vg( btnDeleteNotification, column.column_type == Column.TYPE_NOTIFICATIONS );
vg( llSearch, ( column.column_type == Column.TYPE_SEARCH || column.column_type == Column.TYPE_SEARCH_PORTAL ) );
vg( llListList, ( column.column_type == Column.TYPE_LIST_LIST ) );
vg( cbResolve, ( column.column_type == Column.TYPE_SEARCH ) );
// tvRegexFilterErrorの表示を更新
@ -468,7 +492,7 @@ class ColumnViewHolder
}
}
private void loadBackgroundImage( final ImageView iv, final String url ){
@SuppressLint("StaticFieldLeak") private void loadBackgroundImage( final ImageView iv, final String url ){
try{
if( TextUtils.isEmpty( url ) ){
// 指定がないなら閉じる
@ -571,6 +595,10 @@ class ColumnViewHolder
llColumnSetting.setVisibility( View.GONE );
}
void onListListUpdated(){
etListName.setText( "" );
}
@Override public void onRefresh( SwipyRefreshLayoutDirection direction ){
if( column == null ) return;
@ -714,6 +742,15 @@ class ColumnViewHolder
int idx = activity.app_state.column_list.indexOf( column );
ActColumnCustomize.open( activity, idx, ActMain.REQUEST_CODE_COLUMN_COLOR );
break;
case R.id.btnListAdd:
String tv = etListName.getText().toString().trim();
if( TextUtils.isEmpty( tv ) ){
Utils.showToast( activity, true, R.string.list_name_empty );
return;
}
activity.createNewList( column.access_info, tv );
}
}
@ -737,8 +774,8 @@ class ColumnViewHolder
/////////////////////////////////////////////////////////////////
// Column から呼ばれる
boolean hasHeaderView(){
return status_adapter != null && status_adapter.header != null;
@Nullable HeaderViewHolderBase getHeaderView(){
return status_adapter == null ? null : status_adapter.header;
}
SwipyRefreshLayout getRefreshLayout(){

View File

@ -20,6 +20,8 @@ import jp.juggler.subwaytooter.api.entity.TootAccount;
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.dialog.DlgConfirm;
import jp.juggler.subwaytooter.dialog.DlgListMemberAdd;
import jp.juggler.subwaytooter.dialog.DlgQRCode;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.table.UserRelation;
@ -40,8 +42,6 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener {
private final Dialog dialog;
private final ArrayList< SavedAccount > account_list_non_pseudo = new ArrayList<>();
DlgContextMenu(
@NonNull ActMain activity
, @NonNull Column column
@ -102,6 +102,9 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener {
View btnHideBoost = viewRoot.findViewById( R.id.btnHideBoost );
View btnShowBoost = viewRoot.findViewById( R.id.btnShowBoost );
View btnListMemberAdd= viewRoot.findViewById( R.id.btnListMemberAdd );
View btnListMemberRemove= viewRoot.findViewById( R.id.btnListMemberRemove );
btnStatusWebPage.setOnClickListener( this );
btnText.setOnClickListener( this );
btnFavouriteAnotherAccount.setOnClickListener( this );
@ -129,6 +132,8 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener {
btnConversationMute.setOnClickListener( this );
btnHideBoost.setOnClickListener( this );
btnShowBoost.setOnClickListener( this );
btnListMemberAdd.setOnClickListener( this );
btnListMemberRemove.setOnClickListener( this );
viewRoot.findViewById( R.id.btnQuoteUrlStatus ).setOnClickListener( this );
viewRoot.findViewById( R.id.btnQuoteUrlAccount ).setOnClickListener( this );
@ -137,6 +142,7 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener {
final ArrayList< SavedAccount > account_list = SavedAccount.loadAccountList( activity, log );
// final ArrayList< SavedAccount > account_list_non_pseudo_same_instance = new ArrayList<>();
ArrayList< SavedAccount > account_list_non_pseudo = new ArrayList<>();
for( SavedAccount a : account_list ){
if( ! a.isPseudo() ){
account_list_non_pseudo.add( a );
@ -300,6 +306,16 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener {
btnOpenTimeline.setText( activity.getString( R.string.open_local_timeline_for, host ) );
}
if( access_info.isPseudo() ){
btnListMemberAdd.setVisibility( View.VISIBLE );
btnListMemberRemove.setVisibility( View.GONE );
}else{
btnListMemberAdd.setVisibility( View.VISIBLE );
btnListMemberRemove.setVisibility(
column.column_type == Column.TYPE_LIST_MEMBER || column.column_type == Column.TYPE_LIST_TL
? View.VISIBLE : View.GONE
);
}
}
void show(){
@ -619,7 +635,24 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener {
activity.callFollowingReblogs( access_info, who, true );
}
break;
case R.id.btnListMemberAdd:
if( who != null ){
new DlgListMemberAdd( activity,who, access_info,column.getListId() ).show();
}
break;
case R.id.btnListMemberRemove:
if( who != null ){
final long list_id = column.getListId();
String list_title = column.getListTitle();
DlgConfirm.openSimple( activity, activity.getString( R.string.list_member_delete_confirm, access_info.getFullAcct( who ), list_title ), new Runnable() {
@Override public void run(){
activity.callDeleteListMember( access_info, who ,list_id );
}
} );
}
break;
}
}

View File

@ -1,9 +1,8 @@
package jp.juggler.subwaytooter;
import android.content.Context;
import android.app.Activity;
import android.content.DialogInterface;
import android.graphics.Typeface;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
@ -28,10 +27,13 @@ import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.api.entity.TootCard;
import jp.juggler.subwaytooter.api.entity.TootDomainBlock;
import jp.juggler.subwaytooter.api.entity.TootGap;
import jp.juggler.subwaytooter.api.entity.TootList;
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.dialog.ActionsDialog;
import jp.juggler.subwaytooter.dialog.DlgConfirm;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.ContentWarning;
import jp.juggler.subwaytooter.table.MediaShown;
@ -42,8 +44,6 @@ import jp.juggler.subwaytooter.util.EmojiImageSpan;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator;
import jp.juggler.subwaytooter.util.NetworkEmojiSpan;
import jp.juggler.subwaytooter.view.MyEditText;
import jp.juggler.subwaytooter.view.MyLinkMovementMethod;
import jp.juggler.subwaytooter.view.MyListView;
import jp.juggler.subwaytooter.view.MyNetworkImageView;
@ -98,6 +98,10 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
private final View llSearchTag;
private final Button btnSearchTag;
private final View llList;
private final Button btnListTL;
private final View btnListMore;
private final LinearLayout llExtra;
@Nullable private final TextView tvApplication;
@ -110,6 +114,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
@Nullable private TootGap gap;
@Nullable private TootDomainBlock domain_block;
@Nullable private TootNotification notification;
@Nullable private TootList list;
private final boolean bSimpleList;
@ -204,6 +209,14 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
this.btnSearchTag = view.findViewById( R.id.btnSearchTag );
this.tvApplication = view.findViewById( R.id.tvApplication );
this. llList= view.findViewById( R.id.llList );
this. btnListTL= view.findViewById( R.id.btnListTL );
this. btnListMore= view.findViewById( R.id.btnListMore );
btnListTL.setOnClickListener( this );
btnListMore.setOnClickListener( this );
btnSearchTag.setOnClickListener( this );
btnContentWarning.setOnClickListener( this );
btnShowMedia.setOnClickListener( this );
@ -251,7 +264,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
if( tvApplication != null ){
tvApplication.setTextSize( activity.timeline_font_size_sp );
}
btnListTL.setTextSize( activity.timeline_font_size_sp );
}
if( ! Float.isNaN( activity.acct_font_size_sp ) ){
@ -284,11 +297,13 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
this.gap = null;
this.domain_block = null;
this.notification = null;
this.list = null;
llBoosted.setVisibility( View.GONE );
llFollow.setVisibility( View.GONE );
llStatus.setVisibility( View.GONE );
llSearchTag.setVisibility( View.GONE );
llList.setVisibility( View.GONE );
llExtra.removeAllViews();
if( item == null ) return;
@ -379,9 +394,17 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
showGap( (TootGap) item );
}else if( item instanceof TootDomainBlock ){
showDomainBlock( (TootDomainBlock) item );
}else if( item instanceof TootList ){
showList( (TootList) item );
}
}
private void showList( TootList list ){
this.list = list;
llList.setVisibility( View.VISIBLE );
btnListTL.setText( list.title );
}
private void showDomainBlock( @NonNull TootDomainBlock domain_block ){
this.gap = null;
this.domain_block = domain_block;
@ -728,7 +751,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
@Override public void onClick( View v ){
int pos = activity.nextPosition( column );
final int pos = activity.nextPosition( column );
switch( v.getId() ){
case R.id.btnHideMedia:
@ -816,7 +839,38 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
.show();
}
break;
case R.id.btnListTL:
if( list != null ){
activity.addColumn( pos,access_info,Column.TYPE_LIST_TL,list.id);
}
break;
case R.id.btnListMore:
if( list != null ){
ActionsDialog ad = new ActionsDialog();
ad.addAction( activity.getString( R.string.list_timeline ), new Runnable() {
@Override public void run(){
activity.addColumn( pos,access_info,Column.TYPE_LIST_TL,list.id);
}
} );
ad.addAction( activity.getString( R.string.list_member ), new Runnable() {
@Override public void run(){
activity.addColumn( pos,access_info,Column.TYPE_LIST_MEMBER,list.id);
}
} );
ad.addAction( activity.getString( R.string.delete ), new Runnable() {
@Override public void run(){
DlgConfirm.openSimple( activity, activity.getString( R.string.list_delete_confirm, list.title ), new Runnable() {
@Override public void run(){
activity.callDeleteList( access_info, list.id );
}
} );
}
} );
ad.show( activity ,list.title );
}
break;
}
}

View File

@ -34,7 +34,8 @@ import okhttp3.WebSocketListener;
static final String EP_USER = "/api/v1/streaming/?stream=user";
static final String EP_PUBLIC = "/api/v1/streaming/?stream=public";
static final String EP_PUBLIC_LOCAL = "/api/v1/streaming/?stream=public:local";
static final String EP_HASHTAG = "/api/v1/streaming/?stream=hashtag"; // + &tag=hashtag (先頭のを含まない)
static final String EP_HASHTAG = "/api/v1/streaming/?stream=hashtag&tag="; // + hashtag (先頭のを含まない)
static final String EP_LIST_TL = "/api/v1/streaming/?stream=list&list="; // + list_id
static final Pattern reNumber = Pattern.compile( "([-]?\\d+)" );

View File

@ -7,7 +7,6 @@ import android.text.Spannable;
import android.text.TextUtils;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.EmojiDecoder;
import org.json.JSONArray;
import org.json.JSONObject;
@ -189,6 +188,16 @@ public class TootAccount {
}
private static final Pattern reWhitespace = Pattern.compile( "[\\s\\t\\x0d\\x0a]+" );
public Spannable decodeDisplayName( Context context ){
// remove white spaces
String sv = reWhitespace.matcher( display_name ).replaceAll( " " );
// decode emoji code
return new DecodeOptions().setProfileEmojis( profile_emojis ).decodeEmoji( context, sv );
}
public void setDisplayName( Context context, String username, String sv ){
if( TextUtils.isEmpty( sv ) ){
@ -196,12 +205,7 @@ public class TootAccount {
}else{
this.display_name = Utils.sanitizeBDI( sv );
}
// remove white spaces
sv = reWhitespace.matcher( this.display_name ).replaceAll( " " );
// decode emoji code
this.decoded_display_name = new DecodeOptions().setProfileEmojis( this.profile_emojis ).decodeEmoji( context, sv );
this.decoded_display_name = decodeDisplayName(context);
}

View File

@ -0,0 +1,57 @@
package jp.juggler.subwaytooter.api.entity;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collection;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
public class TootList {
private static final LogCategory log = new LogCategory( "TootList" );
public long id;
@Nullable public String title;
@Nullable
public static TootList parse( JSONObject src ){
if( src == null ) return null;
try{
TootList dst = new TootList();
dst.id = Utils.optLongX( src, "id" );
dst.title = Utils.optStringX( src, "title" );
return dst;
}catch( Throwable ex ){
log.trace( ex );
log.e( ex, "parse failed." );
return null;
}
}
public static class List extends ArrayList< TootList > {
}
@NonNull public static List parseList( JSONArray array ){
TootList.List result = new TootList.List();
if( array != null ){
int array_size = array.length();
result.ensureCapacity( array_size );
for( int i = 0 ; i < array_size ; ++ i ){
JSONObject obj = array.optJSONObject( i );
if( obj != null ) {
TootList dst = TootList.parse( obj );
if( dst != null ) result.add( dst );
}
}
}
return result;
}
}

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.dialog;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.DialogInterface;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.CheckBox;
@ -21,9 +22,9 @@ public class DlgConfirm {
}
public static void open(
final Activity activity
, String message
, final Callback callback
@NonNull final Activity activity
, @NonNull String message
, @NonNull final Callback callback
){
if( ! callback.isConfirmEnabled() ){
@ -52,5 +53,28 @@ public class DlgConfirm {
.show();
}
public static void openSimple(@NonNull final Activity activity
, @NonNull String message
, @NonNull final Runnable callback
){
@SuppressLint("InflateParams")
final View view = activity.getLayoutInflater().inflate( R.layout.dlg_confirm, null, false );
final TextView tvMessage = view.findViewById( R.id.tvMessage );
final CheckBox cbSkipNext = view.findViewById( R.id.cbSkipNext );
tvMessage.setText( message );
cbSkipNext.setVisibility( View.GONE );
new AlertDialog.Builder( activity )
.setView( view )
.setCancelable( true )
.setNegativeButton( R.string.cancel, null )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override public void onClick( DialogInterface dialog, int which ){
callback.run();
}
} )
.show();
}
}

View File

@ -0,0 +1,624 @@
package jp.juggler.subwaytooter.dialog;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.TextUtils;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
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.ActMain;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.Column;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.Styler;
import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootList;
import jp.juggler.subwaytooter.api.entity.TootRelationShip;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator;
import jp.juggler.subwaytooter.util.Utils;
import jp.juggler.subwaytooter.view.MyNetworkImageView;
import okhttp3.Request;
import okhttp3.RequestBody;
public class DlgListMemberAdd implements View.OnClickListener {
private static final LogCategory log = new LogCategory( "DlgListMemberAdd" );
@NonNull private final ActMain activity;
// @NonNull private final TootAccount target_user;
@NonNull private final String target_user_full_acct;
private SavedAccount list_owner;
private long list_id;
@NonNull private final Dialog dialog;
private final Button btnListOwner;
private final Button btnList;
@NonNull private final ArrayList< SavedAccount > account_list;
@Nullable private ArrayList< TootList > list_list;
public DlgListMemberAdd( @NonNull ActMain _activity, @NonNull TootAccount who, @NonNull SavedAccount _list_owner, long list_id ){
this.activity = _activity;
// this.target_user = who;
this.target_user_full_acct = _list_owner.getFullAcct( who );
this.account_list = activity.makeAccountList( log, false, null );
if( _list_owner.isPseudo() ){
this.list_owner = null;
this.list_id = - 1L;
}else{
this.list_owner = _list_owner;
this.list_id = list_id; // -1Lならリスト無し
}
@SuppressLint("InflateParams") final View view = activity.getLayoutInflater().inflate( R.layout.dlg_list_member_add, null, false );
MyNetworkImageView ivUser = view.findViewById( R.id.ivUser );
TextView tvUserName = view.findViewById( R.id.tvUserName );
TextView tvUserAcct = view.findViewById( R.id.tvUserAcct );
btnListOwner = view.findViewById( R.id.btnListOwner );
btnList = view.findViewById( R.id.btnList );
view.findViewById( R.id.btnCancel ).setOnClickListener( this );
view.findViewById( R.id.btnOk ).setOnClickListener( this );
ivUser.setImageUrl( App1.pref, 16f, who.avatar_static, who.avatar );
NetworkEmojiInvalidator user_name_invalidator = new NetworkEmojiInvalidator( activity.handler, tvUserName );
Spannable name = who.decodeDisplayName( activity );
tvUserName.setText( name );
user_name_invalidator.register( name );
tvUserAcct.setText( target_user_full_acct );
btnListOwner.setOnClickListener( this );
btnList.setOnClickListener( this );
setListOwner( list_owner, list_id );
this.dialog = new Dialog( activity );
dialog.setContentView( view );
}
public void show(){
//noinspection ConstantConditions
dialog.getWindow().setLayout( WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT );
dialog.show();
}
@Override public void onClick( View v ){
switch( v.getId() ){
case R.id.btnListOwner:
AccountPicker.pick( activity, false, false, null, account_list, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
// アカウントが変更された時だけリストIDを変更する
long new_list_id = ( list_owner != null && ai.acct.equals( list_owner.acct ) ? list_id : - 1L );
setListOwner( ai, new_list_id );
}
} );
break;
case R.id.btnList:
openListPicker();
break;
case R.id.btnCancel:
try{
dialog.cancel();
}catch( Throwable ignored ){
}
break;
case R.id.btnOk:
addListMember(false);
break;
}
}
private void setListOwner( @Nullable SavedAccount a, long new_list_id ){
this.list_owner = a;
if( a == null ){
btnListOwner.setText( R.string.not_selected );
btnListOwner.setTextColor( Styler.getAttributeColor( activity, android.R.attr.textColorPrimary ) );
btnListOwner.setBackgroundResource( R.drawable.btn_bg_transparent );
//
}else{
String acct = a.getFullAcct( a );
AcctColor ac = AcctColor.load( acct );
String nickname = AcctColor.hasNickname( ac ) ? ac.nickname : acct;
btnListOwner.setText( nickname );
if( AcctColor.hasColorBackground( ac ) ){
btnListOwner.setBackgroundColor( ac.color_bg );
}else{
btnListOwner.setBackgroundResource( R.drawable.btn_bg_transparent );
}
if( AcctColor.hasColorForeground( ac ) ){
btnListOwner.setTextColor( ac.color_fg );
}else{
btnListOwner.setTextColor( Styler.getAttributeColor( activity, android.R.attr.textColorPrimary ) );
}
}
loadLists( new_list_id );
}
@SuppressLint("StaticFieldLeak")
private void loadLists( final long new_list_id ){
if( list_owner == null ){
showList( null, - 1L );
return;
}
//noinspection deprecation
final ProgressDialog progress = new ProgressDialog( activity );
final AsyncTask< Void, String, TootApiResult > task = new AsyncTask< Void, String, TootApiResult >() {
ArrayList< TootList > list_list = new ArrayList<>();
void showProgress( final String sv ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
progress.setMessage( sv );
}
} );
}
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( activity, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( final String sv ){
showProgress( sv );
}
} );
client.setAccount( list_owner );
String path_base = "/api/v1/lists?limit=" + Column.READ_LIMIT;
// head
TootApiResult result = client.request( path_base );
if( result == null || result.array == null ){
list_list = null;
return result;
}
showProgress( activity.getString( R.string.parsing_response ) );
list_list.addAll( TootList.parseList( result.array ) );
String max_id;
if( result.link_older == null ){
max_id = null;
}else{
Matcher m = Column.reMaxId.matcher( result.link_older );
max_id = m.find() ? m.group( 1 ) : null;
}
// trail
while( max_id != null ){
result = client.request( path_base + "&max_id=" + max_id );
if( result == null || result.array == null ){
list_list = null;
return result;
}
showProgress( activity.getString( R.string.parsing_response ) );
list_list.addAll( TootList.parseList( result.array ) );
if( result.link_older == null ){
max_id = null;
}else{
Matcher m = Column.reMaxId.matcher( result.link_older );
max_id = m.find() ? m.group( 1 ) : null;
}
}
return result;
}
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
try{
progress.dismiss();
}catch( Throwable ignored ){
}
showList( list_list, new_list_id );
//noinspection StatementWithEmptyBody
if( result != null
&& ! TextUtils.isEmpty( result.error )
&& !(result.response !=null && result.response.code() ==404 )
){
Utils.showToast( activity, true, result.error );
}
}
};
progress.setIndeterminate( true );
progress.setCancelable( false );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override public void onCancel( DialogInterface dialog ){
task.cancel( true );
}
} );
progress.show();
task.executeOnExecutor( App1.task_executor );
}
private void showList( @Nullable ArrayList< TootList > _list, long new_list_id ){
this.list_list = _list;
if( list_list == null ){
list_id = - 1L;
btnList.setText( R.string.cant_access_list );
return;
}
for( TootList l : list_list ){
if( l.id == new_list_id ){
this.list_id = new_list_id;
btnList.setText( l.title );
return;
}
}
this.list_id = - 1L;
btnList.setText( R.string.not_selected );
}
private void openListPicker(){
if( list_list == null ) return;
ActionsDialog ad = new ActionsDialog();
ad.addAction( activity.getString( R.string.list_create ), new Runnable() {
@Override public void run(){
openListCreator();
}
} );
for( TootList l : list_list ){
final long list_id = l.id;
ad.addAction( ! TextUtils.isEmpty( l.title ) ? l.title : Long.toString( list_id ), new Runnable() {
@Override public void run(){
showList( list_list, list_id );
}
} );
}
ad.show( activity, null );
}
private void openListCreator(){
DlgTextInput.show( activity, activity.getString( R.string.list_create ), null, new DlgTextInput.Callback() {
@Override public void onEmptyError(){
Utils.showToast( activity, false, R.string.list_name_empty );
}
@Override public void onOK( final Dialog dialog, final String title ){
//noinspection deprecation
final ProgressDialog progress = new ProgressDialog( activity );
@SuppressLint("StaticFieldLeak") final AsyncTask< Void, String, TootApiResult > task
= new AsyncTask< Void, String, TootApiResult >() {
void showProgress( final String sv ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
progress.setMessage( sv );
}
} );
}
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( activity, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( String s ){
}
} );
client.setAccount( list_owner );
JSONObject content = new JSONObject();
try{
content.put( "title", title );
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, "can't encoding json parameter." ) );
}
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_JSON
, content.toString()
) );
TootApiResult result = client.request( "/api/v1/lists", request_builder );
showProgress( activity.getString( R.string.parsing_response ) );
if( result != null ){
if( result.object != null ){
list = TootList.parse( result.object );
}
}
return result;
}
TootList list;
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
try{
progress.dismiss();
}catch( Throwable ignored ){
}
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( list != null ){
for( Column column : activity.app_state.column_list ){
column.onListListUpdated( list_owner );
}
Utils.showToast( activity, false, R.string.list_created );
loadLists( list.id );
try{
dialog.dismiss();
}catch( Throwable ignored ){
}
}else{
Utils.showToast( activity, true, result.error );
}
}
};
progress.setIndeterminate( true );
progress.setCancelable( false );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override public void onCancel( DialogInterface dialog ){
task.cancel( true );
}
} );
progress.show();
task.executeOnExecutor( App1.task_executor );
}
} );
}
static final Pattern reFollowError = Pattern.compile( "follow",Pattern.CASE_INSENSITIVE );
private void addListMember(final boolean bFollow){
if( list_owner == null || list_id == - 1L ){
Utils.showToast( activity, false, R.string.list_not_selected );
return;
}
//noinspection deprecation
final ProgressDialog progress = new ProgressDialog( activity );
@SuppressLint("StaticFieldLeak") final AsyncTask< Void, String, TootApiResult > task
= new AsyncTask< Void, String, TootApiResult >() {
void showProgress( final String sv ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
progress.setMessage( sv );
}
} );
}
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( activity, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( String s ){
showProgress(s);
}
} );
TootApiResult result;
client.setAccount( list_owner );
// リストに追加したいアカウントの自タンスでのアカウントIDを取得する
String path = "/api/v1/accounts/search?q=" + target_user_full_acct;
result = client.request( path );
if( result ==null || result.array ==null ){
return result;
}
for( int i = 0, ie = result.array.length() ; i < ie ; ++ i ){
TootAccount item = TootAccount.parse( activity, list_owner, result.array.optJSONObject( i ) );
if( list_owner.getFullAcct( item ).equals( target_user_full_acct ) ){
local_who = item;
break;
}
}
if( local_who == null ){
return new TootApiResult( activity.getString( R.string.account_sync_failed ) );
}
if( bFollow ){
TootRelationShip relation;
if( list_owner.isLocalUser( local_who ) ){
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "" // 空データ
) );
result = client.request( "/api/v1/accounts/"+local_who.id+"/follow", request_builder );
}else{
// リモートフォローする
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "uri=" + Uri.encode( local_who.acct )
) );
result = client.request( "/api/v1/follows", request_builder );
if( result == null || result.object == null ) return result;
TootAccount a = TootAccount.parse( activity, list_owner, result.object );
if( a == null ){
return new TootApiResult( "parse error." );
}
result = client.request( "/api/v1/accounts/relationships?id[]=" + a.id );
}
if( result == null || result.array == null ) return result;
TootRelationShip.List relation_list = TootRelationShip.parseList( result.array );
relation = relation_list.isEmpty() ? null : relation_list.get(0);
if( relation == null ){
return new TootApiResult( "parse error.");
}else if( ! relation.following ){
if( relation.requested ){
return new TootApiResult( activity.getString( R.string.cant_add_list_follow_requesting ) );
}else{
// リモートフォローの場合正常ケースでもここを通る場合がある
// 何もしてはいけない
}
}
}
JSONObject content = new JSONObject();
try{
JSONArray account_ids = new JSONArray();
account_ids.put( Long.toString( local_who.id ) );
content.put( "account_ids", account_ids );
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, "can't encoding json parameter." ) );
}
Request.Builder request_builder = new Request.Builder().post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_JSON
, content.toString()
) );
return client.request( "/api/v1/lists/" + list_id + "/accounts", request_builder );
}
TootAccount local_who;
@Override
protected void onCancelled( TootApiResult result ){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
try{
progress.dismiss();
}catch( Throwable ignored ){
}
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( result.object != null ){
for( Column column : activity.app_state.column_list ){
column.onListMemberUpdated( list_owner, list_id, local_who, true );
}
Utils.showToast( activity, false, R.string.list_member_added );
try{
dialog.dismiss();
}catch( Throwable ignored ){
}
}else{
if( result.response != null
&& result.response.code() == 422
&& result.error != null && reFollowError.matcher( result.error ).find()
){
if( !bFollow ){
DlgConfirm.openSimple(
activity
, activity.getString( R.string.list_retry_with_follow, target_user_full_acct )
, new Runnable() {
@Override public void run(){
addListMember( true );
}
}
);
}else{
new AlertDialog.Builder( activity )
.setCancelable( true )
.setMessage( R.string.cant_add_list_while_follow_progress )
.setNeutralButton( R.string.close,null )
.show();
}
return;
}
Utils.showToast( activity, true, result.error );
}
}
};
progress.setIndeterminate( true );
progress.setCancelable( false );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override public void onCancel( DialogInterface dialog ){
task.cancel( true );
}
} );
progress.show();
task.executeOnExecutor( App1.task_executor );
}
}

View File

@ -862,7 +862,7 @@ public class Utils {
}catch( Throwable ex ){
log.trace( ex );
}
return sb.toString();
return sb.toString().replaceAll( "\n+","\n" );
}
public interface ScanViewCallback {

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

View File

@ -500,6 +500,38 @@
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnListMemberAdd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/btn_bg_transparent"
android:drawablePadding="4dp"
android:gravity="start|center_vertical"
android:minHeight="32dp"
android:paddingBottom="4dp"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:text="@string/list_member_add"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnListMemberRemove"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/btn_bg_transparent"
android:drawablePadding="4dp"
android:gravity="start|center_vertical"
android:minHeight="32dp"
android:paddingBottom="4dp"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:text="@string/list_member_remove"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnSendMessage"
android:layout_width="match_parent"

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:layout_marginEnd="12dp"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:orientation="vertical"
>
<TextView
style="@style/setting_row_label"
android:text="@string/member_will_be_added_to_list"
/>
<LinearLayout
style="@style/setting_row_form"
android:gravity="center_vertical"
>
<jp.juggler.subwaytooter.view.MyNetworkImageView
android:id="@+id/ivUser"
android:layout_width="48dp"
android:layout_height="40dp"
android:layout_marginEnd="4dp"
android:contentDescription="@string/thumbnail"
android:scaleType="fitEnd"
/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
>
<TextView
android:id="@+id/tvUserName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Follower Name"
/>
<TextView
android:id="@+id/tvUserAcct"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="4dp"
android:paddingStart="4dp"
android:textColor="?attr/colorTimeSmall"
android:textSize="12sp"
tools:text="aaaaaaaaaaaaaaaa"
/>
</LinearLayout>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/list_owner"
/>
<LinearLayout
style="@style/setting_row_form"
android:gravity="center_vertical"
>
<Button
android:id="@+id/btnListOwner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/list"
/>
<LinearLayout
style="@style/setting_row_form"
android:gravity="center_vertical"
>
<Button
android:id="@+id/btnList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
/>
</LinearLayout>
</LinearLayout>
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:id="@+id/btnCancel"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/cancel"
/>
<Button
android:id="@+id/btnOk"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/ok"
/>
</LinearLayout>
</LinearLayout>

View File

@ -493,4 +493,31 @@
android:textAllCaps="false"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/llList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:id="@+id/btnListTL"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_bg_transparent"
android:textAllCaps="false"
/>
<ImageButton
android:id="@+id/btnListMore"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:background="@drawable/btn_bg_transparent"
android:src="?attr/btn_more"
android:contentDescription="@string/more"
/>
</LinearLayout>
</LinearLayout>

View File

@ -391,4 +391,34 @@
android:textAllCaps="false"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/llList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="3dp"
android:layout_marginBottom="3dp"
>
<Button
android:id="@+id/btnListTL"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_bg_transparent"
android:textAllCaps="false"
/>
<ImageButton
android:id="@+id/btnListMore"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:layout_gravity="center_vertical"
android:background="@drawable/btn_bg_transparent"
android:src="?attr/btn_more"
android:contentDescription="@string/more"
/>
</LinearLayout>
</LinearLayout>

View File

@ -303,6 +303,42 @@
</RelativeLayout>
<RelativeLayout
android:id="@+id/llListList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSearchFormBackground"
android:paddingBottom="3dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
android:paddingTop="3dp"
>
<ImageButton
android:id="@+id/btnListAdd"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/search"
android:src="?attr/ic_add"
/>
<EditText
android:id="@+id/etListName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/btnListAdd"
android:imeOptions="actionSend"
android:inputType="text"
android:hint="@string/list_create_hint"
tools:ignore="LabelFor"
/>
</RelativeLayout>
<FrameLayout
android:id="@+id/flColumnBackground"
android:layout_width="match_parent"

View File

@ -36,11 +36,18 @@
android:id="@+id/nav_add_tl_local"
android:icon="?attr/btn_local_tl"
android:title="@string/local_timeline"/>
<item
android:id="@+id/nav_add_tl_federate"
android:icon="?attr/btn_federate_tl"
android:title="@string/federate_timeline"/>
<item
android:id="@+id/nav_add_list"
android:icon="?attr/ic_list2"
android:title="@string/lists"/>
<item
android:id="@+id/nav_add_tl_search"
android:icon="?attr/ic_search"

View File

@ -529,6 +529,35 @@
<string name="hide_boost_in_home">Hide boosts from this user to your home</string>
<string name="show_boost_in_home">Show boosts from this user to your home</string>
<string name="operation_succeeded">Operation succeeded.</string>
<string name="list_member_add">Add user to List…</string>
<string name="list_member_remove">Remove user to List…</string>
<string name="lists">Lists</string>
<string name="list_show_member">show list users</string>
<string name="list_show_timeline">show list TL</string>
<string name="list_member_of">\"%1$s\" list users</string>
<string name="list_tl_of">\"%1$s\" List TL</string>
<string name="list_create_hint">name of new list</string>
<string name="list_name_empty">please input name of new list.</string>
<string name="list_created">new list created.</string>
<string name="list_delete_confirm">\"%1$s\" list will be deleted. Are you sure?</string>
<string name="list_member">List users</string>
<string name="list_timeline">List timeline</string>
<string name="list_member_delete_confirm">\"%1$s\" will be deleted from \"%2$s\". Are you sure?</string>
<string name="member_will_be_added_to_list">The user will be added to list</string>
<string name="list_owner">List owner</string>
<string name="list_create">(Create new list…)</string>
<string name="list">List</string>
<string name="list_not_supported">this instance does not support list.</string>
<string name="cant_access_list">Can\'t access to lists.</string>
<string name="list_not_selected">please select list.</string>
<string name="account_sync_failed">account sync failed.</string>
<string name="list_retry_with_follow">You have not followed %1$s yet. Would you like to follow and then add to list?</string>
<string name="cant_add_list_while_follow_progress">Can\'t add user to list:\nfollow operation is progress on server side.</string>
<string name="cant_add_list_follow_requesting">Can\'t add user to list:\nPlease wait until the follow request is approved.</string>
<string name="cant_add_list_follow_failed">Can\'t add user to list:\nfollow operation failed.</string>
<string name="list_member_added">User has been added to the list.</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<!--<string name="abc_action_bar_home_description_format">%1$s, %2$s</string>-->
<!--<string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string>-->

View File

@ -816,4 +816,32 @@
<string name="hide_boost_in_home">このユーザのブーストをホームに表示しない</string>
<string name="show_boost_in_home">このユーザのブーストをホームに表示する</string>
<string name="operation_succeeded">変更できました</string>
<string name="list_member_add">リストに追加…</string>
<string name="list_member_remove">リストから削除…</string>
<string name="lists">リストの一覧</string>
<string name="list_show_member">リストのユーザ</string>
<string name="list_show_timeline">リストのタイムライン</string>
<string name="list_member_of">\"%1$s\"リストのユーザ</string>
<string name="list_tl_of">\"%1$s\"リストのタイムライン</string>
<string name="list_create_hint">新しいリストの名前</string>
<string name="list_name_empty">新しいリストの名前を入力してください</string>
<string name="list_created">リストを作成しました</string>
<string name="list_delete_confirm">\"%1$s\"リストは削除されます。よろしいですか?</string>
<string name="list_member">リストのユーザ</string>
<string name="list_timeline">リストのタイムライン</string>
<string name="list_member_delete_confirm">\"%1$s\"は\"%2$s\"から削除されます。よろしいですか?</string>
<string name="member_will_be_added_to_list">リストに追加されるユーザ</string>
<string name="list_owner">リスト所有者</string>
<string name="list_create">(リストを作る…)</string>
<string name="list">リスト</string>
<string name="list_not_supported">このタンスはリスト機能をサポートしていません</string>
<string name="cant_access_list">リストにアクセスできません</string>
<string name="list_not_selected">リストを選択してください</string>
<string name="account_sync_failed">アカウントを同期できません</string>
<string name="list_retry_with_follow">あなたは %1$s をまだフォローしていません。フォローしてからリストに追加しますか?</string>
<string name="cant_add_list_while_follow_progress">リストに追加できません:\nフォロー処理がサーバで進行中です</string>
<string name="cant_add_list_follow_requesting">リストに追加できません:\nフォローリクエストが承認されるのを待ってください</string>
<string name="cant_add_list_follow_failed">リストに追加できません:\nフォロー操作に失敗しました</string>
<string name="list_member_added">追加できました</string>
</resources>

View File

@ -121,5 +121,7 @@
<attr name="ic_eye_off" format="reference" />
<attr name="ic_pin" format="reference" />
<attr name="ic_follow_wait" format="reference" />
<attr name="ic_list2" format="reference" />
<attr name="ic_add" format="reference" />
</resources>

View File

@ -521,4 +521,31 @@
<string name="show_boost_in_home">Show boosts from this user to your home</string>
<string name="operation_succeeded">Operation succeeded.</string>
<string name="list_member_add">Add user to List…</string>
<string name="list_member_remove">Remove user to List…</string>
<string name="lists">Lists</string>
<string name="list_show_member">show list users</string>
<string name="list_show_timeline">show list TL</string>
<string name="list_member_of">\"%1$s\" list users</string>
<string name="list_tl_of">\"%1$s\" List TL</string>
<string name="list_create_hint">name of new list</string>
<string name="list_name_empty">please input name of new list.</string>
<string name="list_created">new list created.</string>
<string name="list_delete_confirm">\"%1$s\" list will be deleted. Are you sure?</string>
<string name="list_member">List users</string>
<string name="list_timeline">List timeline</string>
<string name="list_member_delete_confirm">\"%1$s\" will be deleted from \"%2$s\". Are you sure?</string>
<string name="member_will_be_added_to_list">The user will be added to list</string>
<string name="list_owner">List owner</string>
<string name="list_create">(Create new list…)</string>
<string name="list">List</string>
<string name="list_not_supported">this instance does not support list.</string>
<string name="cant_access_list">Can\'t access to lists</string>
<string name="list_not_selected">please select list.</string>
<string name="account_sync_failed">account sync failed.</string>
<string name="list_retry_with_follow">You have not followed %1$s yet. Would you like to follow and then add to list?</string>
<string name="cant_add_list_while_follow_progress">Can\'t add user to list:\nfollow operation is progress on server side.</string>
<string name="cant_add_list_follow_requesting">Can\'t add user to list:\nPlease wait until the follow request is approved.</string>
<string name="cant_add_list_follow_failed">Can\'t add user to list:\nfollow operation failed.</string>
<string name="list_member_added">User has been added to the list.</string>
</resources>

View File

@ -91,6 +91,8 @@
<item name="ic_eye_off">@drawable/ic_eye_off</item>
<item name="ic_pin">@drawable/ic_pin</item>
<item name="ic_follow_wait">@drawable/ic_follow_wait</item>
<item name="ic_list2">@drawable/ic_list2</item>
<item name="ic_add">@drawable/ic_add</item>
</style>
@ -186,6 +188,8 @@
<item name="ic_eye_off">@drawable/ic_eye_off_dark</item>
<item name="ic_pin">@drawable/ic_pin_dark</item>
<item name="ic_follow_wait">@drawable/ic_follow_wait_dark</item>
<item name="ic_list2">@drawable/ic_list2_dark</item>
<item name="ic_add">@drawable/ic_add_dark</item>
</style>