diff --git a/app/build.gradle b/app/build.gradle
index 29ba307f..3e4e1ea7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,8 +10,8 @@ android {
applicationId "jp.juggler.subwaytooter"
minSdkVersion 21
targetSdkVersion 26
- versionCode 195
- versionName "1.9.5"
+ versionCode 196
+ versionName "1.9.6"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java
index 82cf4082..873a0997 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java
+++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java
@@ -88,6 +88,7 @@ import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.table.UserRelation;
import jp.juggler.subwaytooter.dialog.ActionsDialog;
+import jp.juggler.subwaytooter.util.ChromeTabOpener;
import jp.juggler.subwaytooter.util.LinkClickContext;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.MyClickableSpan;
@@ -1266,7 +1267,7 @@ public class ActMain extends AppCompatActivity
final String host = m.group( 1 );
final long status_id = Long.parseLong( m.group( 3 ), 10 );
// ステータスをアプリ内で開く
- openStatusOtherInstance( getDefaultInsertPosition(), null, uri.toString(), status_id, host, status_id );
+ openStatusOtherInstance( getDefaultInsertPosition(), uri.toString(), status_id, host, status_id );
}catch( Throwable ex ){
Utils.showToast( this, ex, "can't parse status id." );
}
@@ -1769,41 +1770,72 @@ public class ActMain extends AppCompatActivity
}
- static final Pattern reUrlHashTag = Pattern.compile( "\\Ahttps://([^/]+)/tags/([^?#]+)(?:\\z|[?#])" );
+ static final Pattern reUrlHashTag = Pattern.compile( "\\Ahttps://([^/]+)/tags/([^?#・\\s\\-+.,:;/]+)(?:\\z|[?#])" );
static final Pattern reUserPage = Pattern.compile( "\\Ahttps://([^/]+)/@([A-Za-z0-9_]+)(?:\\z|[?#])" );
static final Pattern reStatusPage = Pattern.compile( "\\Ahttps://([^/]+)/@([A-Za-z0-9_]+)/(\\d+)(?:\\z|[?#])" );
- public void openChromeTab( final int pos, @Nullable final SavedAccount access_info, final String url, boolean noIntercept ){
+ public void openChromeTab( @NonNull final ChromeTabOpener opener ){
+
try{
- log.d( "openChromeTab url=%s", url );
+ log.d( "openChromeTab url=%s", opener.url );
- if( ! noIntercept && access_info != null ){
+ if( opener.bAllowIntercept && opener.access_info != null ){
- // ハッシュタグをアプリ内で開く
- Matcher m = reUrlHashTag.matcher( url );
+ // ハッシュタグはいきなり開くのではなくメニューがある
+ Matcher m = reUrlHashTag.matcher( opener.url );
if( m.find() ){
+
// https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0
- String host = m.group( 1 );
- String tag_without_sharp = Uri.decode( m.group( 2 ) );
- if( access_info.isNA() || ! host.equalsIgnoreCase( access_info.host ) ){
- openHashTagOtherInstance( pos, access_info, url, host, tag_without_sharp );
- }else{
- openHashTag( pos, access_info, tag_without_sharp );
+ final String host = m.group( 1 );
+ final String tag_without_sharp = Uri.decode( m.group( 2 ) );
+ final String tag_with_sharp = "#" + tag_without_sharp;
+
+ ActionsDialog d = new ActionsDialog()
+ .addAction( getString( R.string.open_hashtag_column ), new Runnable() {
+ @Override public void run(){
+ openHashTagOtherInstance( opener.pos, opener.url, host, tag_without_sharp );
+ }
+ } )
+ .addAction( getString( R.string.open_in_browser ), new Runnable() {
+ @Override public void run(){
+ App1.openCustomTab( ActMain.this, opener.url );
+ }
+ } )
+ .addAction( getString( R.string.quote_hashtag_of, tag_with_sharp ), new Runnable() {
+ @Override public void run(){
+ openPost( tag_with_sharp + " " );
+ }
+ } );
+
+ if( opener.tag_list != null && opener.tag_list.size() > 1 ){
+ StringBuilder sb = new StringBuilder();
+ for( String s : opener.tag_list ){
+ if( sb.length() > 0 ) sb.append( ' ' );
+ sb.append( s );
+ }
+ final String tag_all = sb.toString();
+ d.addAction( getString( R.string.quote_all_hashtag_of, tag_all ), new Runnable() {
+ @Override public void run(){
+ openPost( tag_all + " " );
+ }
+ } );
}
+
+ d.show( ActMain.this, tag_with_sharp );
return;
}
// ステータスページをアプリから開く
- m = reStatusPage.matcher( url );
+ m = reStatusPage.matcher( opener.url );
if( m.find() ){
try{
// https://mastodon.juggler.jp/@SubwayTooter/(status_id)
final String host = m.group( 1 );
final long status_id = Long.parseLong( m.group( 3 ), 10 );
- if( access_info.isNA() || ! host.equalsIgnoreCase( access_info.host ) ){
- openStatusOtherInstance( pos, access_info, url, status_id, host, status_id );
+ if( opener.access_info.isNA() || ! host.equalsIgnoreCase( opener.access_info.host ) ){
+ openStatusOtherInstance( opener.pos, opener.url, status_id, host, status_id );
}else{
- openStatusLocal( pos, access_info, status_id );
+ openStatusLocal( opener.pos, opener.access_info, status_id );
}
}catch( Throwable ex ){
Utils.showToast( this, ex, "can't parse status id." );
@@ -1812,23 +1844,22 @@ public class ActMain extends AppCompatActivity
}
// ユーザページをアプリ内で開く
- m = reUserPage.matcher( url );
+ m = reUserPage.matcher( opener.url );
if( m.find() ){
// https://mastodon.juggler.jp/@SubwayTooter
final String host = m.group( 1 );
final String user = Uri.decode( m.group( 2 ) );
- openProfileByHostUser( pos, access_info, url, host, user );
+ openProfileByHostUser( opener.pos, opener.access_info, opener.url, host, user );
return;
}
}
- App1.openCustomTab( this,url);
-
+ App1.openCustomTab( this, opener.url );
}catch( Throwable ex ){
// log.trace( ex );
- log.e( ex, "openChromeTab failed. url=%s", url );
+ log.e( ex, "openChromeTab failed. url=%s", opener.url );
}
}
@@ -1841,18 +1872,16 @@ public class ActMain extends AppCompatActivity
// 他インスタンスのハッシュタグの表示
private void openHashTagOtherInstance(
int pos
- , @NonNull SavedAccount access_info
, @NonNull String url
, @NonNull String host
, @NonNull String tag_without_sharp
){
- openHashTagOtherInstance_sub( pos, access_info, url, host, tag_without_sharp );
+ openHashTagOtherInstance_sub( pos, url, host, tag_without_sharp );
}
// 他インスタンスのハッシュタグの表示
private void openHashTagOtherInstance_sub(
final int pos
- , @NonNull final SavedAccount access_info
, @NonNull final String url
, @NonNull final String host
, @NonNull final String tag_without_sharp
@@ -1883,7 +1912,7 @@ public class ActMain extends AppCompatActivity
// ブラウザで表示する
dialog.addAction( getString( R.string.open_web_on_host, host ), new Runnable() {
@Override public void run(){
- openChromeTab( pos, access_info, url, true );
+ App1.openCustomTab( ActMain.this, url );
}
} );
@@ -1958,63 +1987,32 @@ public class ActMain extends AppCompatActivity
}
}
final int pos = nextPosition( column );
+ @Nullable SavedAccount access_info = column == null ? null : column.access_info;
- // ハッシュタグはいきなり開くのではなくメニューがある
- Matcher m = reUrlHashTag.matcher( span.url );
- if( m.find() ){
- // https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0
- final String host = m.group( 1 );
- final String tag_with_sharp = span.text.startsWith( "#" ) ? span.text : "#" + Uri.decode( m.group( 2 ) );
- final String tag_without_sharp = tag_with_sharp.substring( 1 );
-
- ActionsDialog d = new ActionsDialog()
- .addAction( getString( R.string.open_hashtag_column ), new Runnable() {
- @Override public void run(){
- openHashTagOtherInstance( pos, (SavedAccount) span.lcc, span.url, host, tag_without_sharp );
- }
- } )
- .addAction( getString( R.string.quote_hashtag_of, tag_with_sharp ), new Runnable() {
- @Override public void run(){
- openPost( tag_with_sharp + " " );
- }
- } );
-
- final ArrayList< String > tag_list = new ArrayList<>();
- try{
- //noinspection ConstantConditions
- CharSequence cs = ( (TextView) view_orig ).getText();
- if( cs instanceof Spannable ){
- Spannable content = (Spannable) cs;
- for( MyClickableSpan s : content.getSpans( 0, content.length(), MyClickableSpan.class ) ){
- m = reUrlHashTag.matcher( s.url );
- if( m.find() ){
- String s_tag = s.text.startsWith( "#" ) ? s.text : "#" + Uri.decode( m.group( 2 ) );
- tag_list.add( s_tag );
- }
+ final ArrayList< String > tag_list = new ArrayList<>();
+
+ try{
+ //noinspection ConstantConditions
+ CharSequence cs = ( (TextView) view_orig ).getText();
+ if( cs instanceof Spannable ){
+ Spannable content = (Spannable) cs;
+ for( MyClickableSpan s : content.getSpans( 0, content.length(), MyClickableSpan.class ) ){
+ Matcher m = reUrlHashTag.matcher( s.url );
+ if( m.find() ){
+ String s_tag = s.text.startsWith( "#" ) ? s.text : "#" + Uri.decode( m.group( 2 ) );
+ tag_list.add( s_tag );
}
}
- }catch( Throwable ex ){
- log.trace( ex );
}
- if( tag_list.size() > 1 ){
- StringBuilder sb = new StringBuilder();
- for( String s : tag_list ){
- if( sb.length() > 0 ) sb.append( ' ' );
- sb.append( s );
- }
- final String tag_all = sb.toString();
- d.addAction( getString( R.string.quote_all_hashtag_of, tag_all ), new Runnable() {
- @Override public void run(){
- openPost( tag_all + " " );
- }
- } );
- }
-
- d.show( ActMain.this, tag_with_sharp );
- return;
+ }catch( Throwable ex ){
+ log.trace( ex );
}
- openChromeTab( pos, (SavedAccount) span.lcc, span.url, false );
+ new ChromeTabOpener( ActMain.this, pos, span.url )
+ .accessInfo( access_info )
+ .tagList( tag_list )
+ .open();
+
}
};
@@ -2120,7 +2118,7 @@ public class ActMain extends AppCompatActivity
Utils.showToast( ActMain.this, true, result.error );
// 仕方ないのでchrome tab で開く
- openChromeTab( pos, access_info, who_url, true );
+ App1.openCustomTab( ActMain.this, who_url );
}
}
@@ -2175,7 +2173,7 @@ public class ActMain extends AppCompatActivity
return;
}
// ダメならchromeで開く
- openChromeTab( pos, access_info, url, true );
+ App1.openCustomTab( ActMain.this, url );
}
} );
}else{
@@ -2192,7 +2190,7 @@ public class ActMain extends AppCompatActivity
if( ! SavedAccount.hasRealAccount( log ) ){
// 疑似アカウントではユーザ情報APIを呼べないし検索APIも使えない
// chrome tab で開くしかない
- openChromeTab( pos, access_info, url, true );
+ App1.openCustomTab( ActMain.this, url );
}else{
// アカウントを選択して開く
AccountPicker.pick( this, false, false
@@ -2559,18 +2557,18 @@ public class ActMain extends AppCompatActivity
public void openStatus( int pos, @NonNull SavedAccount access_info, @NonNull TootStatusLike status ){
if( access_info.isNA() || ! access_info.host.equalsIgnoreCase( status.host_access ) ){
- openStatusOtherInstance( pos, access_info, status );
+ openStatusOtherInstance( pos, status );
}else{
openStatusLocal( pos, access_info, status.id );
}
}
- public void openStatusOtherInstance( int pos, @NonNull SavedAccount access_info, @Nullable TootStatusLike status ){
+ public void openStatusOtherInstance( int pos, @Nullable TootStatusLike status ){
// アカウント情報がないと出来ないことがある
if( status == null || status.account == null ) return;
if( status instanceof MSPToot ){
- openStatusOtherInstance( pos, access_info, status.url
+ openStatusOtherInstance( pos, status.url
, status.id
, null, - 1L
);
@@ -2580,7 +2578,7 @@ public class ActMain extends AppCompatActivity
// uri から投稿元タンスでのステータスIDを調べる
long status_id_original = TootStatusLike.parseStatusId( status );
- openStatusOtherInstance( pos, access_info, status.url
+ openStatusOtherInstance( pos, status.url
, status_id_original
, null, - 1L
);
@@ -2588,7 +2586,7 @@ public class ActMain extends AppCompatActivity
}else if( status instanceof TootStatus ){
if( status.host_original.equals( status.host_access ) ){
// TLアカウントのホストとトゥートのアカウントのホストが同じ場合
- openStatusOtherInstance( pos, access_info, status.url
+ openStatusOtherInstance( pos, status.url
, status.id
, null, - 1L
);
@@ -2597,7 +2595,7 @@ public class ActMain extends AppCompatActivity
// uri から投稿元タンスでのステータスIDを調べる
long status_id_original = TootStatusLike.parseStatusId( status );
- openStatusOtherInstance( pos, access_info, status.url
+ openStatusOtherInstance( pos, status.url
, status_id_original
, status.host_access, status.id
);
@@ -2607,7 +2605,6 @@ public class ActMain extends AppCompatActivity
void openStatusOtherInstance(
final int pos
- , @Nullable final SavedAccount access_info
, @NonNull final String url
, final long status_id_original
, final String host_access, final long status_id_access
@@ -2619,7 +2616,7 @@ public class ActMain extends AppCompatActivity
// 選択肢:ブラウザで表示する
dialog.addAction( getString( R.string.open_web_on_host, host_original ), new Runnable() {
@Override public void run(){
- openChromeTab( pos, access_info, url, true );
+ App1.openCustomTab( ActMain.this, url );
}
} );
diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.java b/app/src/main/java/jp/juggler/subwaytooter/App1.java
index 47bf05d6..d670d0d2 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/App1.java
+++ b/app/src/main/java/jp/juggler/subwaytooter/App1.java
@@ -525,7 +525,8 @@ public class App1 extends Application {
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl( activity, Uri.parse( url ) );
}catch( Throwable ex ){
- log.e( ex, "openCustomTab: failed." );
+ log.trace( ex );
+ Utils.showToast(activity,false,"can't open browser app");
}
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java
index 35d406b5..955f2d37 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java
+++ b/app/src/main/java/jp/juggler/subwaytooter/DlgContextMenu.java
@@ -332,7 +332,7 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener {
case R.id.btnStatusWebPage:
if( status != null ){
- activity.openChromeTab( pos, access_info, status.url, true );
+ App1.openCustomTab( activity, status.url );
}
break;
@@ -356,7 +356,7 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener {
case R.id.btnConversationAnotherAccount:
if( status != null ){
- activity.openStatusOtherInstance( pos, access_info, status );
+ activity.openStatusOtherInstance( pos, status );
}
break;
@@ -478,7 +478,7 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener {
case R.id.btnAccountWebPage:
if( who != null ){
- activity.openChromeTab( pos, access_info, who.url, true );
+ App1.openCustomTab( activity,who.url );
}
break;
@@ -559,7 +559,8 @@ class DlgContextMenu implements View.OnClickListener, View.OnLongClickListener {
case R.id.btnAvatarImage:
if( who != null ){
String url = ! TextUtils.isEmpty( who.avatar ) ? who.avatar : who.avatar_static;
- if( url != null ) activity.openChromeTab( pos, access_info, url, true );
+ if( url != null ) App1.openCustomTab( activity,url );
+ // FIXME: 設定によっては内蔵メディアビューアで開けないか?
}
break;
diff --git a/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderInstance.java b/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderInstance.java
index f86cf454..36c81d5a 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderInstance.java
+++ b/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderInstance.java
@@ -148,7 +148,7 @@ class HeaderViewHolderInstance extends HeaderViewHolderBase implements View.OnCl
case R.id.btnInstance:
if( instance != null && instance.uri != null ){
- activity.openChromeTab( activity.nextPosition( column ), column.access_info, "https://" + instance.uri + "/about", true );
+ App1.openCustomTab( activity, "https://" + instance.uri + "/about");
}
break;
diff --git a/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderProfile.java b/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderProfile.java
index 3b5df510..f749cb30 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderProfile.java
+++ b/app/src/main/java/jp/juggler/subwaytooter/HeaderViewHolderProfile.java
@@ -244,7 +244,7 @@ class HeaderViewHolderProfile extends HeaderViewHolderBase implements View.OnCli
case R.id.tvRemoteProfileWarning:
if( who != null ){
// 強制的にブラウザで開く
- activity.openChromeTab( activity.nextPosition( column ), access_info, who.url, true );
+ App1.openCustomTab( activity, who.url );
}
break;
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java
index bf9083f4..c8bf5988 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java
+++ b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java
@@ -785,8 +785,11 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
break;
case R.id.ivCardThumbnail:
- if( status != null && status.card != null ){
- activity.openChromeTab( pos, access_info, status.card.url, false );
+ if( status != null
+ && status.card != null
+ && !TextUtils.isEmpty( status.card.url )
+ ){
+ App1.openCustomTab( activity, status.card.url);
}
break;
@@ -911,7 +914,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
private void clickMedia( int i ){
try{
if( status instanceof MSPToot ){
- activity.openStatusOtherInstance( activity.nextPosition( column ), access_info, status );
+ activity.openStatusOtherInstance( activity.nextPosition( column ), status );
return;
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java
index c48c3a53..73fafd93 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java
+++ b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java
@@ -205,7 +205,7 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener {
switch( v.getId() ){
case R.id.btnConversation:
- activity.openStatusOtherInstance( activity.nextPosition( column ), access_info, status );
+ activity.openStatusOtherInstance( activity.nextPosition( column ), status );
break;
case R.id.btnBoost:
diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/ChromeTabOpener.java b/app/src/main/java/jp/juggler/subwaytooter/util/ChromeTabOpener.java
new file mode 100644
index 00000000..553628e6
--- /dev/null
+++ b/app/src/main/java/jp/juggler/subwaytooter/util/ChromeTabOpener.java
@@ -0,0 +1,46 @@
+package jp.juggler.subwaytooter.util;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+
+import jp.juggler.subwaytooter.ActMain;
+import jp.juggler.subwaytooter.table.SavedAccount;
+
+public class ChromeTabOpener {
+ @NonNull public final ActMain activity;
+ @NonNull public final String url;
+ public final int pos;
+
+ public ChromeTabOpener( @NonNull ActMain activity, int pos, @NonNull String url ){
+ this.activity = activity;
+ this.pos = pos;
+ this.url = url;
+ }
+
+ public void open(){
+ activity.openChromeTab( this );
+ }
+
+ @Nullable public SavedAccount access_info;
+
+ public ChromeTabOpener accessInfo( @Nullable SavedAccount access_info ){
+ this.access_info = access_info;
+ return this;
+ }
+
+ public boolean bAllowIntercept = true;
+ // public ChromeTabOpener allowIntercept( boolean v){
+ // this.bAllowIntercept = v;
+ // return this;
+ // }
+
+ @Nullable public ArrayList< String > tag_list;
+
+ public ChromeTabOpener tagList( ArrayList< String > tag_list ){
+ this.tag_list = tag_list;
+ return this;
+ }
+
+}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.java b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.java
index 3a15b3b9..53c62dce 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.java
+++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.java
@@ -82,7 +82,7 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
, Pattern.CASE_INSENSITIVE
);
- private static final Pattern reCharsNotTag = Pattern.compile( "[\\s\\-+.,:;/]" );
+ private static final Pattern reCharsNotTag = Pattern.compile( "[・\\s\\-+.,:;/]" );
private static final Pattern reCharsNotEmoji = Pattern.compile( "[^0-9A-Za-z_-]" );
public String content;
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1914f4a0..2c15a03c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -577,4 +577,5 @@
Can\'t repeat downloading same URL in a few second.
×%3$.1f\n%1$d×%2$d
Media uploading has not completed yet.
+