This commit is contained in:
tateisu 2017-05-08 10:16:43 +09:00
parent 23c5c9d572
commit 492ec81fb4
26 changed files with 842 additions and 114 deletions

View File

@ -9,8 +9,8 @@ android {
applicationId "jp.juggler.subwaytooter"
minSdkVersion 21
targetSdkVersion 25
versionCode 40
versionName "0.4.0"
versionCode 41
versionName "0.4.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

View File

@ -131,7 +131,10 @@
android:name=".ActMutedApp"
android:label="@string/muted_app"
/>
<activity
android:name=".ActMutedWord"
android:label="@string/muted_word"
/>
<activity
android:name=".ActColumnCustomize"
android:label="@string/color_and_background"
@ -142,7 +145,11 @@
android:label="@string/nickname_and_color"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
/>
<activity
android:name=".ActText"
android:label="@string/select_and_copy"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
/>
<meta-data
android:name="android.max_aspect"
android:value="ratio_float"/>

View File

@ -12,6 +12,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.text.BidiFormatter;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
@ -454,6 +455,10 @@ public class ActMain extends AppCompatActivity
}else if( id == R.id.nav_muted_app ){
startActivity( new Intent( this, ActMutedApp.class ) );
}else if( id == R.id.nav_muted_word ){
startActivity( new Intent( this, ActMutedWord.class ) );
// Handle the camera action
// }else if( id == R.id.nav_gallery ){
//
@ -1630,8 +1635,14 @@ public class ActMain extends AppCompatActivity
);
return;
}else if( bFollow ){
BidiFormatter bidiFormatter = BidiFormatter.getInstance();
String msg = getString( R.string.confirm_follow_who_from
, bidiFormatter.unicodeWrap(who.display_name )
, AcctColor.getNickname( access_info.acct )
);
DlgConfirm.open( this
, getString( R.string.confirm_follow_who_from, who.display_name, AcctColor.getNickname( access_info.acct ) )
, msg
, new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return access_info.confirm_follow;
@ -2212,47 +2223,6 @@ public class ActMain extends AppCompatActivity
////////////////////////////////////////////////
void sendStatus( SavedAccount access_info, TootStatus status ){
try{
StringBuilder sb = new StringBuilder();
sb.append( getString( R.string.send_header_url ) );
sb.append( ": " );
sb.append( status.url );
sb.append( "\n" );
sb.append( getString( R.string.send_header_date ) );
sb.append( ": " );
sb.append( TootStatus.formatTime( status.time_created_at ) );
sb.append( "\n" );
sb.append( getString( R.string.send_header_from_acct ) );
sb.append( ": " );
sb.append( access_info.getFullAcct( status.account ) );
sb.append( "\n" );
sb.append( getString( R.string.send_header_from_name ) );
sb.append( ": " );
sb.append( status.account.display_name );
sb.append( "\n" );
if( ! TextUtils.isEmpty( status.spoiler_text ) ){
sb.append( getString( R.string.send_header_content_warning ) );
sb.append( ": " );
sb.append( HTMLDecoder.decodeHTMLForClipboard( access_info, status.spoiler_text ) );
sb.append( "\n" );
}
sb.append( "\n" );
sb.append( HTMLDecoder.decodeHTMLForClipboard( access_info, status.content ) );
Intent intent = new Intent();
intent.setAction( Intent.ACTION_SEND );
intent.setType( "text/plain" );
intent.putExtra( Intent.EXTRA_TEXT, sb.toString() );
startActivity( intent );
}catch( Throwable ex ){
log.e( ex, "sendStatus failed." );
ex.printStackTrace();
Utils.showToast( this, ex, "sendStatus failed." );
}
}
////////////////////////////////////////////////
final RelationChangedCallback follow_complete_callback = new RelationChangedCallback() {

View File

@ -156,6 +156,7 @@ public class ActMutedApp extends AppCompatActivity {
}
void bind( MyItem item ){
itemView.setTag( item ); // itemView は親クラスのメンバ変数
tvName.setText( item.name );
}

View File

@ -0,0 +1,217 @@
package jp.juggler.subwaytooter;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.woxthebox.draglistview.DragItem;
import com.woxthebox.draglistview.DragItemAdapter;
import com.woxthebox.draglistview.DragListView;
import com.woxthebox.draglistview.swipe.ListSwipeHelper;
import com.woxthebox.draglistview.swipe.ListSwipeItem;
import java.util.ArrayList;
import jp.juggler.subwaytooter.table.MutedWord;
public class ActMutedWord extends AppCompatActivity {
DragListView listView;
MyListAdapter listAdapter;
@Override
protected void onCreate( @Nullable Bundle savedInstanceState ){
super.onCreate( savedInstanceState );
App1.setActivityTheme(this,false);
initUI();
loadData();
}
@Override public void onBackPressed(){
setResult( RESULT_OK );
super.onBackPressed();
}
private void initUI(){
setContentView( R.layout.act_mute_app );
// リストのアダプター
listAdapter = new MyListAdapter();
// ハンドル部分をドラッグで並べ替えできるRecyclerView
listView = (DragListView) findViewById( R.id.drag_list_view );
listView.setLayoutManager( new LinearLayoutManager( this ) );
listView.setAdapter( listAdapter, false );
listView.setCanDragHorizontally( true );
listView.setDragEnabled( false );
listView.setCustomDragItem( new MyDragItem( this, R.layout.lv_mute_app ) );
listView.getRecyclerView().setVerticalScrollBarEnabled( true );
// listView.setDragListListener( new DragListView.DragListListenerAdapter() {
// @Override
// public void onItemDragStarted( int position ){
// // 操作中はリフレッシュ禁止
// // mRefreshLayout.setEnabled( false );
// }
//
// @Override
// public void onItemDragEnded( int fromPosition, int toPosition ){
// // 操作完了でリフレッシュ許可
// // mRefreshLayout.setEnabled( USE_SWIPE_REFRESH );
//
//// if( fromPosition != toPosition ){
//// // 並べ替えが発生した
//// }
// }
// } );
// リストを左右スワイプした
listView.setSwipeListener( new ListSwipeHelper.OnSwipeListenerAdapter() {
@Override
public void onItemSwipeStarted( ListSwipeItem item ){
// 操作中はリフレッシュ禁止
// mRefreshLayout.setEnabled( false );
}
@Override
public void onItemSwipeEnded( ListSwipeItem item, ListSwipeItem.SwipeDirection swipedDirection ){
// 操作完了でリフレッシュ許可
// mRefreshLayout.setEnabled( USE_SWIPE_REFRESH );
// 左にスワイプした(右端に青が見えた) なら要素を削除する
if( swipedDirection == ListSwipeItem.SwipeDirection.LEFT ){
Object o = item.getTag();
if( o instanceof MyItem){
MyItem adapterItem = ( MyItem ) o;
MutedWord.delete( adapterItem.name );
listAdapter.removeItem( listAdapter.getPositionForItem( adapterItem ) );
}
}
}
} );
}
private void loadData(){
ArrayList< MyItem > tmp_list = new ArrayList<>();
try{
Cursor cursor = MutedWord.createCursor();
if( cursor != null ){
try{
int idx_name = cursor.getColumnIndex( MutedWord.COL_NAME );
while( cursor.moveToNext() ){
String name = cursor.getString( idx_name);
MyItem item = new MyItem( name );
tmp_list.add( item );
}
}finally{
cursor.close();
}
}
}catch( Throwable ex ){
ex.printStackTrace();
}
listAdapter.setItemList( tmp_list );
}
// リスト要素のデータ
static class MyItem {
String name;
MyItem(String name ){
this.name = name;
}
}
// リスト要素のViewHolder
static class MyViewHolder extends DragItemAdapter.ViewHolder {
final TextView tvName;
MyViewHolder( final View viewRoot ){
super( viewRoot
, R.id.ivDragHandle // View ID ここを押すとドラッグ操作をすぐに開始する
, false // 長押しでドラッグ開始するなら真
);
tvName = (TextView) viewRoot.findViewById( R.id.tvName );
// リスト要素のビューが ListSwipeItem だった場合Swipe操作を制御できる
if( viewRoot instanceof ListSwipeItem ){
ListSwipeItem lsi = (ListSwipeItem) viewRoot;
lsi.setSwipeInStyle( ListSwipeItem.SwipeInStyle.SLIDE );
lsi.setSupportedSwipeDirection( ListSwipeItem.SwipeDirection.LEFT );
}
}
void bind( MyItem item ){
itemView.setTag( item ); // itemView は親クラスのメンバ変数
tvName.setText( item.name );
}
// @Override
// public boolean onItemLongClicked( View view ){
// return false;
// }
// @Override
// public void onItemClicked( View view ){
// }
}
// ドラッグ操作中のデータ
private class MyDragItem extends DragItem {
MyDragItem( Context context, int layoutId ){
super( context, layoutId );
}
@Override
public void onBindDragView( View clickedView, View dragView ){
((TextView)dragView.findViewById( R.id.tvName )).setText(
((TextView)clickedView.findViewById( R.id.tvName )).getText()
);
dragView.findViewById(R.id.item_layout).setBackgroundColor(
Styler.getAttributeColor( ActMutedWord.this, R.attr.list_item_bg_pressed_dragged)
);
}
}
private class MyListAdapter extends DragItemAdapter< MyItem, MyViewHolder > {
MyListAdapter(){
super();
setHasStableIds( true );
setItemList( new ArrayList< MyItem >() );
}
@Override
public MyViewHolder onCreateViewHolder( ViewGroup parent, int viewType ){
View view = getLayoutInflater().inflate( R.layout.lv_mute_app, parent, false );
return new MyViewHolder( view );
}
@Override
public void onBindViewHolder( MyViewHolder holder, int position ){
super.onBindViewHolder( holder, position );
holder.bind( getItemList().get( position ) );
}
@Override
public long getItemId( int position ){
MyItem item = mItemList.get( position ); // mItemList は親クラスのメンバ変数
return item.name.hashCode();
}
}
}

View File

@ -48,6 +48,8 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
@ -158,7 +160,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
break;
case R.id.btnPost:
performPost( false );
performPost( false,false );
break;
case R.id.btnRemoveReply:
@ -867,7 +869,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
// 設定からリサイズ指定を読む
int resize_to = list_resize_max[ pref.getInt( Pref.KEY_RESIZE_IMAGE, 4 ) ];
Bitmap bitmap = Utils.createResizedBitmap( log, this, uri, true,resize_to );
Bitmap bitmap = Utils.createResizedBitmap( log, this, uri, true, resize_to );
if( bitmap != null ){
try{
File cache_dir = getExternalCacheDir();
@ -1251,7 +1253,18 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
///////////////////////////////////////////////////////////////////////////////////////
// post
private void performPost( boolean bConfirm ){
// [:word:] 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation)
// [:alpha:] 英字 (Letter | Mark)
static final String word = "[_\\p{L}\\p{M}\\p{Nd}\\p{Pc}]";
static final String alpha = "[_\\p{L}\\p{M}]";
static final Pattern reTag = Pattern.compile(
"(?:^|[^/)\\w])#(" + word + "*" + alpha + word + "*)"
, Pattern.CASE_INSENSITIVE
);
private void performPost( final boolean bConfirmTag, final boolean bConfirmAccount ){
final String content = etContent.getText().toString().trim();
if( TextUtils.isEmpty( content ) ){
Utils.showToast( this, true, R.string.post_error_contents_empty );
@ -1269,7 +1282,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
}
}
if( ! bConfirm ){
if( ! bConfirmAccount ){
DlgConfirm.open( this
, getString( R.string.confirm_post_from, AcctColor.getNickname( account.acct ) )
, new DlgConfirm.Callback() {
@ -1283,12 +1296,29 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
}
@Override public void onOK(){
performPost( true );
performPost( bConfirmTag, true );
}
} );
return;
}
if( ! bConfirmTag ){
Matcher m = reTag.matcher( content );
if( m.find() && ! TootStatus.VISIBILITY_PUBLIC.equals( visibility ) ){
new AlertDialog.Builder( this )
.setCancelable( true )
.setMessage( R.string.hashtag_and_visibility_not_match )
.setNegativeButton( R.string.cancel, null )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override public void onClick( DialogInterface dialog, int which ){
performPost( true, bConfirmAccount );
}
} )
.show();
return;
}
}
final StringBuilder sb = new StringBuilder();
sb.append( "status=" );

View File

@ -0,0 +1,180 @@
package jp.juggler.subwaytooter;
import android.app.SearchManager;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.table.MutedWord;
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 ActText extends AppCompatActivity implements View.OnClickListener {
static final LogCategory log = new LogCategory( "ActText" );
static String encodeStatus( Context context, SavedAccount access_info, TootStatus status ){
StringBuilder sb = new StringBuilder();
sb.append( context.getString( R.string.send_header_url ) );
sb.append( ": " );
sb.append( status.url );
sb.append( "\n" );
sb.append( context.getString( R.string.send_header_date ) );
sb.append( ": " );
sb.append( TootStatus.formatTime( status.time_created_at ) );
sb.append( "\n" );
sb.append( context.getString( R.string.send_header_from_acct ) );
sb.append( ": " );
sb.append( access_info.getFullAcct( status.account ) );
sb.append( "\n" );
sb.append( context.getString( R.string.send_header_from_name ) );
sb.append( ": " );
sb.append( status.account.display_name );
sb.append( "\n" );
if( ! TextUtils.isEmpty( status.spoiler_text ) ){
sb.append( context.getString( R.string.send_header_content_warning ) );
sb.append( ": " );
sb.append( HTMLDecoder.decodeHTMLForClipboard( access_info, status.spoiler_text ) );
sb.append( "\n" );
}
sb.append( "\n" );
sb.append( HTMLDecoder.decodeHTMLForClipboard( access_info, status.content ) );
return sb.toString();
}
static final String EXTRA_TEXT = "text";
public static void open( ActMain activity, SavedAccount access_info, TootStatus status ){
String sv = encodeStatus( activity, access_info, status );
Intent intent = new Intent( activity, ActText.class );
intent.putExtra( EXTRA_TEXT, sv );
activity.startActivity( intent );
}
@Override protected void onCreate( @Nullable Bundle savedInstanceState ){
super.onCreate( savedInstanceState );
initUI();
if( savedInstanceState == null ){
Intent intent = getIntent();
String sv = intent.getStringExtra( EXTRA_TEXT );
etText.setText(sv);
etText.setSelection( 0,sv.length() );
}
}
EditText etText;
void initUI(){
setContentView( R.layout.act_text );
etText = (EditText) findViewById( R.id.etText );
findViewById( R.id.btnCopy ).setOnClickListener( this );
findViewById( R.id.btnSearch ).setOnClickListener( this );
findViewById( R.id.btnSend ).setOnClickListener( this );
findViewById( R.id.btnMuteWord ).setOnClickListener( this );
}
@Override public void onClick( View v ){
switch( v.getId() ){
case R.id.btnCopy:
copy();
break;
case R.id.btnSearch:
search();
break;
case R.id.btnSend:
send();
break;
case R.id.btnMuteWord:
muteWord();
break;
}
}
private String getSelection(){
int s = etText.getSelectionStart();
int e = etText.getSelectionEnd();
String text = etText.getText().toString();
if( s == e ){
return text;
}else{
return text.substring( s, e );
}
}
private void copy(){
try{
// Gets a handle to the clipboard service.
ClipboardManager clipboard = (ClipboardManager)
getSystemService( Context.CLIPBOARD_SERVICE );
// Creates a new text clip to put on the clipboard
ClipData clip = ClipData.newPlainText( "text", getSelection() );
// Set the clipboard's primary clip.
clipboard.setPrimaryClip( clip );
Utils.showToast( this,false,R.string.copy_complete );
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( this, ex, "copy failed." );
}
}
private void search(){
try{
Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.putExtra( SearchManager.QUERY, getSelection() );
if( intent.resolveActivity(getPackageManager()) != null ) {
startActivity(intent);
}
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( this, ex, "search failed." );
}
}
private void send(){
try{
Intent intent = new Intent();
intent.setAction( Intent.ACTION_SEND );
intent.setType( "text/plain" );
intent.putExtra( Intent.EXTRA_TEXT, getSelection() );
startActivity( intent );
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( this, ex, "send failed." );
}
}
private void muteWord(){
try{
MutedWord.save( getSelection() );
for( Column column : App1.getAppState( this ).column_list ){
column.removeMuteApp();
}
Utils.showToast( this, false, R.string.word_was_muted );
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( this, ex, "muteWord failed." );
}
}
}

View File

@ -31,6 +31,7 @@ import jp.juggler.subwaytooter.api.entity.TootApplication;
import jp.juggler.subwaytooter.api.entity.TootNotification;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.MutedWord;
import jp.juggler.subwaytooter.table.NotificationTracking;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.LogCategory;
@ -129,13 +130,15 @@ public class AlarmService extends IntentService {
}else if( ACTION_NOTIFICATION_CLICK.equals( action ) ){
log.d( "Notification clicked!" );
long db_id = received_intent.getLongExtra( EXTRA_DB_ID, 0L );
NotificationTracking.updateRead( db_id );
notification_manager.cancel( Long.toString( db_id ), NOTIFICATION_ID );
//
// 画面を開く
intent = new Intent( this, ActCallback.class );
intent.setData( Uri.parse( "subwaytooter://notification_click?db_id=" + db_id ) );
intent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
startActivity( intent );
// 通知をキャンセル
notification_manager.cancel( Long.toString( db_id ), NOTIFICATION_ID );
// DB更新処理
NotificationTracking.updateRead( db_id );
return;
}
@ -156,7 +159,8 @@ public class AlarmService extends IntentService {
boolean bAlarmRequired = false;
HashSet< String > muted_app = MutedApp.getNameSet();
HashSet< String > muted_word = MutedWord.getNameSet();
for( SavedAccount account : account_list ){
try{
if( account.notification_mention
@ -168,7 +172,7 @@ public class AlarmService extends IntentService {
ArrayList< Data > data_list = new ArrayList<>();
checkAccount( client, data_list, account, muted_app );
checkAccount( client, data_list, account, muted_app ,muted_word);
showNotification( account.db_id, data_list );
@ -200,7 +204,7 @@ public class AlarmService extends IntentService {
private static final String PATH_NOTIFICATIONS = "/api/v1/notifications";
private void checkAccount( TootApiClient client, ArrayList< Data > data_list, SavedAccount account, HashSet< String > muted_app ){
private void checkAccount( TootApiClient client, ArrayList< Data > data_list, SavedAccount account, HashSet< String > muted_app, HashSet< String > muted_word ){
log.d( "checkAccount account_db_id=%s", account.db_id );
NotificationTracking nr = NotificationTracking.load( account.db_id );
@ -213,7 +217,7 @@ public class AlarmService extends IntentService {
JSONArray array = new JSONArray( nr.last_data );
for( int i = array.length() - 1 ; i >= 0 ; -- i ){
JSONObject src = array.optJSONObject( i );
update_sub( src, nr, account, dst_array, data_list, duplicate_check, muted_app );
update_sub( src, nr, account, dst_array, data_list, duplicate_check, muted_app ,muted_word);
}
}catch( JSONException ex ){
ex.printStackTrace();
@ -237,7 +241,7 @@ public class AlarmService extends IntentService {
JSONArray array = result.array;
for( int i = array.length() - 1 ; i >= 0 ; -- i ){
JSONObject src = array.optJSONObject( i );
update_sub( src, nr, account, dst_array, data_list, duplicate_check, muted_app );
update_sub( src, nr, account, dst_array, data_list, duplicate_check, muted_app, muted_word );
}
}catch( JSONException ex ){
ex.printStackTrace();
@ -277,7 +281,10 @@ public class AlarmService extends IntentService {
, ArrayList< Data > data_list
, HashSet< Long > duplicate_check
, HashSet< String > muted_app
) throws JSONException{
, HashSet< String > muted_word
)
throws JSONException
{
long id = src.optLong( "id" );
@ -306,18 +313,11 @@ public class AlarmService extends IntentService {
return;
}
// app mute
{
TootStatus status = notification.status;
if( status != null ){
TootApplication application = status.application;
if( application != null ){
String name = application.name;
if( name != null ){
if( muted_app.contains( name ) ){
return;
}
}
if( status.checkMuted( muted_app,muted_word )){
return;
}
}
}

View File

@ -35,6 +35,7 @@ import jp.juggler.subwaytooter.table.ClientInfo;
import jp.juggler.subwaytooter.table.ContentWarning;
import jp.juggler.subwaytooter.table.LogData;
import jp.juggler.subwaytooter.table.MediaShown;
import jp.juggler.subwaytooter.table.MutedWord;
import jp.juggler.subwaytooter.table.NotificationTracking;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.table.UserRelation;
@ -50,7 +51,7 @@ public class App1 extends Application {
static final LogCategory log = new LogCategory( "App1" );
static final String DB_NAME = "app_db";
static final int DB_VERSION = 10;
static final int DB_VERSION = 11;
// 2017/4/25 v10 1=>2 SavedAccount に通知設定を追加
// 2017/4/25 v10 1=>2 NotificationTracking テーブルを追加
// 2017/4/29 v20 2=>5 MediaShown,ContentWarningのインデクスが間違っていたので貼り直す
@ -59,6 +60,7 @@ public class App1 extends Application {
// 2017/5/02 v32 7=>8 (この変更は取り消された)
// 2017/5/02 v32 8=>9 AcctColor テーブルの追加
// 2017/5/04 v33 9=>10 SavedAccountに項目追加
// 2017/5/08 v41 10=>11 MutedWord テーブルの追加
static DBOpenHelper db_open_helper;
@ -101,6 +103,7 @@ public class App1 extends Application {
UserRelation.onDBCreate( db );
AcctSet.onDBCreate( db );
AcctColor.onDBCreate( db );
MutedWord.onDBCreate( db );
}
@Override
@ -116,6 +119,7 @@ public class App1 extends Application {
UserRelation.onDBUpgrade( db, oldVersion, newVersion );
AcctSet.onDBUpgrade( db, oldVersion, newVersion );
AcctColor.onDBUpgrade( db, oldVersion, newVersion );
MutedWord.onDBUpgrade( db, oldVersion, newVersion );
}
}

View File

@ -22,7 +22,6 @@ import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootApplication;
import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.api.entity.TootContext;
import jp.juggler.subwaytooter.api.entity.TootGap;
@ -34,6 +33,7 @@ import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.AcctSet;
import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.MutedWord;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.table.UserRelation;
import jp.juggler.subwaytooter.util.LogCategory;
@ -93,11 +93,11 @@ class Column {
private static final String KEY_DONT_SHOW_REPLY = "dont_show_reply";
private static final String KEY_REGEX_TEXT = "regex_text";
static final String KEY_HEADER_BACKGROUND_COLOR = "header_background_color";
static final String KEY_HEADER_TEXT_COLOR = "header_text_color";
static final String KEY_COLUMN_BACKGROUND_COLOR = "column_background_color";
static final String KEY_COLUMN_BACKGROUND_IMAGE = "column_background_image";
static final String KEY_COLUMN_BACKGROUND_IMAGE_ALPHA = "column_background_image_alpha";
private static final String KEY_HEADER_BACKGROUND_COLOR = "header_background_color";
private static final String KEY_HEADER_TEXT_COLOR = "header_text_color";
private static final String KEY_COLUMN_BACKGROUND_COLOR = "column_background_color";
private static final String KEY_COLUMN_BACKGROUND_IMAGE = "column_background_image";
private static final String KEY_COLUMN_BACKGROUND_IMAGE_ALPHA = "column_background_image_alpha";
private static final String KEY_PROFILE_ID = "profile_id";
private static final String KEY_PROFILE_TAB = "tab";
@ -726,17 +726,14 @@ class Column {
ArrayList< Object > tmp_list = new ArrayList<>( list_data.size() );
HashSet< String > muted_app = MutedApp.getNameSet();
HashSet< String > muted_word = MutedWord.getNameSet();
for( Object o : list_data ){
if( o instanceof TootStatus ){
TootStatus item = (TootStatus) o;
TootApplication application = item.application;
if( application != null ){
String name = application.name;
if( name != null && muted_app.contains( name ) ){
log.d( "removeMuteApp: mute app %s", name );
continue;
}
if( item.checkMuted( muted_app,muted_word )){
continue;
}
}
if( o instanceof TootNotification ){
@ -744,9 +741,8 @@ class Column {
TootStatus status = item.status;
if( status != null ){
if( status.application != null ){
String sv = status.application.name;
if( sv != null && muted_app.contains( sv ) ) continue;
if( status.checkMuted( muted_app,muted_word )){
continue;
}
}
}
@ -772,7 +768,8 @@ class Column {
}
HashSet< String > muted_app = MutedApp.getNameSet();
HashSet< String > muted_word = MutedWord.getNameSet();
for( TootStatus status : src ){
if( with_attachment ){
if( ! hasMedia( status ) && ! hasMedia( status.reblog ) ) continue;
@ -799,12 +796,8 @@ class Column {
}
}
if( status.application != null ){
String sv = status.application.name;
if( sv != null && muted_app.contains( sv ) ){
log.d( "addWithFilter: mute app %s", sv );
continue;
}
if( status.checkMuted( muted_app,muted_word )){
continue;
}
dst.add( status );
@ -815,19 +808,18 @@ class Column {
private void addWithFilter( ArrayList< Object > dst, TootNotification.List src ){
HashSet< String > muted_app = MutedApp.getNameSet();
HashSet< String > muted_word = MutedWord.getNameSet();
for( TootNotification item : src ){
TootStatus status = item.status;
if( status != null ){
if( status.application != null ){
String sv = status.application.name;
if( sv != null && muted_app.contains( sv ) ){
log.d( "addWithFilter: mute app %s", sv );
continue;
}
if( status.checkMuted( muted_app,muted_word ) ){
log.d( "addWithFilter: status muted.");
continue;
}
}
dst.add( item );
@ -2243,6 +2235,7 @@ class Column {
fireShowContent();
if( holder != null ){
//noinspection StatementWithEmptyBody
if(restore_idx >= 0 ){
setItemTop( restore_idx + added - 1, restore_y );
}else{

View File

@ -61,7 +61,7 @@ class DlgContextMenu implements View.OnClickListener {
View llStatus = viewRoot.findViewById( R.id.llStatus );
View btnStatusWebPage = viewRoot.findViewById( R.id.btnStatusWebPage );
View btnStatusSendApp = viewRoot.findViewById( R.id.btnStatusSendApp );
View btnText = viewRoot.findViewById( R.id.btnText );
View btnFavouriteAnotherAccount = viewRoot.findViewById( R.id.btnFavouriteAnotherAccount );
View btnBoostAnotherAccount = viewRoot.findViewById( R.id.btnBoostAnotherAccount );
View btnDelete = viewRoot.findViewById( R.id.btnDelete );
@ -98,7 +98,7 @@ class DlgContextMenu implements View.OnClickListener {
boolean status_by_me = access_info.isMe( status.account );
btnStatusWebPage.setOnClickListener( this );
btnStatusSendApp.setOnClickListener( this );
btnText.setOnClickListener( this );
if( account_list_non_pseudo_same_instance.isEmpty() ){
btnFavouriteAnotherAccount.setVisibility( View.GONE );
@ -237,9 +237,9 @@ class DlgContextMenu implements View.OnClickListener {
}
break;
case R.id.btnStatusSendApp:
case R.id.btnText:
if( status != null ){
activity.sendStatus( access_info, status );
ActText.open( activity,access_info,status);
}
break;

View File

@ -97,7 +97,7 @@ public class TootAccount {
if( TextUtils.isEmpty( sv ) ){
dst.display_name = dst.username;
}else{
dst.display_name = Emojione.decodeEmoji( HTMLDecoder.decodeEntity(sv ) );
dst.display_name = filterDisplayName(sv);
}
dst.locked = src.optBoolean( "locked" );
@ -123,6 +123,7 @@ public class TootAccount {
}
}
public static TootAccount parse( LogCategory log, LinkClickContext account,JSONObject src ){
return parse( log, account, src, new TootAccount() );
}
@ -142,4 +143,16 @@ public class TootAccount {
return result;
}
private static CharSequence filterDisplayName( String sv ){
// decode HTML entity
sv = HTMLDecoder.decodeEntity(sv );
// sanitize LRO,RLO
sv = Utils.sanitizeBDI( sv);
// decode emoji code
return Emojione.decodeEmoji( sv ) ;
}
}

View File

@ -11,6 +11,7 @@ import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
@ -24,7 +25,6 @@ import jp.juggler.subwaytooter.util.Utils;
public class TootStatus extends TootId {
public static class List extends ArrayList< TootStatus > {
public List(){
@ -230,4 +230,38 @@ public class TootStatus extends TootId {
}
}
public boolean checkMuted( HashSet< String > muted_app, HashSet< String > muted_word ){
// app mute
if( application != null ){
String name = application.name;
if( name != null ){
if( muted_app.contains( name ) ){
return true;
}
}
}
// word mute
for( String word: muted_word ){
if( decoded_content != null && decoded_content.toString().contains( word ) ){
return true;
}
if( decoded_spoiler_text != null && decoded_spoiler_text.toString().contains( word ) ){
return true;
}
}
// reblog
//noinspection RedundantIfStatement
if( reblog != null && reblog.checkMuted( muted_app,muted_word ) ){
return true;
}
return false;
}
}

View File

@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -142,7 +143,7 @@ public class AcctColor {
@NonNull public static String getNickname( @NonNull String acct ){
AcctColor ac = load( acct );
return ac != null && ! TextUtils.isEmpty( ac.nickname ) ? ac.nickname : acct;
return ac != null && ! TextUtils.isEmpty( ac.nickname ) ? Utils.sanitizeBDI( ac.nickname ) : acct;
}
public static boolean hasNickname( @Nullable AcctColor ac ){

View File

@ -0,0 +1,114 @@
package jp.juggler.subwaytooter.table;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.util.HashSet;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.util.LogCategory;
public class MutedWord {
private static final LogCategory log = new LogCategory( "MutedWord" );
private static final String table = "word_mute";
public static final String COL_NAME = "name";
private static final String COL_TIME_SAVE = "time_save";
public static void onDBCreate( SQLiteDatabase db ){
log.d("onDBCreate!");
db.execSQL(
"create table if not exists " + table
+ "(_id INTEGER PRIMARY KEY"
+ ",name text not null"
+ ",time_save integer not null"
+ ")"
);
db.execSQL(
"create unique index if not exists " + table + "_name on " + table + "(name)"
);
}
public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){
if(oldVersion < 11 && newVersion >= 11){
onDBCreate( db );
}
}
public static void save( String word ){
if( word == null ) return;
try{
long now = System.currentTimeMillis();
ContentValues cv = new ContentValues();
cv.put( COL_NAME, word );
cv.put( COL_TIME_SAVE, now );
App1.getDB().replace( table, null, cv );
}catch( Throwable ex ){
log.e( ex, "save failed." );
}
}
public static Cursor createCursor(){
return App1.getDB().query( table, null,null,null, null, null, COL_NAME+" asc" );
}
public static void delete( String name ){
try{
App1.getDB().delete( table, COL_NAME+"=?",new String[]{ name });
}catch( Throwable ex ){
log.e( ex, "delete failed." );
}
}
public static HashSet<String> getNameSet(){
HashSet<String> dst = new HashSet<>();
try{
Cursor cursor = App1.getDB().query( table, null,null,null, null, null, null);
if( cursor != null ){
try{
int idx_name = cursor.getColumnIndex( COL_NAME );
while(cursor.moveToNext()){
String s = cursor.getString(idx_name);
dst.add( s);
}
}finally{
cursor.close();
}
}
}catch(Throwable ex){
ex.printStackTrace( );
}
return dst;
}
// private static final String[] isMuted_projection = new String[]{COL_NAME};
// private static final String isMuted_where = COL_NAME+"=?";
// private static final ThreadLocal<String[]> isMuted_where_arg = new ThreadLocal<String[]>() {
// @Override protected String[] initialValue() {
// return new String[1];
// }
// };
// public static boolean isMuted( String app_name ){
// if( app_name == null ) return false;
// try{
// String[] where_arg = isMuted_where_arg.get();
// where_arg[0] = app_name;
// Cursor cursor = App1.getDB().query( table, isMuted_projection,isMuted_where , where_arg, null, null, null );
// try{
// if( cursor.moveToFirst() ){
// return true;
// }
// }finally{
// cursor.close();
// }
// }catch( Throwable ex ){
// log.e( ex, "load failed." );
// }
// return false;
// }
}

View File

@ -74,8 +74,6 @@ public abstract class Emojione
public static CharSequence decodeEmoji( String s ){
DecodeEnv decode_env = new DecodeEnv();
Matcher matcher = SHORTNAME_PATTERN.matcher(s);
int last_end = 0;
@ -101,6 +99,7 @@ public abstract class Emojione
}
// close span
decode_env.closeSpan();
return decode_env.sb;
}
}

View File

@ -16,6 +16,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@ -1040,4 +1041,51 @@ public class Utils {
return null;
}
private static final char ALM = (char)0x061c; // Arabic letter mark (ALM)
private static final char LRM = (char)0x200E; // Left-to-right mark (LRM)
private static final char RLM = (char)0x200F; // Right-to-left mark (RLM)
private static final char LRE = (char)0x202A; // Left-to-right embedding (LRE)
private static final char RLE = (char)0x202B; // Right-to-left embedding (RLE)
private static final char PDF = (char)0x202C; // Pop directional formatting (PDF)
private static final char LRO = (char)0x202D; // Left-to-right override (LRO)
private static final char RLO = (char)0x202E; // Right-to-left override (RLO)
private static final String CHARS_MUST_PDF = String.valueOf( LRE ) + RLE + LRO + RLO ;
private static final char LRI = (char)0x2066; // Left-to-right isolate (LRI)
private static final char RLI = (char)0x2067; // Right-to-left isolate (RLI)
private static final char FSI = (char)0x2068; // First strong isolate (FSI)
private static final char PDI = (char)0x2069; // Pop directional isolate (PDI)
private static final String CHARS_MUST_PDI = String.valueOf( LRI ) + RLI + FSI ;
public static String sanitizeBDI(String src){
LinkedList< Character > stack = null;
for( int i = 0, ie = src.length() ; i < ie ; ++ i ){
char c = src.charAt( i );
if( - 1 != CHARS_MUST_PDF.indexOf( c ) ){
if( stack == null ) stack = new LinkedList<>();
stack.add( PDF );
}else if( - 1 != CHARS_MUST_PDI.indexOf( c ) ){
if( stack == null ) stack = new LinkedList<>();
stack.add( PDI );
}else if( stack != null && ! stack.isEmpty() && stack.getLast() == c ){
stack.removeLast();
}
}
if( stack == null || stack.isEmpty() ) return src;
StringBuilder sb = new StringBuilder();
sb.append( src );
while( ! stack.isEmpty() ){
sb.append( (char) stack.removeLast() );
}
return sb.toString();
}
}

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<jp.juggler.subwaytooter.util.MyEditText
android:id="@+id/etText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
/>
</LinearLayout>
</ScrollView>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fadeScrollbars="false"
>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingBottom="8dp"
>
<Button
android:id="@+id/btnCopy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/copy"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/search_web"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnMuteWord"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/mute_word"
android:textAllCaps="false"
/>
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>

View File

@ -60,7 +60,7 @@
/>
<Button
android:id="@+id/btnStatusSendApp"
android:id="@+id/btnText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/btn_bg_transparent"
@ -70,7 +70,7 @@
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:text="@string/send_text"
android:text="@string/select_and_copy"
android:textAllCaps="false"
/>

View File

@ -273,7 +273,7 @@
<LinearLayout
android:id="@+id/llContents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>

View File

@ -270,7 +270,7 @@
<LinearLayout
android:id="@+id/llContents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>

View File

@ -97,6 +97,11 @@
android:id="@+id/nav_muted_app"
android:icon="?attr/ic_setting"
android:title="@string/muted_app"/>
<item
android:id="@+id/nav_muted_word"
android:icon="?attr/ic_setting"
android:title="@string/muted_word"/>
<item
android:id="@+id/nav_app_about"
android:icon="?attr/ic_info"

View File

@ -286,5 +286,13 @@
<string name="menu">Menu</string>
<string name="open_in_pseudo_account">open in pseudo account %1$s</string>
<string name="pick_image">pick image</string>
<string name="hashtag_and_visibility_not_match">this message contains hashtags, but message visibility is not public. normally hashtags are searchable only in public messages. Are you sure?</string>
<string name="copy">Copy</string>
<string name="mute_word">mute word</string>
<string name="muted_word">Muted words</string>
<string name="search_web">Web search</string>
<string name="select_and_copy">Select and copy</string>
<string name="send">Send</string>
<string name="word_was_muted">Word was muted.</string>
</resources>

View File

@ -282,4 +282,12 @@
<string name="menu">メニュー</string>
<string name="open_in_pseudo_account">疑似アカウント %1$s で開く</string>
<string name="pick_image">画像の選択</string>
<string name="hashtag_and_visibility_not_match">メッセージにハッシュタグが含まれていますが、公開範囲が公開ではありません。ハッシュタグは公開メッセージに含まれる場合のみ検索されることができます。続けてもよろしいですか?</string>
<string name="copy">コピー</string>
<string name="mute_word">単語をミュート</string>
<string name="muted_word">ミュートした単語</string>
<string name="search_web">Web検索</string>
<string name="select_and_copy">選択してコピー…</string>
<string name="send">共有</string>
<string name="word_was_muted">単語をミュートしました</string>
</resources>

View File

@ -28,7 +28,6 @@
<color name="Light_colorPostFormBackground">#eee</color>
<color name="Light_colorColumnStripBackground">#FFFFFFFF</color>
<color name="Light_colorProfileBackgroundMask">#C0FFFFFF</color>
@ -44,6 +43,7 @@
<color name="Light_colorColumnListDeleteBackground">#FF0000</color>
<color name="Light_colorColumnListDeleteText">#fff</color>
<color name="Light_colorRegexFilterError">#f00</color>
<color name="Light_colorColumnStripBackground">#000000</color>
<!-- Dark theme -->
@ -78,7 +78,6 @@
<color name="Dark_colorProfileBackgroundMask">#C0000000</color>
<color name="Dark_colorBackground">#000</color>
<color name="Dark_colorColumnStripBackground">#000</color>
<color name="Dark_colorPrimaryDark">#222</color>
@ -91,5 +90,6 @@
<color name="Dark_colorColumnListDeleteBackground">#FF0000</color>
<color name="Dark_colorColumnListDeleteText">#fff</color>
<color name="Dark_colorRegexFilterError">#f00</color>
<color name="Dark_colorColumnStripBackground">#000</color>
</resources>

View File

@ -282,4 +282,13 @@
<string name="image">image</string>
<string name="column_header">column header</string>
<string name="foreground_color">foreground color</string>
<string name="hashtag_and_visibility_not_match">this message contains hashtags, but message visibility is not public. normally hashtags are searchable only in public messages. Are you sure?</string>
<string name="copy">Copy</string>
<string name="send">Send</string>
<string name="mute_word">mute word</string>
<string name="select_and_copy">Select and copy</string>
<string name="search_web">Web search</string>
<string name="muted_word">Muted words</string>
<string name="word_was_muted">Word was muted.</string>
<string name="copy_complete">Clipboard updated.</string>
</resources>