Links and tags in statuses are now clickable and open suitable pages.
Mentions are also, incidentally, but still link to the account page for that user in the browser. This should be changed to an in-app account page when that's finished, but it's actually fairly suitable fallback behaviour for now.
This commit is contained in:
parent
83f8b4303c
commit
dbb2663882
|
@ -34,6 +34,7 @@
|
|||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
<activity android:name=".ViewVideoActivity" />
|
||||
<activity android:name=".ViewThreadActivity" />
|
||||
<activity android:name=".ViewTagActivity" />
|
||||
<service
|
||||
android:name=".NotificationService"
|
||||
android:description="@string/notification_service_description"
|
||||
|
|
|
@ -198,4 +198,8 @@ public class NotificationsFragment extends SFragment implements
|
|||
Notification notification = adapter.getItem(position);
|
||||
super.viewThread(notification.getStatus());
|
||||
}
|
||||
|
||||
public void onViewTag(String tag) {
|
||||
super.viewTag(tag);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -244,4 +244,10 @@ public class SFragment extends Fragment {
|
|||
intent.putExtra("id", status.getId());
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
protected void viewTag(String tag) {
|
||||
Intent intent = new Intent(getContext(), ViewTagActivity.class);
|
||||
intent.putExtra("hashtag", tag);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,4 +24,5 @@ public interface StatusActionListener {
|
|||
void onMore(View view, final int position);
|
||||
void onViewMedia(String url, Status.MediaAttachment.Type type);
|
||||
void onViewThread(int position);
|
||||
void onViewTag(String tag);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,13 @@ package com.keylesspalace.tusky;
|
|||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.method.MovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
|
@ -28,7 +34,10 @@ import android.widget.TextView;
|
|||
import com.android.volley.toolbox.ImageLoader;
|
||||
import com.android.volley.toolbox.NetworkImageView;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||
private View container;
|
||||
|
@ -89,8 +98,32 @@ public class StatusViewHolder extends RecyclerView.ViewHolder {
|
|||
username.setText(usernameText);
|
||||
}
|
||||
|
||||
public void setContent(Spanned content) {
|
||||
this.content.setText(content);
|
||||
public void setContent(Spanned content, final StatusActionListener listener) {
|
||||
// Redirect URLSpan's in the status content to the listener for viewing tag pages.
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(content);
|
||||
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
|
||||
for (URLSpan span : urlSpans) {
|
||||
int start = builder.getSpanStart(span);
|
||||
int end = builder.getSpanEnd(span);
|
||||
int flags = builder.getSpanFlags(span);
|
||||
CharSequence tag = builder.subSequence(start, end);
|
||||
if (tag.charAt(0) == '#') {
|
||||
final String viewTag = tag.subSequence(1, tag.length()).toString();
|
||||
ClickableSpan newSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
listener.onViewTag(viewTag);
|
||||
}
|
||||
};
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(newSpan, start, end, flags);
|
||||
}
|
||||
}
|
||||
// Set the contents.
|
||||
this.content.setText(builder);
|
||||
// Make links clickable.
|
||||
this.content.setLinksClickable(true);
|
||||
this.content.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
public void setAvatar(String url) {
|
||||
|
@ -203,6 +236,7 @@ public class StatusViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
public void setupButtons(final StatusActionListener listener, final int position) {
|
||||
|
||||
replyButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
@ -239,9 +273,8 @@ public class StatusViewHolder extends RecyclerView.ViewHolder {
|
|||
setDisplayName(status.getDisplayName());
|
||||
setUsername(status.getUsername());
|
||||
setCreatedAt(status.getCreatedAt());
|
||||
setContent(status.getContent());
|
||||
setContent(status.getContent(), listener);
|
||||
setAvatar(status.getAvatar());
|
||||
setContent(status.getContent());
|
||||
setReblogged(status.getReblogged());
|
||||
setFavourited(status.getFavourited());
|
||||
String rebloggedByUsername = status.getRebloggedByUsername();
|
||||
|
|
|
@ -47,12 +47,14 @@ public class TimelineFragment extends SFragment implements
|
|||
HOME,
|
||||
MENTIONS,
|
||||
PUBLIC,
|
||||
TAG,
|
||||
}
|
||||
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private RecyclerView recyclerView;
|
||||
private TimelineAdapter adapter;
|
||||
private Kind kind;
|
||||
private String hashtag;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private EndlessOnScrollListener scrollListener;
|
||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
|
@ -65,11 +67,24 @@ public class TimelineFragment extends SFragment implements
|
|||
return fragment;
|
||||
}
|
||||
|
||||
public static TimelineFragment newInstance(Kind kind, String hashtag) {
|
||||
TimelineFragment fragment = new TimelineFragment();
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putString("kind", kind.name());
|
||||
arguments.putString("hashtag", hashtag);
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
kind = Kind.valueOf(getArguments().getString("kind"));
|
||||
Bundle arguments = getArguments();
|
||||
kind = Kind.valueOf(arguments.getString("kind"));
|
||||
if (kind == Kind.TAG) {
|
||||
hashtag = arguments.getString("hashtag");
|
||||
}
|
||||
|
||||
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
||||
|
||||
|
@ -103,20 +118,24 @@ public class TimelineFragment extends SFragment implements
|
|||
adapter = new TimelineAdapter(this, this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
|
||||
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {}
|
||||
if (kind != Kind.TAG) {
|
||||
TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
|
||||
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {}
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
jumpToTop();
|
||||
}
|
||||
};
|
||||
layout.addOnTabSelectedListener(onTabSelectedListener);
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
jumpToTop();
|
||||
}
|
||||
};
|
||||
layout.addOnTabSelectedListener(onTabSelectedListener);
|
||||
}
|
||||
|
||||
sendFetchTimelineRequest();
|
||||
|
||||
|
@ -125,8 +144,10 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
|
||||
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
|
||||
if (kind != Kind.TAG) {
|
||||
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
|
||||
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
|
||||
}
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
|
@ -151,6 +172,11 @@ public class TimelineFragment extends SFragment implements
|
|||
endpoint = getString(R.string.endpoint_timelines_public);
|
||||
break;
|
||||
}
|
||||
case TAG: {
|
||||
assert(hashtag != null);
|
||||
endpoint = String.format(getString(R.string.endpoint_timelines_tag), hashtag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
String url = "https://" + domain + endpoint;
|
||||
if (fromId != null) {
|
||||
|
@ -250,4 +276,8 @@ public class TimelineFragment extends SFragment implements
|
|||
public void onViewThread(int position) {
|
||||
super.viewThread(adapter.getItem(position));
|
||||
}
|
||||
|
||||
public void onViewTag(String tag) {
|
||||
super.viewTag(tag);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is part of Tusky.
|
||||
*
|
||||
* Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky. If not, see
|
||||
* <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
|
||||
public class ViewTagActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_view_tag);
|
||||
|
||||
String hashtag = getIntent().getStringExtra("hashtag");
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar bar = getSupportActionBar();
|
||||
if (bar != null) {
|
||||
bar.setTitle(String.format(getString(R.string.title_tag), hashtag));
|
||||
}
|
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
Fragment fragment = TimelineFragment.newInstance(TimelineFragment.Kind.TAG, hashtag);
|
||||
fragmentTransaction.add(R.id.fragment_container, fragment);
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
}
|
|
@ -140,4 +140,8 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
|
|||
public void onViewThread(int position) {
|
||||
super.viewThread(adapter.getItem(position));
|
||||
}
|
||||
|
||||
public void onViewTag(String tag) {
|
||||
super.viewTag(tag);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/activity_view_thread"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="com.keylesspalace.tusky.ViewTagActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/overlay_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</RelativeLayout>
|
|
@ -5,6 +5,8 @@
|
|||
<string name="oauth_redirect_host">oauth2redirect</string>
|
||||
<string name="preferences_file_key">com.keylesspalace.tusky.PREFERENCES</string>
|
||||
|
||||
<string name="content_uri_format_tag">content://com.keylesspalace.tusky.viewtagactivity/%s</string>
|
||||
|
||||
<string name="endpoint_status">/api/v1/statuses</string>
|
||||
<string name="endpoint_media">/api/v1/media</string>
|
||||
<string name="endpoint_timelines_home">/api/v1/timelines/home</string>
|
||||
|
@ -53,6 +55,7 @@
|
|||
<string name="title_notifications">Notifications</string>
|
||||
<string name="title_public">Public</string>
|
||||
<string name="title_thread">Thread</string>
|
||||
<string name="title_tag">#%s</string>
|
||||
|
||||
<string name="status_username_format">\@%s</string>
|
||||
<string name="status_boosted_format">%s boosted</string>
|
||||
|
|
Loading…
Reference in New Issue